Model Context Protocol and Tool Servers
MCP makes a tool a versioned protocol with an auth story and a threat model, and every connected tool is a token bill on every single turn.
Why a tool needs a protocol and not just a schema
The LLM Agents lesson described a tool as a typed schema you validate a model's call against. That was the whole story while every agent talked only to tools its own team wrote. It stops being the whole story the moment tools are published by people you do not employ.
The reason is arithmetic. With M agent frameworks and N tool providers, a per-vendor function-calling schema means M times N integrations, each maintained by someone with no reason to care about the other M minus 1. A protocol collapses that to M plus N: every tool provider implements the protocol once, every agent speaks it once. That is the same argument that produced ODBC (one way for any program to talk to any database) and LSP (one way for any editor to talk to any language's tooling), and it is why the Model Context Protocol (MCP) exists.
What standardizing buys beyond the schema is the part worth designing against. A schema tells the model what arguments a function takes. A protocol adds runtime discovery (the agent asks a server what it offers instead of being compiled against a fixed list), a transport contract, a versioning rule so a server can change without breaking every client, and an authorization model, which a bare JSON schema does not have at all. Messages are JSON-RPC 2.0: each one is a small JSON object naming a method to run, its arguments, and an id the reply comes back stamped with, so several calls can be in flight on one connection without the answers getting confused.
The five primitives, and who decides
| Primitive | Direction | Who decides to invoke it | What it is for |
|---|---|---|---|
| Tools | client calls server | The model | Actions with effects: query a system, write a record, send a message |
| Resources | client calls server | The host application | Read-only context the application chooses to attach |
| Prompts | client calls server | The user | Templated workflows the user picks, like a slash command |
| Sampling | server requests it, client re-sends | The server, asking your model to complete something | Letting a server reason without shipping its own model or its own key |
| Elicitation | server requests it, client re-sends | The server, asking your user for a value | Getting a missing input mid-operation, such as a confirmation |
Read that table down the third column. Exactly one row is invoked by the model, and that is the row an attacker who controls your input can reach. Resources and prompts are chosen by your application and your user, so a sentence buried in a retrieved document cannot cause one to fire. Note what the last two rows no longer say. Those two used to let a server open a request back at the client mid-call; now the server pauses instead, hands back a question, and waits for the client to ask again with the answer attached. Concretely, revision 2026-07-28 removed server-initiated requests entirely and replaced them with Multi Round-Trip Requests, where a server that needs sampling or elicitation answers with an InputRequiredResult rather than a result, and the client re-sends the same call carrying an inputResponses field, which is a breaking change against every earlier revision. The two rows that turn a call around this way are the ones summaries drop and the ones that surprise people in review: sampling spends your tokens and your model on a third party's request, and elicitation puts a third party's question in front of your user with your product's face on it.
Transports: local pipe, remote stream
Two transports are defined. stdio runs the server as a local subprocess and passes JSON-RPC messages over its standard input and output. It is the right answer for anything that must touch the user's own machine, and it inherits that machine's trust: a local server runs as the user, with the user's files.
Streamable HTTP is the remote transport. The client POSTs a request to a single endpoint, and the server answers either with one JSON response or with a stream of server-sent events on that same response when it needs to send several messages back.
--> POST /mcp
{"jsonrpc":"2.0","id":7,"method":"tools/call",
"params":{"name":"search_orders",
"arguments":{"customer_id":"c_9931","status":"open"}}}
<-- 200 OK
{"jsonrpc":"2.0","id":7,
"result":{"content":[{"type":"text",
"text":"2 open orders: #4471 shipped, #4488 processing"}],
"isError":false}}
An older HTTP plus SSE transport, which used one endpoint for a long-lived event stream and a second endpoint for posting messages, is deprecated. The reason is operational rather than aesthetic: it required a connection held open for the whole session, which fits badly with request-scoped serverless compute and with load balancers that will happily drop an idle stream, and it left resumption as an unspecified client problem.
Versioning is per request now
The current revision is 2026-07-28. Two things in it change how you build a client, and both are the kind of fact that has to be shown rather than named.
1. Discovery is an RPC every server must implement, not a convention:
--> {"jsonrpc":"2.0","id":1,"method":"server/discover"}
<-- {"jsonrpc":"2.0","id":1,"result":{ ...capabilities, and the revisions
this server speaks... }}
2. Every request states its revision, and it rides in two places:
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28 <- the HTTP layer's statement
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"search_orders",
"arguments":{"customer_id":"c_9931"},
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":
{"sampling":{},"elicitation":{}}}}}
^^^^^ both keys are required per request, not once at connect
Version negotiation moved out of the initialization handshake and into a per-request _meta field carrying two required keys, the revision the client speaks and the capabilities it offers back, and server/discover became mandatory: every server must implement it; a client may skip it and handle UnsupportedProtocolVersionError inline. The consequence for your design is that a session is no longer pinned to whatever the two sides agreed at connect time: a proxy can route on the revision without replaying a handshake, and a long-lived session can shift revisions without being torn down. The spec documents a compatibility path back to 2025-11-25 and earlier, so a client that implements both eras has a defined path to an older server; a modern-only client does not. What you must not do is infer the revision from behavior, which is how clients quietly break on a server upgrade.
Authorization: the server is a resource server
An MCP server that holds anything worth holding is an OAuth 2.1 resource server, and nothing else. It does not mint tokens. It validates tokens minted for it.
Three pieces make that work, and all three are the Level 8 "OAuth 2.1 & OpenID Connect" material applied to a new client:
- Protected resource metadata. The client that gets a 401 from a server needs to know which authorization server to go to. The server publishes that, so discovery is a fetch rather than a configuration file every client edits by hand.
- Resource indicators (RFC 8707). The client asks for a token for a named resource, and the authorization server stamps that audience into the token. A token minted for the invoices server presented to the analytics server is rejected by the analytics server, because the audience does not name it.
- Per-request user authorization. Authenticating the calling client is not the same as authorizing the end user for this record. The server must decide, on every call, whether this user may see this row.
Skip the second piece and every server you connect to is holding a bearer token, a credential whose whole security model is that whoever bears it gets in, that works on every other server you connect to. That is not a hypothetical: it is what a shared, audience-less token means.
The threat model, with a defense on each line
The protocol publishes a threat model. Four items matter for design, and each has a control that belongs in your platform rather than in a prompt.
- Tool poisoning. The description is model-visible text from a third party, so it can carry instructions aimed at your model rather than at your user. Defense: treat the manifest as code. Fetch it, diff it, review it, and gate the model's exposure to a new server behind that review.
- Rug pull. The description you approved is not necessarily the description you get served next month, and the change costs the server operator nothing. Defense: hash the approved manifest, compare on every connect, and fail closed to re-approval on a mismatch.
- Confused deputy. The server holds its own credentials and acts on behalf of whoever asks. If it satisfies a user-scoped request with a static service credential, it has lent its authority to a caller who never had it. Defense: the server authorizes the end user per request and never substitutes a service credential for a missing user grant.
- Token passthrough. The server forwards the token it received to a downstream API that token was never minted for. Defense: audience-bound tokens, and a deliberate exchange for a downstream token rather than a replay of the one in hand.
There is a fifth control that is not on that list and belongs on yours. A server that can reach the open internet can carry your data out of it, so the boundary includes what the server itself is allowed to call: a destination allow-list, not only an input schema.
Tool definitions are tokens, on every turn
The bill is the easy half. The harder half is that accuracy moves too. OpenAI's function-calling guide sets a soft target of fewer than 20 functions available at the start of a turn, and the long-context function-calling literature measures the slope: LongFuncEval reports performance drops in the range of 7% to 85% as the number of available tools rises, with further degradation from long tool responses and from long multi-turn conversations. So the failure is not that the model runs out of room. It is that the model picks the wrong tool out of a hundred plausible ones, and picks it fluently.
Four mitigations, cheapest first:
- Namespacing.
invoices.searchandsupport.searchare two different tools;searchandsearch_2are a coin flip. - Dynamic tool search. Ship one tool that finds tools, and load definitions on demand instead of up front.
- Progressive disclosure. Give the model names and one-line summaries, and fetch a full schema only for the tool it chose.
- Code execution against tools. Let the model write a short program that calls tools, so a five-step chain costs one turn and the intermediate results never enter the context at all.
The result to carry out of this section: an adaptively selected short list of roughly seven tools can match the coverage of a fixed fifty-tool catalog. The fix is selection, not a bigger context window.
Interview nuance: an MCP server is a dependency you have granted read access to your agent's reasoning. Design it like a third-party integration that gets a security review, with a pinned manifest, an audience-bound token, and an egress rule, and not like a library you added to a lockfile.
Recap: MCP turns a tool from a schema into a protocol with five primitives split by who invokes them, two live transports with stdio local and Streamable HTTP remote, per-request version negotiation as of revision 2026-07-28 alongside a mandatory server/discover, an OAuth 2.1 resource-server model with audience-bound tokens, a published threat model whose four entries all resolve to platform controls rather than prompt text, and a token cost per turn that makes tool selection an architectural decision.
Sources: MCP specification, revision 2026-07-28 · How many tools should an LLM agent see · Code execution with MCP · LongFuncEval
Apply
Your turn
The task this lesson builds to.
Define the tool layer for an internal agent platform where 40 teams each publish their own MCP servers, an agent may only reach tools its user is authorized for, and one team's bad server must not be able to influence another team's agent.
Think about
- What does a central registry have to store about a server before an agent is allowed to connect to it?
- Which token does an agent present to a team's server, and what stops that same token working on a different team's server?
- A team edits a tool description after approval. What in your platform notices, and what does it do?
- Forty teams of tools is how many tokens in front of every turn, and what decides which of them a given agent sees?
Solve it here in your browser Nothing to install, and your work saves as you go.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Propose the tool boundary, the authorization model, and the audit trail for a support-triage agent that can read a customer's email, search a company document store, and make outbound HTTP calls, given that ticket text is written by whoever opened the ticket. Make exfiltration of the document store impossible rather than detected, and say which single control is the one that actually stops it.
Think about
- Which of the three tools is the one an attacker needs, and what happens to the attack if it is not there?
- What does the outbound HTTP tool look like if you keep it but make it useless for carrying data out?
- Which parts of this survive an attacker who reads your detection rules and retries for free?
- What does the audit trail have to record for you to answer 'what left the building' the morning after?
Solve it here in your browser Nothing to install, and your work saves as you go.