DEV Community

MrClaw207
MrClaw207

Posted on

I Ran 6 MCP Servers Behind One Agent. Here's What the Token Bill Actually Looked Like.

I wired six MCP servers into a single production agent last month — filesystem, Postgres, GitHub, a custom CRM connector, a browser tool, and an internal search index. The promise of MCP is "USB-C for tools," and at one server, it really does feel that way. At six, things get interesting in ways the tutorials don't prepare you for.

This is what I measured, what surprised me, and the four changes I shipped afterward.

The Setup

The agent runs on Sonnet 4.5 with a 200k context window. Six MCP servers, all using Streamable HTTP (the post-2026-04 transport), all behind a single tool gateway. I'm logging every tool call — input tokens (the schema and arguments), output tokens (the response), and wall time.

# instrumented mcp tool call
async def call_tool(server: str, tool: str, args: dict) -> Any:
    schema_tokens = count_tokens(server.tool_schemas[tool])
    arg_tokens = count_tokens(json.dumps(args))
    t0 = time.perf_counter()
    result = await server.call(tool, args)
    result_tokens = count_tokens(json.dumps(result))
    log(
        server=server,
        tool=tool,
        in_tokens=schema_tokens + arg_tokens,
        out_tokens=result_tokens,
        latency_ms=(time.perf_counter() - t0) * 1000,
    )
    return result
Enter fullscreen mode Exit fullscreen mode

Over a seven-day window: 41,200 tool calls across the six servers, the agent averaging 14 tool calls per session. Here's what the ledger said.

Finding 1: Schema tokens dominate the cost

This is the one nobody warns you about. Each MCP tool ships its full JSON Schema on every tools/list request — and the schema lives in the agent's context for the entire conversation.

The Postgres MCP server alone exposed 47 tools. Each tool's schema averaged 380 input tokens. That's ~18,000 tokens of pure schema overhead sitting in the context window before the user has typed a word. The GitHub server added another ~12,000. By the time all six servers were loaded, 41% of my agent's available context was tools the user might never invoke.

I confirmed this with a quick before/after:

Configuration System prompt tokens Cache hit rate
1 MCP server, 6 tools 4,200 92%
3 MCP servers, 38 tools 13,100 71%
6 MCP servers, 140 tools 43,800 48%

The cache hit rate collapse is the silent killer. Anthropic's prompt caching assumes your prefix is stable, but every tool definition loads fresh into the cache, and the cache is priced by total tokens cached, not tokens used.

Finding 2: Streamable HTTP isn't free — and it isn't always faster

I ran the same workload over stdio and Streamable HTTP. The intuition is that HTTP is "more scalable," and that's true for multi-client scenarios. But for a single agent on a single host, the numbers flipped:

stdio (local):
  Avg latency:        38ms
  p99 latency:         210ms
  Failed calls:       0.2%

Streamable HTTP (local gateway, http://localhost):
  Avg latency:        71ms (+87%)
  p99 latency:        480ms (+128%)
  Failed calls:       1.4% (mostly connection resets on long streams)
Enter fullscreen mode Exit fullscreen mode

For my single-tenant agent, I switched the four local servers back to stdio and kept Streamable HTTP only for the two remote services (one customer's CRM, one shared search index). Latency dropped 38% across the board. If your agents and servers share a host, stdio is almost always the right default.

Finding 3: Tool descriptions are load-bearing — and most are bad

When the schema's 380 tokens, 80 of them are the tool description. The LLM uses these to decide which tool to call, and "List issues in repository" versus "Get issues assigned to me" versus "Search issues by query" is the difference between a useful agent and one that calls the wrong tool every time.

I had three engineers independently rewrite the descriptions on our GitHub MCP server's tools. Then I ran an eval: 50 ambiguous real-world requests, scored by whether the agent picked the right tool on the first try.

Description style First-try accuracy
Original (vendor-shipped) 64%
Engineer A rewrite 71%
Engineer B rewrite 78%
Final consensus (A+B merged) 86%

Twenty-two percentage points from rewriting prose. This is the cheapest win in MCP right now. If you maintain an MCP server, spend an afternoon rewriting your tool descriptions in first-person imperative ("Search the user's open pull requests. Filters by repo, state, author.") — not the third-person flat style most SDK generators spit out.

Finding 4: OAuth 2.1 isn't optional anymore, but it's painful in surprising ways

The June 2026 MCP spec made OAuth 2.1 a hard requirement for any server exposing user-scoped data. I implemented it for the CRM connector using the standard mcp-auth library. Three things broke that weren't in any guide:

  1. Token refresh storms. Every tool call from an active session triggered a metadata check that sometimes cascaded into a token refresh. With 12 concurrent agents, I saw 140 refresh requests/minute from a pool of 4 valid tokens. The fix: cache the metadata response for the lifetime of the access token, not for 5 minutes like the docs suggest.

  2. Dynamic Client Registration (DCR) + enterprise SSO. The spec assumes a public client with PKCE. The moment your customer wants SSO against Okta or Entra, DCR fights you, and you end up writing a per-tenant client registry by hand. I now keep a tenants.json that maps issuer -> client_id and use static registration for enterprise tenants.

  3. The "scope" trap. MCP's auth model doesn't include tool-level scopes. Once a user authenticates, the agent can call any tool on the server. I solved this by wrapping the OAuth handshake in an internal policy layer:

@tool("crm.update_contact")
async def update_contact(args: ContextArgs) -> Result:
    if not has_scope(args.session, "crm:write"):
        return Result(error="requires crm:write scope")
    # ... actual update
Enter fullscreen mode Exit fullscreen mode

External OAuth proves who the user is. It does not prove what the agent may do on their behalf. For multi-tenant MCP, that second question is yours to answer.

What I shipped afterward

Four changes that took a week and cut our per-session cost by 53%:

  1. Tool budget per session. Enforce a max number of tools loaded per agent run (cap = 40 schemas, ~16k tokens). Lazy-load the rest from a registry at first call.
  2. Switched local servers to stdio. Reserver Streamable HTTP for genuinely remote services.
  3. Description audit pass. Every tool description rewritten, evaluated, and pinned in CI. New tools require an eval sample before merge.
  4. Tool-level policy layer. OAuth handles identity, but every sensitive tool checks an internal scope before executing. Auditable, logged, and reversible per-user.

What I learned

MCP is genuinely the right abstraction. The "USB-C" framing is real — I haven't written tool glue code in months. But the abstraction hides three things that matter at scale: schema overhead, transport choice, and authorization granularity. Each one is cheap to do right, expensive to retrofit, and completely invisible until you're already in production.

If you're putting MCP in front of users this quarter, run one weekend benchmark before you ship: log every tool call's input tokens, output tokens, and latency, broken down by server. The numbers will tell you what to fix. My numbers told me to rewrite descriptions, switch transports, and never trust OAuth alone.

The protocol got us to plug-and-play. The engineering is what makes it production-ready.

Top comments (0)