Written 5 August 2026. This part ages fastest: it is about the inside, and the inside is what the 28 July revision rewrote. Everything below is checked against that revision — which also means anything you read about MCP internals written before it is describing the previous design.

How it works inside

How it works inside

Take away the vocabulary and an MCP server is three things: a list of tools, a description of each written to be read by a machine rather than by a person, and one of two transports to carry the messages — stdio for a server on your own machine, Streamable HTTP for one that is not. Everything else in the Model Context Protocol hangs off those three — JSON-RPC, OAuth 2.1 authorization, and the three levels of failure, the third of which cost me twelve published articles and is invisible to the protocol by construction.

The third part is about construction. It goes deeper than the first two and still without code, because none of the decisions worth arguing about are visible in the code: what a description has to say to be picked, what a tool must never be trusted to claim about itself, and what you have to check after a call has already returned success.

Useful if you are deciding whether to take this on, or want to follow the people who already have.

What actually travels down the wire

Everyone writes about the server: it offers a list of actions and carries them out when asked. Almost nobody explains the MCP client, which is the program the model lives in — it holds the conversation, keeps the list of connected servers, decides which tool to call, and asks the user for confirmation when a call looks irreversible. The model never touches the server. It produces an intention, the client turns that intention into a call, and it is the client the server answers. When you read that the model called a tool, a client did.

Between them travels JSON-RPC, a message format whose second version was published in 2010 and has not moved since 2013 — a decade and a half before any of this AI business, and roughly as exciting as a shipping label. A request has three parts: a method name, an object of arguments, and an identifier that lets answers be matched to questions when several are in flight. Taking it off the shelf was right: nothing left to argue about, a parser in every language, and the authors of the Model Context Protocol — worth spelling out once, since the abbreviation has swallowed the name — free to spend attention on what was new.

Why an MCP call looks nothing like a REST call

This is nothing like REST. In REST the address carries the meaning: GET /articles/17 puts the noun in the path and the verb in the method, and a cache in the middle can act on it without opening the body. In MCP every call goes to the same endpoint and says "tools/call". Which tool, which arguments, what it is about to do to your data — all of it sits inside the body, and anything standing between the two sides sees identical envelopes. That is why the July 2026 revision had to add headers letting a middleman route and cache a request without reading its contents: the protocol was buying back something HTTP has had for free since long before anyone wrote the style down.

Two transports: stdio and Streamable HTTP

The protocol says what a message looks like. It says nothing about how the bytes get from one process to another, and that is a separate job with a name of its own: the transport. The pipe, not the language. MCP supports two, and the choice follows from where the server runs rather than from any merit. If you have configured a server in Claude Desktop, Cursor or VS Code, you have already picked one without being asked: those clients keep their servers in a configuration file, and a local entry is simply a command line. Mine is a stdio server, which is also why nothing here will tell you what Streamable HTTP is like to operate. I have read that part of the specification; I have not lived in it.

    • stdio: the server runs as a neighbouring process on the same machine, talking through its standard input and output — the same two pipes command-line programs have used since the 1970s. No ports, no network. Most local servers work this way, which is why a careless one can be running before anyone has thought about authentication.
    • Streamable HTTP: one network address that is both written to and read from, able to hold a connection open and push messages back. The transport for anything running on a machine that is not yours.
    • The old HTTP+SSE transport, which used two endpoints instead of one, has been obsolete since March 2025. If a tutorial uses it, the tutorial is stale in other ways too.
    • What July 2026 removed: recovery of a broken stream. A client used to reconnect and ask for what it had missed; now it asks again from the start. Simpler protocol, more repeated work — a trade this revision makes over and over.
The two transports

The two transports

Securing a local server

A server on your own machine feels safe and is the least safe thing in the picture. It runs as you, it holds your keys, and nothing about it was hardened because nobody was expected to reach it. One thing can: a page open in your browser. Mine included: it runs as me and it holds the keys to every platform I publish to. The only reason the three requirements below have never applied to it is that it speaks over stdio and listens on nothing at all — which is a fact about my configuration rather than a property of my server, and it stops being true the day I move it off this machine.

The specification names three requirements here and writes them in capitals, which in a standards document is a defined term and not shouting: validate the Origin header, bind only to the loopback address, require authentication. (RFC 2119 fixed the meanings in 1997; RFC 8174, twenty years later, fixed the convention that only the capitals count. Skip a MUST and what you have is not an implementation of the specification at all; skip a SHOULD and you had better know what you gave up. The capitals are there so a reader in a hurry cannot mistake a requirement for advice.)

DNS rebinding: the attack those three rules prevent

The attack they prevent has an unhelpful name — DNS rebinding — and a simple shape. You open a web page belonging to a name the attacker controls, and when your browser looked that name up it got the attacker's own address. Moments later the attacker changes the answer, and the same name now resolves to 127.0.0.1: your own machine. The browser looks it up again, sees the same name, and concludes nothing has changed — its security model is built on names, not addresses. The page keeps every permission it had, and it is now talking to whatever listens on your laptop, from inside your laptop, and reading the replies.

Which is why binding to the loopback address saves nothing on its own: the request really is coming from your machine. The three work only together. The Origin header is the browser honestly reporting which page the request came from, and that page is not your client; authentication makes reaching the server different from being allowed to use it.

How a tool is described

Part one of this series said a server can offer three things and then, like most writing about MCP, discussed only the first. Tools are actions: the model asks, something happens, an answer comes back. Resources are data the server makes readable — a file, a table, a page — each with an address and no side effects, and with one difference that matters: the client or the user decides to pull a resource into the conversation, not the model reaching for it. Prompts are ready-made requests offered as a menu, the things that turn up as slash commands in a client. In practice tools dominate to the point of monopoly. Most servers ship nothing else, mine included.

What a tool description consists of

A tool is described by four things: a name the program uses, a human-readable title, a description in prose, and a schema for the arguments. The schema is JSON Schema, a boring standard for describing the shape of data — which fields exist, which are required, what type each is, what each means. Dullness is the feature: every language has a library for it, and the client can check the model's arguments before a line of your code runs. There is an optional second schema for the result, which sounds redundant since the result arrives regardless. It is not for the result: it lets the client validate what came back and tells the caller the shape of an answer in advance. Alongside its text, a tool can return structured content — one register for the model to read, one for the program to act on.

Never read status out of prose

That distinction gives the rule I would put above every other rule here. Never read status out of prose.

I learned it the ordinary way. My publishing server puts drafts on Medium by driving a real browser, and the adapter needed to know whether the editor had failed to save. It searched the text of the page for the phrases Medium shows on failure, one of which is "something is wrong" — and found that phrase in the article being published, in a sentence reading "if something is wrong you flip back". A run that had worked perfectly was declared a failure, the retry ran, and a duplicate draft appeared. The fix was to stop reading the page as text and look only at the elements a page uses to announce status. The lesson generalises far past browsers: if the only way to learn an outcome is to hunt for words in prose, you have guessed it rather than learned it, and prose will eventually contain your keyword by accident.

Tool annotations are a claim, not a guarantee

A tool may also carry annotations about its own behaviour: read-only, destructive, idempotent, open-world. In the same breath the specification warns that a client must not trust them unless it controls the server. I would go further. Behaviour annotations are the most dangerous part of the specification: "read-only" looks like a guarantee and is a promise made by somebody else's code. A guarantee is enforced by the party that suffers when it breaks. This one is enforced by the party that benefits.

And the consequence that costs money: a tool description is text a model reads on every request, before any work begins. It is billed each time, it takes room in the context window that could have held the conversation, and it is the only thing deciding whether your tool gets picked over the other forty. Writing descriptions is not documentation work. The description is the interface.

Three levels of failure

Most systems have one way to fail. MCP has two by design, and a third the design cannot see.

The first is a protocol error: no tool by that name, arguments that did not match the schema, a malformed request. Nothing ran. It comes back the way JSON-RPC has always returned errors, with a code, and it is a statement about the conversation rather than about your data. The client's plumbing deals with it.

The second is an execution error: the tool ran and the news is bad. The file was not there, the API returned 403, the branch already exists. Here MCP does something that looks wrong the first time you see it — the bad news arrives as a successful response carrying a flag, written out in the text. The transport worked, and it says so.

That is the point rather than sloppiness. In a REST service an error is addressed to a programmer who is not present: return 500, log it, and code written months ago copes or does not. In MCP the recipient is a model in the middle of a task, able to do something about it. Told the branch exists, it can switch to that branch; told the file is missing, it can list the directory. A protocol error would hide the news from the only participant capable of responding, since those are handled by the client's plumbing and never reach the model.

The third level: a success that did not happen

The third level is the one no specification can catch, and it cost me twelve articles.

Part two tells that story in full: the audit of all twenty-five entries carrying a published URL, twelve live posts on Medium left holding between 41% and 72% of their text, and the adapter that closed the tab while Medium was still saving the body over background requests. What belongs here is the protocol's share of it. Every one of the twelve came back as a valid URL and an unremarkable "ok", issued by the tool that had just failed to finish the work.

The flag that reports failure is set by the same code that was wrong about what happened. That is the whole of it. A protocol can carry a verdict; it cannot audit one. This level of failure is invisible to it by construction.

The three that came through intact include two driven by the same browser as Medium; what separated them from Medium was the shape of the submission, not the machinery. Nobody noticed for weeks because the monitoring asked whether the URLs were reachable, and they were. A truncated article is a perfectly healthy web page.

The conclusion is unwelcome and unavoidable. For anything that matters, the check that the work happened has to be separate from the tool that did it, and it has to ask a different question: not "did the call return" but "is what is now on the server the same as what I sent". I have a script that reads every published article back and compares it against the source. It exists because a successful response is an opinion.

Who is allowed in

The script that reads those articles back needs a key to do it, which is the other half of a server nobody plans for: not what it can do, but who is allowed to ask. (A warning about vocabulary before we start. In part two, on what MCP costs in tokens, "token" meant the unit text is counted and billed in. Here it means a credential: a string proving who you are and what you may do. Same word, unrelated meanings, both standard usage in their own corner.)

Authorization in MCP is heavier than in REST for a structural reason. A REST key identifies one program calling another. In MCP there are three parties — a person, a client acting on their behalf, and a server that may itself hold credentials to a fourth system — and the interesting failures live in the gaps between them.

    • In REST, typically: a key in a header, and that is the whole story. It works because both ends are usually written by the same team, or at least read the same documentation.
    • In MCP: OAuth 2.1, with PKCE mandatory, plus separate standards for how a client discovers whom to ask for permission at all. PKCE deserves plain words. An authorization flow comes back through the browser or the operating system's URL handling, carrying a short-lived code, and that path is not private — another application on the machine can be listening. So the client invents a random secret at the start, sends only a fingerprint of it, and must produce the original when trading the code for a token. A stolen code becomes useless to whoever stole it: they have the code and not the secret, and the exchange fails.
    • Binding a token to its recipient: the server must check that the token in front of it was issued for it, not merely that it is valid. A token is not a password, it is a note addressed to somebody, and accepting one addressed elsewhere is how a server becomes a way in.
    • An outright ban on passing someone else's token onward, and the specification names the failure it prevents: the confused deputy. A deputy is a component with more authority than its caller — your server, holding an admin key. Confuse it about who is asking and it will spend its own authority on the caller's behalf.
    • What changed in 2026: dynamic client registration, where a client registers itself with an authorization server on the fly, was marked obsolete. It was the most convenient part of the flow and the most abused.

The question that matters in practice: the wrapper

Everything above is protocol. This is the section you will actually spend time on.

Almost always, an MCP server is a translator standing in front of a REST API you already have. It is not a new system, it holds no data of its own, and if it disappears the underlying service carries on unbothered. Mine talks to my own admin API over HTTP with an API key, the same endpoints the admin interface uses. The API has an OpenAPI description; the MCP server does not read it, because a description written for a programmer at integration time is not what a model needs at runtime. That is the normal case, not a shortcut.

The interesting part is what appears in the translator that never existed in the API. Descriptions come first, and they are written for a reader who has not seen your documentation and never will. Then permissions, because the caller is a model now, not your own front end, and the front end could be trusted to ask only for what the screen offered. Confirmations, for anything you would not want to happen twice. And filtering — the part everyone skips: an endpoint returning sixty fields is free when a program reads it and expensive when a model does, because the model pays for all sixty and needed six.

Do not generate one tool per endpoint

Then comes the temptation, and it gets nearly everyone. You have two hundred endpoints; generating two hundred tools takes an afternoon, and the result is not merely useless but expensive and useless. Every description is billed on every request, the list is long enough that the wrong item gets picked regularly, and the shape is wrong at a deeper level: endpoints are organised the way a database is, tasks the way a person thinks. "Close this task" is one thing to a human and three calls to an API.

A curated fifteen beats the complete list, and the curation is where the work is. The clearest example in my own server is a set of tools with no counterpart in the API at all: three verifiers. Two of them check an article will survive the platform before it goes anywhere — no markdown tables where the editor has no table block, no heading duplicating the title, no unclosed code fence — and the third reads the published page back afterwards and complains. The API has no such endpoint and does not need one. Neither would a human. They exist because the caller is a model and I wanted a guardrail it could not skip.

I should give my own number, since I am handing out advice. My promotion-calendar server has 20 tools. That is five more than I recommend, and I know how it happened: each one was obviously justified on the day it was added. Nobody decides to have twenty tools. You have eleven, then a shortcut that saves a step, then a listing tool that pairs with an existing one, and one morning the list no longer fits on a screen. Curation is not a decision made once. It is something you keep doing, against yourself.

The wrapper

The wrapper

Moving to the 2026-07-28 revision

The largest rework in the protocol's history has one organising idea, and the change list reads as a single move: everything that assumed the two sides remember each other has been taken out. The year of grace and why nothing broke on the day are in part one; here I care about what is on your list.

    • Sessions are gone: state moves into arguments the server itself hands out. The client gives back what it was given, and any instance of the server can answer.
    • The handshake is gone: no opening negotiation, with the version and the capabilities riding along on every request. A request now stands on its own — the protocol is stateless, which is the entire point.
    • A dedicated call arrived for asking a server what it can do — previously a side effect of connecting, now a question you ask when you want the answer.
    • A server needing more information answers "input required", and the client repeats the request with the answer supplied. The pause no longer lives inside an open connection.
    • Three capabilities are obsolete, with replacements: folder access becomes ordinary arguments; the server calling back to the model becomes a direct call to that model's own API, where it belonged; protocol-level logging becomes the error stream or your existing telemetry, where that belonged too.

Where to start, if you are going ahead

Start with one tool. Not a representative sample, not the read-only subset — one, the single thing you most want to be able to ask for. Ship it, live with it for a fortnight, and watch whether the model calls it when it should. That question is answered by the description far more than by the code, and you will rewrite the description more times than you expect.

Before adding the second, measure. I did not, and that is exactly how a server gets to twenty tools. Take the descriptions you have, count what they cost in tokens and what share of the context window they occupy, and multiply by every request for the rest of the year. This step gets skipped, and it is the one that changes decisions.

Keep a person in the loop for anything irreversible. The specification asks for this outright, and after the three levels of failure you know why I hold the position more firmly than the specification states it: a tool reporting success is making a claim, not producing evidence.

Budget for rewriting. Five revisions in twenty months, the latest of which removed foundations. Whatever you build now, plan to touch it again within the year — with some comfort in the direction of travel being toward simplification.

Most teams do not need an MCP server right now. The right first step is not to write one; it is to work out what the tool descriptions will cost in tokens, on every request, for the workload you actually have. For half of them that will be the end of it, and it will have been an evening well spent. A server earns its keep when the set of actions genuinely cannot be known in advance — when it depends on what a user attaches, on where a task turns next, on a decision made in conversation. Mine does, and it still cost me twelve articles before I learned to check the work instead of the answer. If you can write the list of calls in advance, write the list. That was true in 2000, and the 2026 revision has quietly made it truer.

This is the last of three articles. The first is where MCP and REST came from, and why the July 2026 revision turned MCP back toward REST; the second is what MCP costs in tokens.

Sources

The MCP specification is published at modelcontextprotocol.io; this used revisions 2025-06-18 and 2026-07-28: the transport, tools and authorization sections; the official changelog for the latest revision; proposals SEP-2567, SEP-2575, SEP-2322, SEP-2549. The OAuth 2.1 draft, RFC 8707, RFC 9728, RFC 7591, and RFC 2119 with RFC 8174 for the capitals. The incidents described above are from my own publishing server, audited on 25 July 2026.