MCP Token Cost: I Cut My MCP Server From 15 Tools to 4
By Sarvar Nadaf • 9 min read
Here is a number that should bother you. GitHub's official MCP server loads about 55,000 tokens across 93 tool definitions before you ask it a single question. On a 200K context window that is more than a quarter of your budget, spent, before the agent does any work.
I did not believe it until I measured my own. So I built a small AWS MCP server the way most people build them, wired it into Kiro, and asked it one question: "where am I wasting money in this account?" It took seven tool calls to answer. Then I rebuilt the exact same server following AWS's own tool-design playbook. Same account, same question. Two tool calls. Fifteen tools became four. The tool schemas shrank by 88%.
This is the story of that rebuild, with the numbers on screen. It is a practical look at MCP token cost: where it comes from, how to measure it, and how to cut it. Not a hello-world server with an add_numbers tool. A read-only AWS cost-triage server that finds genuine wasted spend, shipped in two versions in the same repo.
The full code is on GitHub, both servers in one place so you can diff them:
A read-only AWS cost-triage MCP server, shipped bloated (15 tools) and lean (4 tools), to demonstrate MCP tool design and cut token cost. Same account, same question: 7 tool calls down to 2.
🪶 aws-lean-mcp
The same AWS MCP server, built twice (bloated, 15 tools, and lean, 4 tools), so you can watch your AI agent get cheaper and smarter, live, in Kiro's /context view.
⭐ If this helped you design better MCP tools, give it a star! It helps others find it.
You have wired up a few MCP servers in Kiro, Claude Code, or Cursor. Maybe you built one. It worked in the demo, and then your agent started feeling slower, dumber, and more expensive as you added tools. You have seen the phrase "context bloat" and want to know if it is real, how bad it is, and what to do about it.
If you can read a Python function and a JSON file, you can follow this.
Bloat. Every tool definition loads into the context window on every call, whether the model uses it or not. Each tool schema runs 550 to 1,400 tokens, so fifty tools can mean 30,000 to 70,000 tokens of definitions before any work starts. A typical Claude Code session with five to ten MCP servers installed burns 50,000 to 67,000 tokens before you type your first prompt. You pay that tax on every turn.
Confusion. As the context fills up and your tools start looking alike (describe_volumes, describe_addresses, describe_snapshots, all thin, all similar), the model picks the wrong one, passes the wrong parameters, gets an empty result, and retries. Each retry adds more tokens. Bloat causes confusion, confusion causes more bloat. A spiral.
The root cause of both is the most common way people build MCP servers: take an API, wrap each endpoint as its own tool, ship 30 of them. It feels complete. It is a trap.
2. The anti-pattern, on purpose (v1: 15 tools)
To fix something you need something broken. So server_v1_bloated.py is built the way most servers really ship. Fifteen tools, each a thin 1:1 wrapper over one boto3 call.
@mcp.tool()defdescribe_volumes()->dict:"""Describe Amazon EBS volumes in the account for the configured region.
This tool calls the EC2 DescribeVolumes API and returns the complete
response payload for every EBS volume it can see. Use this tool when you
need to inspect block storage devices, their attachment state, size in
GiB, volume type (gp2, gp3, io1, io2, st1, sc1, standard), encryption
status, IOPS, throughput, the Availability Zone, creation timestamp, and
the list of tags. A volume whose State field is 'available' is not
attached to any instance and may be wasted spend...
"""returnec2.describe_volumes(Filters=_DEMO_FILTER)
That docstring is trimmed here for print. In the repo it is longer, and that is the point. Notice three problems baked in:
The docstring is long, and it loads into context every single call, used or not.
It returns the full 40-field boto3 blob. That is response bloat: the agent gets fifteen fields it will never read.
To answer "where am I wasting money?" the agent has to know to call describe_volumes, then describe_addresses, then describe_snapshots, then check load balancers, then correlate all of it itself.
When I asked the bloated server my question in Kiro, it made seven waste-server tool calls: get_caller_identity, describe_volumes, describe_addresses, describe_snapshots, describe_instances, describe_nat_gateways, describe_load_balancers. It fished around, one call at a time, then assembled the answer. It got there. It was not pretty.
3. The rebuild, fix by fix (v2: 4 tools)
server_v2_lean.py has the exact same capability. Same read-only checks, same account, same findings. It applies AWS's ladder, and each rung is one clear fix.
Fix 1: outcome-level tools. Instead of fifteen thin wrappers, four tools that each complete a whole business action. The headline one:
@mcp.tool()deffind_waste(scope:Literal["account","region","service"]="account",detail:bool=False)->dict:"""Find wasted AWS spend in one call. Returns each finding with 5 decision
fields (id, kind, size/ip, why, region). Set detail=True for the full blob.
"""
"Where am I wasting money?" is now one call, not seven. The tool runs all the checks internally and returns findings.
Fix 2: schema constraints. The scope parameter is a Literal["account", "region", "service"], not free text. The model cannot pass a garbage value, and it does not have to guess. AWS Prescriptive Guidance recommends keeping tools to around eight parameters or fewer. Every tool here has two.
Fix 3: lean responses. By default find_waste returns five decision-relevant fields per finding, not the full blob. If the agent genuinely needs everything, it passes detail=True. Anthropic's own guidance on code execution with MCP makes the same point: filter and shrink tool results before they reach the model, so the agent reads five rows instead of ten thousand.
Fix 4: lazy taxonomy. Instead of baking every valid value into every tool description, there is a small describe_scope() tool the agent calls only when it needs to confirm inputs. Anthropic's Tool Search approach reports up to 85% token reduction by loading definitions only when relevant.
Fix 5: real errors. A bad scope returns "scope must be one of: account, region, service", not "no results". The model can recover from the first. The second sends it into a retry loop.
When I asked the lean server the same question, it called find_waste once, then estimate_savings to price it. Two calls. Clean.
4. The scoreboard: what changed
Same read-only agent. Same question. Same real AWS account in us-east-1. Every number here came from an actual run, filmed live.
Metric
v1 bloated
v2 lean
Delta
Tools exposed
15
4
-73%
Tool-description size (chars, proxy for schema tokens)
7,898
897
-88%
Tool calls to answer the question
7
2
-71%
Real findings cited
3 IDs
same 3
identical
Dollars from real Price List API
yes
yes
identical
Attempted any mutation?
no
no
read-only held
Both versions found the same wasted spend and cited the same resource IDs:
vol-0c85bd44ef6f9c19d, an unattached 8 GiB gp2 volume, about $0.80 a month.
snap-0e8e4ff717cead51e, an orphaned 8 GiB snapshot, about $0.40 a month.
eipalloc-000992c9956cfaaa5, an unassociated Elastic IP. The agent could not get a verified rate from the pricing tool, so it labeled the impact "not verified" rather than guessing. I built that honest refusal in on purpose.
One outcome-level tool did the work that took the bloated server seven separate guess-and-fish calls.
A quick note on what those schema chars cost in dollars. The 7,898 characters of v1 definitions are roughly 2,000 input tokens, loaded on every single turn. The lean 897 characters are about 225. On Claude Sonnet at $3 per million input tokens, that gap is tiny per call, a fraction of a cent. The reason it matters is that it multiplies: by every turn, every agent, every developer, and by the 90-plus tools a production setup runs instead of 15. A rounding error at demo scale becomes a line item at team scale.
5. The honest part about the context meter
Here is where I have to be straight with you, because most posts on this topic are not.
On my small demo, Kiro's /context tool budget only moved from 1.1% to 0.8%. True, but small. Fifteen tiny tools are still tiny against a 200K window, and the shared pricing server dominates the budget in both agents anyway.
So I am not going to put a dramatic "72% down to 8%" meter on screen and pretend it is my demo. The dramatic, filmed-live numbers are the ones in the table: 7 calls to 2, 15 tools to 4, 88% smaller schemas.
The big context figures (the 55,000-token GitHub server, the "72% of context consumed before the agent takes a single action") hold up, but they are industry data at production scale, clearly labeled as such. The point is the direction: a design principle that saves 0.3% on 15 tools saves 20% or more on the 90-plus tools a production setup runs. You do not want to discover that at scale.
6. Even AWS keeps its tool list tiny
The strongest proof is not mine. AWS's own managed AWS MCP Server exposes about eight tools for all of AWS, and in July 2026 it deprecated its call_aws tool in favor of a single run_script tool, shrinking the surface further. Its docs say, in plain words: "avoid referring to specific tool names in prompts, let your agent select the appropriate tool."
Lean, outcome-level surfaces plus on-demand context is not a hack I invented. It is how AWS builds its flagship server. This repo just lets you prove it on your own.
7. Try it yourself
Everything is in the repo, both servers side by side.
git clone https://github.com/simplynadaf/aws-lean-mcp.git
cd aws-lean-mcp
Prove the difference without an agent at all:
python3 scripts/smoke_test.py
# 15 -> 4 tools, 897 vs 7898 description chars (88% fewer), real IDs from both servers
Then wire each into Kiro and run the A/B with the same prompt in both. The agent JSONs and read-only IAM guidance are in the repo. It is read-only end to end: the servers only call Describe, Get, and List. There is no write action in the code.
If you want findings to triage, scripts/create-demo-resources.sh creates a few cheap, clearly tagged, intentionally wasteful resources, and the teardown script removes 100% of them.
8. What the blog posts do not tell you
Three things I only learned by building it.
The docstring is the schema. With FastMCP, your Python docstring and type hints become the tool definition the model sees. So you make the bloat-versus-lean call at the code level, every time you write a tool. A three-paragraph docstring is three paragraphs loaded on every call.
Splitting a tool can make the schema smaller, not bigger. It feels backwards. But moving valid values out of a fat description and into a separate describe_scope() tool that loads on demand shrinks what gets sent every turn. Fewer always-loaded tokens, at the cost of one extra round-trip when the info is genuinely needed.
Response bloat is half the problem and nobody talks about it. Everyone counts tool definitions. Almost nobody counts the fat JSON your tools return. Returning five fields by default instead of fifty is the cheapest win available, and it compounds on every call.
You did not need more MCP servers. You needed to build the one you have correctly.
Now I am curious about yours. Open the MCP server you use most and count the tools it exposes. Drop that one number in the comments: is it under 10, or did it creep past 30? And if you have ever measured the token cost before your first prompt, tell me what you saw. I will reply to every number.