Open a Rust file in VS Code and misspell a variable name. A red squiggle appears under it, with the compiler’s error on hover. Open the same file in Neovim or Zed and you get the same squiggle with the same message. None of these editors contains a Rust type checker. Each one starts the same separate program, rust-analyzer, and talks to it in a shared format called the Language Server Protocol.
It wasn’t always like that. Before the protocol, each editor needed its own plugin for each language it wanted to understand, written and maintained (and abandoned) separately. Twenty editors and forty languages add up to as many as 800 plugins, of very uneven quality. The protocol changed the arithmetic: a language ships one server, an editor implements one client, and every pair works.
AI applications hit the same wall one layer up. Part 13 showed how a model uses a tool: the application describes the tool, the model writes a JSON request, and the application runs it. That leaves a question Part 13 didn’t need to ask: who writes the tools? If your AI IDE should read issues from your code host, someone has to write list_issues. If the chat app down the hall wants the same thing, someone writes it again. The standard that fixes this is the Model Context Protocol, and it borrows the editor trick, with one twist that makes it work for tools nobody has invented yet.
Every pair needs its own bridge
The editor arithmetic carries over unchanged. Say a company has M AI applications (an IDE plugin, a chat assistant, a support bot) that should each work with N services (the code host, team chat, the ticket tracker, a database). Without a shared standard, every pair needs its own integration: code that knows how that app hands tools to its model and how that service’s API works. That’s M × N pieces of code, each written, tested and kept working whenever either side changes. With one protocol, each app writes a client once and each service a server once, and any client can talk to any server.
Yes: a company with a single app, if it writes every piece itself. One app and 8 services need 8 integrations by hand, but 9 pieces with MCP, the 8 servers plus a client nobody needed before (one service and many apps works the same way). At 2 × 2 it’s a tie. From there the standard pulls ahead fast, because one side is a product and the other a sum: at 8 apps and 10 services it’s 80 against 18. The next service tells the same story: at 5 apps it costs five new integrations by hand, one per app, against one new server with MCP.
A protocol is overhead until enough people share it, and they don’t all have to work at your company. If the services already publish their own servers (the whole idea of the next section), that lone app writes just its client: 1 piece instead of 8. The single-app penalty is real only when your team has to write the servers too, as with internal services.
Now turn on the breaking change, and a second, quieter cost appears. When the code host makes a breaking change to its API, every hand-built integration that touches it breaks at once, in M different codebases owned by M different teams. With a shared protocol, the code host updates its own server, and nobody else ships anything.
Same tool calls, new owner
The Model Context Protocol (MCPMCPModel Context Protocol. Open standard for services to expose tools, resources, and prompts to AI applications.) is an open standard for connecting AI applications to outside systems: code hosts, chat tools, databases, monitoring services and anything else with an API. Anthropic introduced it in late 2024 as an open specification, and it is now widely supported by AI applications and service providers alike. The resemblance to the editor story is deliberate; the specification names the Language Server Protocol as an inspiration.
Each service publishes an MCP server: a program that offers the service’s tools, and that may run on your own machine or at the service (more on that below). Every tool comes with a description and an input schema, which tell a model what the tool does and how to call it. These are exactly the tool definitions from Part 13. A service builds them once (“list issues” and “open pull request”, for a code host), and any MCP-compatible application can use them.
So for tools, which are most of what MCP is used for, MCP is tool callingTool callingThe model outputs structured JSON asking the application to run a named tool with arguments. under the hood. The model still reads definitions and writes a structured request; code outside the model still does the work. (A server can also offer two things that aren’t tool calls at all; more on those later.) What changes is who owns the tools. In Part 13, the application’s developers wrote and maintained them. With MCP, whoever writes the server does, ideally the service provider itself, and the application simply connects. Plenty of servers come from third parties instead, which will matter once we get to trust.
The protocol gives the three pieces names:
- The host is the AI application you use: an IDE, a chat app, an agent.
- A client is one connection inside the host, to one server. A host connected to three servers runs three clients, all instances of the same client code. Each server sees only its own connection, so one server can’t read another’s traffic. Only the host, and through it the model, sees everything.
- A server exposes capabilities: here, a code host’s tools.
One session, message by message
An AI IDE connects to a code-hosting service’s MCP server. You ask it to summarize the open bugs in a repository. What actually travels between the pieces?
The messages use JSON-RPCJSON-RPCLightweight remote-procedure-call format used by MCP. 2.0, a small and deliberately boring format for calling a function in another program. A request names a method, may carry params, and has an id. The response repeats that id and carries either a result or an error, so the client can match answers to questions even when several are in flight. A notification is a request without an id: one-way, and nobody replies. You’ll meet all three below.
The model never touches MCP. It talks only to the host, through its provider’s API, in the same tool-calling shape as Part 13: those are the dashed arrows. Everything MCP-specific, from the handshake to the tool list to the tools/call request with its id, happens between the host’s client and the server, on the solid and dotted arrows. Compare steps 8 and 9: the model’s tool call and the tools/call request carry the same name and the same arguments. Most of the client’s job is translation. (Some model APIs can now play the client for you, connecting to remote MCP servers from the provider’s side. That moves the client, not the boundary: the model still only writes tool calls.)
If you only remember one message, make it this one, from my notes:
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": { "name": "list_issues",
"arguments": { "repo": "acme/api", "state": "open" } } }
A tool that fails comes back as a normal result with isError: true, so the model can read what went wrong; a JSON-RPC error is reserved for protocol problems, like a tool name the server doesn’t know.
And the tool list at step 6 came from the code host’s own developers: they wrote those names, descriptions and schemas, not whoever built the IDE. Now picture a second app, a chat assistant, connecting to the same server. It sends the same initialize, the same tools/list, the same tools/call, and gets the same answers. No new integration work: that’s the last step of the example, and the whole point.
Local or remote: the envelope
JSON-RPC says what a message looks like, not how it travels. MCP defines two standard transports. Scroll back up, pick any step and flip the transport toggle as you read.
stdio is for a local process. The host starts the server as a child process on your machine and writes each message to the server’s standard input as one line of JSON; replies come back on its standard output. No network between host and server, and no open ports. A local server usually gets its credentials, such as an API token, from environment variables the host passes in.
Streamable HTTP is for remote servers. The server runs somewhere else, usually at the service provider, behind a single URL. The client POSTs every message to that URL, and the reply comes back as plain JSON, or as a stream of events when the server has more than one thing to say. Signing in happens here too; more on that below.
The envelope changes; the JSON inside stays the same. Because the messages don’t depend on how they travel, one protocol covers a server running next to you and one running across the internet.
Three things a server can offer
Tools are the headline, but a server can expose three kinds of thing, which the spec calls primitives:
- Tools: actions the model can call, such as
list_issuesorcreate_pull_request. - Resources: readable data, such as a readme or an issue, each named by a URI.
- Prompts: reusable templates, such as “review this pull request the way our team does”.
They differ less in what they contain than in who decides to use them. Before you open the figure, a small puzzle: you want the model to see your project’s readme every time, before it answers. Should the server offer the readme as a tool or as a resource? Switch between the tabs and look at the “Who decides to use it” box.
A resource. Behind a tool, reading the readme is the model’s call, and a model that thinks it already knows enough simply won’t make it. As a resource, the decision moves to the host or to you: attach it, and it’s there. Flip the goal and the answer flips. If you want the model to go and find whichever file is relevant, a get_file tool is the right shape, because then you want the model deciding. A server is free to offer the same data both ways.
That’s the real difference between the three: tools are chosen by the model, resources by the app or by you, prompts by you. The rest follows. A tool can change things (create_pull_request opens a real pull request, which is why a careful host asks first). A resource is only read, and its text reaches the model without any tool call. A prompt arrives as ready-made messages, usually in your voice, written in advance by people who know the service.
What you take on when you connect a server
A tool description is a prompt
Here is the uncomfortable consequence of the design. A tool’s description is written by the server’s author, and it goes straight into the model’s context. The model reads it the way it reads everything else: as text that might contain instructions. Most hosts show you a name and a one-line summary at a glance, and few people open the rest. The model reads every word.
So connecting a server is an act of trust. A malicious or compromised server can return harmful instructions in its results, just as any tool can (Part 13’s warning that tool results are untrusted, because they can carry prompt injectionPrompt injectionMalicious instructions hidden in content the model reads (web pages, documents, tool results)., applies in full). But a server can also hide instructions in its tool descriptions, which the model reads before you’ve asked anything. This is called tool poisoning.
And a server you approved can change under you. A remote one changes whenever its owner deploys. A local one started with something like npx weather-helper can fetch a new version the next time it launches. The protocol even has a message for it, notifications/tools/list_changed, so the client knows to fetch the list again.
Nothing changed on your screen, and that’s the attack. The description gained a paragraph addressed to the model, the schema gained an innocent-looking notes field, and the model’s next call to a weather tool carried private issue titles it had read from a different server a moment earlier. The weather server never touched the code host. It couldn’t have, since servers don’t see each other’s traffic, and it didn’t need to. It asked the model, in text only the model reads, and the model carried the data across in the arguments of a call to the weather server’s own tool. This is Part 13’s riskiest combination, built from two servers that each look harmless: private data (the issues), untrusted text (the description) and a way out (the notes argument).
No single defense covers everything, which is why you stack them. Trust comes first: prefer servers published by the service itself, or code you’ve read, because a server is code with access to your data whether it runs on your laptop or behind a URL. A host that pins the definitions you approved, flags changes and keeps the new text away from the model until you’ve looked stops the silent update, but can’t help if the server was hostile on the day you approved it. Confirming calls with their full arguments catches leaks like this one, as long as people actually read the arguments; ask them every minute and they stop. And least privilege doesn’t stop this leak at all, but it limits what everything else can do.
Eighty tools is a lot of reading
You met this bill in Part 13: tool definitions sit in the context, so they cost tokens, and because the model is statelessStatelessKeeps no memory between calls. The full context must be sent every time. (Part 1) they’re sent again with every request. MCP makes the bill easier to run up, because you don’t add tools one at a time. By default, connect a server and all of its tools come along, whether this conversation needs them or not: the one you wanted and the 25 you didn’t.
At five servers: 80 × 300 = 24,000 tokens, 12% of the window, re-sent on every request. (That’s more per tool than Part 13’s four toy tools, at about 65 tokens each. Real servers’ tools carry longer descriptions, enums and nested schemas, and often run to a few hundred tokens apiece.) Prompt caching can make those repeats cheaper, since the definitions sit at the front of every request, but they still take up room in the window each time.
The second cost is subtler and probably worse. Watch the highlighted squares as you add servers: most of them bring another tool that plausibly answers “find the bug report”. Is it an issue on the code host, a ticket, a chat thread, a support case? The model decides by reading names and descriptions, and with half a dozen look-alikes all called search-something, there are more ways to choose wrong. A long context makes it harder still: models generally use information less reliably as the context grows, so a crowded tool list is a worse place to choose from. And nothing warns you when the choice goes wrong; you just get a confident answer from the wrong place.
The fixes follow from the cause. Connect the servers a task needs, not every server you have. Prefer servers with a few sharp tools over ones that mirror every endpoint of an API. Hosts help too: some prefix each tool’s name with its server’s, so the model sees codehost and tickets in the names, and some load full definitions only when the model searches for a tool, so the context holds a short index instead of every schema.
In this post the model read one tool result, wrote a summary and stopped. Let it read the result, call another tool, read that, and keep going until the job is done, and you have an agent. That’s next.