Docs / How Agents Talk to Models / MCP, in depth
MCP, in depth
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:
- stdio — the server runs as a local subprocess; JSON-RPC over stdin/stdout. Zero network overhead; right for local tools (a filesystem server, a local DB).
- Streamable HTTP — a remote process over HTTP with SSE push; right for hosted services (a GitHub integration, a managed Postgres).
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:
- Tools are callable functions —
query,list_prs,create_file. The agent folds their JSON schemas into thetoolsarray of every subsequent model request. To the model, a postgres MCP tool and a nativeReadtool are identical: both are entries in the same array. - Resources are data the server can serve on demand — table rows, documents, code files. The agent fetches them explicitly and injects the content into
messages, typically as a user-turn ortool_result. Resource content never enters thetoolsarray; it becomes ordinary conversation context. - Prompts are server-provided templates — system prompt snippets, few-shot examples, task scaffolding. The agent retrieves them and expands them into
messagesbefore the call. The model reads prose and turns, not a “prompt” primitive.
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.
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:
// 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 taskThat 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.
tools array and lower input cost on every call for the rest of the session.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.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.tools[] and injected context in messages[]. Tool schemas cost input tokens on every call; keep connected servers to the minimum the task requires.