When developers prototype their first autonomous AI agent, they usually focus on tool definitions: creating clean JSON schemas, wiring up prompt instructions, and watching the model successfully call a function.
Everything looks great in single-turn evals.
Then the agent is deployed into a real multi-turn workflow—monitoring support queues, processing webhooks, or syncing transactional state. Within hours, two alarming things happen:
- Your token usage graphs spike parabolically, blowing past your monthly inference budget.
- The agent starts hallucinating, forgetting earlier instructions, or hitting
context_length_exceedederrors.
The culprit is almost never the system prompt. It is what I call the Context Window Tax: the compounding cost of bloated, raw tool responses pinned in conversation history.
The Anatomy of the Multi-Turn Token Penalty
In a traditional web application, an API call is ephemeral. A microservice queries a database, gets 500 KB of JSON, extracts two fields, renders an HTML template, and garbage-collects the rest.
Autonomous agents do not have garbage collection for context.
Under the Model Context Protocol (MCP) and modern agent loop architectures, when a model triggers a tool call, the tool's return payload is appended to the conversation message history as a tool_result turn:
Turn 1 [User]: "Check if we received the invoice confirmation email from vendor ACME." Turn 2 [Agent]: call_tool -> get_latest_email({ query: "from:acme" }) Turn 3 [Tool Ret]: { ... 4,200 tokens of raw headers, MIME boundaries, CSS ... } Turn 4 [Agent]: "Found it. Now checking customer account ID in Postgres." Turn 5 [Agent]: call_tool -> get_account({ id: "cust_9981" }) Turn 6 [Tool Ret]: { ... 2,500 tokens of raw database columns ... } ... Turn 15 [Agent]: Final summary. Notice what happened to that 4,200-token email payload from Turn 3? It was resent to the LLM on Turn 4, Turn 5, Turn 6, and all the way through Turn 15.
That single bloated tool call didn't cost 4,200 tokens. It cost 4,200 × 12 turns = 50,400 input tokens.
If your agent runs 200 tasks a day, a single unoptimized tool response compounds into tens of millions of reprocessed tokens every week.
The Anti-Pattern: Dumping Raw Upstream Payloads
The most common anti-pattern in MCP servers and custom agent tools is lazy serialization:
// ❌ THE DANGEROUS LAZY RETURN server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "fetch_customer_data") { const rawCustomer = await db.query("SELECT * FROM customers WHERE id = $1", [args.id]); // Dumping the entire database row + metadata into context: return { content: [{ type: "text", text: JSON.stringify(rawCustomer) }] }; } }); A raw SQL row or third-party API payload contains dozens of fields the LLM will never use: internal UUIDs, password hash nulls, legacy migration flags, tracing headers, and timestamps.
Email is even worse. A standard raw email object contains:
- 40+ RFC headers (
Received: by...,DKIM-Signature,X-Spam-Status,Message-ID) - Multipart MIME boundary delimiters
- Inlined CSS styles and tracking pixel
<img>tags - Redundant raw HTML tables alongside plain text
That easily totals 15 KB to 30 KB of raw text (~4,000 to 8,000 tokens) when the agent's LLM only needed three things: sender, subject, and cleanTextBody.
The Prompt Cache Invalidation Hazard
Modern frontier models (Anthropic Claude 3.5 Sonnet, OpenAI GPT-4o) offer massive prompt caching discounts (up to 90% cost reduction on cached prefix tokens).
However, prompt caching relies on prefix matching. If turn 3 injects a tool response that includes non-deterministic fields—such as ephemeral request IDs, microsecond timestamps, or un-sorted JSON keys—the entire cache prefix after that point is invalidated.
Not only are you paying for bloated tokens, but you are also paying full price for them on every turn instead of taking advantage of cached context.
Building an Inbound Token Optimizer in TypeScript
The solution is to enforce deterministic Inbound Token Optimization at the tool and protocol boundary. Your tools must act as strict projection filters before returning data to the model.
Here is a clean, production-grade implementation of a Token Optimizer pattern for an MCP tool in TypeScript:
import { z } from "zod"; interface EmailPayload { from: string; to: string; subject: string; headers: Record<string, string>; rawHtml?: string; textBody?: string; } interface OptimizedEmailResult { from: string; subject: string; body: string; tokenReductionRatio: string; } /** * Optimizes inbound email payloads by stripping RFC headers, * converting HTML to lean text, and truncating under a hard token budget. */ export function optimizeEmailForContext( raw: EmailPayload, maxCharBudget: number = 2000 ): OptimizedEmailResult { const initialLength = JSON.stringify(raw).length; // 1. Prioritize clean text, strip HTML markup if text is missing let cleanBody = raw.textBody || ""; if (!cleanBody && raw.rawHtml) { cleanBody = raw.rawHtml .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "") .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "") .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") .trim(); } // 2. Collapse excessive whitespace and line breaks cleanBody = cleanBody.replace(/\n{3,}/g, "\n\n").trim(); // 3. Enforce deterministic budget truncation if (cleanBody.length > maxCharBudget) { cleanBody = cleanBody.slice(0, maxCharBudget) + " ... [TRUNCATED FOR CONTEXT BUDGET]"; } const optimized = { from: raw.from, subject: raw.subject, body: cleanBody, }; const finalLength = JSON.stringify(optimized).length; const reduction = (((initialLength - finalLength) / initialLength) * 100).toFixed(1); return { ...optimized, tokenReductionRatio: `${reduction}% saved`, }; } Self-Healing Error Handling with Zod safeParse
As highlighted by community engineers, never let invalid arguments throw an unhandled error out of your tool. Return { isError: true } so the LLM gets actionable feedback in its scratchpad:
const FetchEmailArgsSchema = z.object({ messageId: z.string().min(1, "messageId must not be empty"), maxTokens: z.number().int().positive().default(1000) }); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "fetch_inbound_email") { const parseResult = FetchEmailArgsSchema.safeParse(request.params.arguments); // Self-healing error pattern: Let the model recover gracefully if (!parseResult.success) { return { isError: true, content: [{ type: "text", text: `[SCHEMA ERROR]: Invalid tool arguments: ${parseResult.error.issues.map(i => i.message).join(", ")}. Please correct and retry.` }] }; } const { messageId, maxTokens } = parseResult.data; const rawEmail = await getRawEmailFromStore(messageId); const optimized = optimizeEmailForContext(rawEmail, maxTokens * 4); return { content: [{ type: "text", text: JSON.stringify(optimized, null, 2) }] }; } }); The Production Mandate
As autonomous agents transition from novelty scripts to production background workers, managing the context window boundary becomes as critical as database connection pooling or API rate limiting.
When designing tools for agentic workflows, remember these core rules:
- Never return raw database rows or full upstream API responses. Always project down to the minimum viable fields the LLM needs to make its next decision.
- Sanitize non-deterministic data (dynamic timestamps, random IDs) from tool outputs to maximize prefix prompt cache hit rates.
- Use self-healing error results (
isError: true) instead of protocol-level exceptions so the agent can self-correct parameters without human intervention.
This exact multi-turn token compounding is why purpose-built agent infrastructure handles optimization natively. For instance, in email delivery for autonomous AI agents, platforms like SadaSend implement an agent-native architecture: pairing a transactional API and hosted MCP server with a built-in Inbound Token Optimizer that strips RFC headers, MIME boundaries, and tracking clutter at the wire level—cutting token consumption by ~85% before the payload ever touches the model's prompt history.
Filter at the protocol boundary, keep your tool payloads lean, and stop paying the Context Window Tax on every turn.

