I was watching my MCP agent run through a 40-step workflow last month when I noticed something strange in the token counter. Each step was burning 12,000–15,000 tokens — but the actual task was trivial: look up a price, compare two dates, return a boolean.
The bottleneck wasn't the model. It wasn't the tools. It was the schema.
Every single turn, my agent was re-sending 7,000+ tokens of JSON tool definitions to the LLM — even when only 2 tools were relevant to the current step. After 40 turns, I'd spent more tokens describing what the tools are than actually running them.
This is the tax nobody talks about with MCP in production.
How MCP Sends Tool Schemas (And Why It's Expensive)
When an MCP client connects to a server, it receives a list of tools — each with a name, description, and full JSON Schema input object. Here's a typical MCP tool definition that looks small:
{
"name": "query_database",
"description": "Execute a read-only SQL query against the analytics database",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL SELECT statement to execute"
},
"params": {
"type": "array",
"description": "Query parameters for prepared statements"
}
},
"required": ["query"]
}
}
Looks harmless. Now multiply that by 40 tools. Now add nested objects, $ref pointers, and oneOf unions that some MCP servers love to vomit into their schemas.
The raw JSON for a well-documented MCP server with 40 tools easily hits 15,000–30,000 tokens per full schema push.
The model doesn't care about most of those tools on any given turn. But it receives all of them anyway, because MCP's spec as commonly implemented has no built-in filtering mechanism.
Measuring the Actual Cost
Here's the thing — you can't fix what you haven't measured. I logged the prompt size on every turn for a week. The results:
- Median tokens per turn (my workload): 8,200
- Median tokens from user message: 340
- Median tokens from tool schemas: 7,860
I was spending 96% of my token budget on tool descriptions that the model couldn't act on that turn.
At $0.01/1K tokens for a mid-tier model, a 40-step workflow was costing me $3.28 in tool schema overhead alone. Scale that to 1,000 workflows a day and you're looking at $3,280/day in wasted tokens — just because nobody thought to filter the schema list.
The Three Fixes That Actually Work
After three weeks of experimenting, I landed on three patterns. They're not mutually exclusive — I use all three together.
Fix 1: Schema Pruning at the Client Level
Before sending tool schemas to the LLM, filter them by relevance to the current task. You don't need to implement this in the MCP server — do it in your client wrapper:
def get_relevant_tools(all_tools: list, current_task: str, top_k: int = 8) -> list:
"""Return only the top-k most relevant tools for this task turn."""
embeddings = embed([t["description"] for t in all_tools])
task_embedding = embed(current_task)
scores = cosine_similarity([task_embedding], embeddings)[0]
top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k]
return [all_tools[i] for i in top_indices]
This alone cut my median tokens per turn from 8,200 to 1,400 — a 83% reduction in schema overhead.
The tradeoff: you need a fast embedding model for filtering (I used a lightweight local model, ~0.3ms latency). And you might accidentally filter out a tool the model needed but couldn't predict from context.
Fix 2: Compress Schemas with Domain-Specific Short Names
One of the biggest token sinks is verbose descriptions in tool schemas. A description: "Execute a read-only SQL query..." field sounds helpful but adds up fast when you have 40 tools.
I wrote a schema compressor that strips redundant descriptions and replaces them with one-word domain terms:
import json
def compress_schema(tool: dict) -> dict:
"""Strip verbose descriptions, keep only what the model needs."""
return {
"name": tool["name"],
"description": tool["description"].split(".")[0] + ".", # first sentence only
"inputSchema": compress_input_schema(tool["inputSchema"])
}
def compress_input_schema(schema: dict) -> dict:
"""Recursively remove descriptions from nested properties."""
if "properties" in schema:
return {
"type": schema.get("type", "object"),
"properties": {
k: {"type": v.get("type")} # drop descriptions
for k, v in schema["properties"].items()
},
"required": schema.get("required", [])
}
return {"type": schema.get("type", "any")}
This took my 7,860 median schema tokens down to 2,100. The model lost almost nothing — it already knows what a string type is.
Fix 3: Turn-Aware Tool Registration
The nuclear option: don't send tools that are categorically useless in the current workflow phase.
I organized my agent's tasks into phases (data_fetch, analysis, synthesis, output) and registered only phase-relevant tools:
PHASE_TOOLS = {
"data_fetch": ["query_database", "fetch_api", "read_file"],
"analysis": ["run_sql_aggregation", "filter_results", "compute_metrics"],
"synthesis": ["format_report", "generate_summary", "write_output"],
"output": ["send_email", "post_webhook", "update_record"]
}
def get_tools_for_phase(phase: str) -> list:
return [TOOL_REGISTRY[name] for name in PHASE_TOOLS.get(phase, [])]
This requires you to structure your agent workflow deliberately, but it gives you the cleanest result. My 40-step workflow now uses 3–6 tools per turn instead of 40.
Combining All Three: The Results
After applying all three fixes together, here's what I measured over 500 production workflow runs:
| Metric | Before | After | Change |
|---|---|---|---|
| Median tokens/turn | 8,200 | 1,100 | -87% |
| Median cost/workflow | $0.082 | $0.011 | -87% |
| Tool call latency (p50) | 340ms | 290ms | -15% |
| Task success rate | 94.2% | 94.8% | +0.6pp |
The success rate actually went up slightly — I think because the model had a shorter context to reason over and made fewer errors parsing schema ambiguity.
What I Learned
The first thing I learned is that MCP's schema overhead is a real, measurable cost that nobody ships benchmarks for. Vendors advertise token counts for inference but nobody talks about token counts for tool description.
The second thing: you don't need to compromise on tool quality to reduce overhead. Stripping verbose descriptions from schemas sounds like it would hurt — it doesn't. The LLM is very good at inferring parameter intent from names alone.
The third thing is the most important: measure before you optimize. The specific overhead in your system depends entirely on how many tools you have, how verbose your schemas are, and what your workflow looks like. What worked for me might be wrong for you. Run the numbers first.
I now log schema token cost on every turn as a first-class metric. It's become part of my agent's standard observability stack alongside latency and error rate. If you have an MCP agent in production, I'd recommend doing the same — you might be surprised what you find.
Have a different approach to MCP token efficiency? I'd genuinely like to hear it — especially if you've worked with larger tool registries (100+ tools) where filtering gets harder.
Top comments (0)