Firstpass v0.2.1
DocsGuaranteeBenchmarksCLIGitHub

Docs / How Agents Talk to Models / MCP, in depth

Part A2 · Beyond the core pathModule 11 of 13

MCP, in depth

MCP is a protocol your agent speaks to external tool servers — it lives entirely above the model API. The model never speaks MCP. By the time a request reaches the model, MCP has already done its job: tools became entries in the tools array; resources became injected context in messages; prompts became expanded templates. The cost consequence is precise: every connected server's tool schemas ride the tools array as input tokens on every single call — whether those tools are used on that call or not.

The Model Context Protocol gets talked about as if the model “understands” MCP or “speaks” it directly. It doesn't. MCP is a JSON-RPC 2.0 protocol between your agent — acting as an MCP client — and external tool servers. It's a standard way for the agent to discover what a server exposes and invoke it. Everything MCP does happens before the model call. By the time any of it reaches the model, it's ordinary HTTP: a tools array and a messages array, nothing special about them.

Transports

Two transport modes — the agent's concern, never in the model request:

Either way, the agent calls tools/list at startup, discovers the schemas, and caches them for the session.

Primitives

An MCP server can expose three kinds of things, and each lands somewhere different in the model request:

The key asymmetry: tool schemas are re-sent on every call for the whole session; resources and prompt templates only appear when the agent explicitly fetches them and injects the result.

MCP sits above the model API. Two MCP servers on the left — postgres over stdio exposing tools (query, schema) and resources (table_doc), and github over streamable HTTP exposing tools (list_prs, create_pr) and prompts (pr_template) — send their primitives via MCP JSON-RPC to the Agent in the center, which acts as the MCP client. The agent folds server tools into the tools array, and injects fetched resources and expanded prompts into messages, then sends a plain POST to the model API. A dashed vertical line marks where MCP ends at the agent boundary. On the right the model request shows MCP-sourced tools as highlighted ordinary entries in the same tools array as native tools, with resources and prompts injected into messages — the model never speaks MCP. postgres MCP server · stdio tools: query, schema resources: table_doc github MCP server · streamable HTTP tools: list_prs, create_pr prompts: pr_template Agent (MCP client) folds tools → tools[ ] fetches resources → messages[ ] expands prompts → messages[ ] ↓ POST /v1/messages (no MCP on the wire) MCP ends here POST /v1/messages (nothing MCP here) "tools": [ { "name": "Read" … }, { "name": "Edit" … }, ← native { "name": "query" … }, { "name": "schema" … }, ← postgres { "name": "list_prs" … }, { "name": "create_pr" … } ← github ] ← all tool schemas billed as input tokens on every call "messages": [ … history, task … + resource content injected here ← fetched resources land in messages + expanded prompt templates injected ← prompts land here too ] ← resources & prompts cost tokens only when fetched
MCP is the agent's protocol, not the model's. Server tools fold into the ordinary tools[] array; resources and prompts are injected into messages[]. By the time the request reaches the model, it's just plain HTTP — MCP ends at the agent boundary.

Cost

Every MCP server's tool schemas enter the tools array and are billed as input tokens on every call for the whole session — whether the model uses those tools on that call or not. For the six-call fix-a-bug task from the tool-loops module, adding two MCP servers looks like this:

json
// POST /v1/messages — native tools + MCP schemas in one tools array, every call
{
  "model": "claude-sonnet-5",
  "tools": [
    // native agent tools (always present)
    { "name": "Read",      "description": "Read a file",          "input_schema": { ... } },
    { "name": "Edit",      "description": "Apply edits to a file","input_schema": { ... } },
    // postgres MCP server — ~300 tokens of schema, re-billed on all 6 calls
    { "name": "query",     "description": "Run a SQL query",      "input_schema": { ... } },
    { "name": "schema",    "description": "List tables + columns","input_schema": { ... } },
    // github MCP server — ~260 tokens of schema, re-billed on all 6 calls
    { "name": "list_prs",  "description": "List pull requests",   "input_schema": { ... } },
    { "name": "create_pr", "description": "Open a pull request",  "input_schema": { ... } }
  ],
  // fetched resources are injected into messages, not tools
  "messages": [{ "role": "user", "content": "Fix the handler bug and open a PR." }]
}
// MCP overhead: ~560 extra input tokens × 6 calls = ~3 400 extra tokens for the task
// On a strong-tier model at $15/Mtok input: ~$0.05 of MCP overhead per task

That overhead is constant — it doesn't grow with the transcript the way multi-turn history does. But it starts on call one, never shrinks during the session, and multiplies with both call count and tier price. Ten servers with detailed input schemas can add thousands of tokens to every call. The pattern is also invisible unless you log usage.input_tokens per call and notice it creeping up from session to session.

Key idea MCP tool schemas are input tokens that ride the request on every call whether or not those tools are actually used. Connect only the servers a given task needs. Disconnect them when the task is done. Fewer connected servers means a smaller tools array and lower input cost on every call for the rest of the session.
Common mistake Assuming resources are “free” because they're not in the tools array. They aren't. A fetched resource is injected into messages and re-read on every subsequent call as part of the conversation history — exactly like Module 3's multi-turn compounding. A large document fetched early in the session can dominate the input bill by the tenth call, with no tool schemas to blame.
Under the hood — two things called MCP There are two unrelated uses of the name and it's worth separating them clearly. Data-plane MCP is what this module describes: external tool servers the agent folds into requests. Firstpass is completely transparent to it — the tools array is forwarded byte-for-byte and Firstpass never reads or rewrites MCP content on the routed path. Firstpass's own firstpass mcp is a separate, unrelated thing entirely: a stdio self-audit server that exposes your deployment's trace store to an agent so it can inspect routing decisions. Its nine tools are list_traces, get_trace, get_savings, get_evals, explain_route, verify_receipts, submit_feedback, rehearse_policy, and list_modes. It sits beside the router and reads the audit log — it is not on the routed traffic path. Same acronym, opposite side of the wire.
MCP is the agent's protocol, not the model's. Its three primitives — tools, resources, prompts — land in the model request as ordinary entries in tools[] and injected context in messages[]. Tool schemas cost input tokens on every call; keep connected servers to the minimum the task requires.