MCP Explained Against the Spec: What a Server Really Is
One line of JSON into a subprocess and thirteen tool definitions come back, read against the 2026-07-28 revision that removed the handshake.
On this page
Install a published MCP server, send it one line of JSON, and read what comes back.
npm i @modelcontextprotocol/server-everything@2026.8.31
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| npx @modelcontextprotocol/server-everything stdio{"result":{"tools":[{"name":"echo","title":"Echo Tool","description":"Echoes
back the input string","inputSchema":{"$schema":"http://json-schema.org/draft-07/
schema#","type":"object","properties":{"message":{"type":"string","description":
"Message to echo"}},"required":["message"]},"annotations":{"readOnlyHint":true,
… … 7,663 bytes on one line …
"jsonrpc":"2.0","id":1}Thirteen tool definitions, on a single line, from a process that read one line from its standard input. You have now spoken the Model Context Protocol, with no SDK, no client library and no framework. That is the whole of it: a transport, a message format, and a small set of named methods.
Chapter 18 defined a tool as two things — a JSON Schema the model sees, and an endpoint in your code that the model never sees. Chapter 23 built a harness that holds a catalogue of them. Neither answered the question that decides whether any of it is reusable: who writes the schema, and how does it get from whoever wrote it into your prompt? MCP is one answer to that question, and it is worth reading in the original, because almost everything written about it describes a revision that no longer exists.
Three things about the command you just ran are wrong, and each one is a section of this chapter. It carried no protocol version, so a conformant server would have refused it. It got an answer anyway, for a reason the specification calls a hazard rather than a feature. And it asked for one of three primitives without ever discovering that the other two exist.
The problem it solves, and the analogy the spec makes itself
Link to the section: The problem it solves, and the analogy the spec makes itselfBefore the wire, the arithmetic. You have AI applications and things they should be able to reach — a calendar, a ticket tracker, a warehouse database, a design tool. Without a shared contract, somebody writes integrations, and every one of them is a schema plus an endpoint plus an authentication story plus a maintenance burden. With one, the tool vendor writes a server, the application vendor writes a client, and the total is .
That is not a new observation, and the specification says whose idea it was:
MCP takes some inspiration from the Language Server Protocol, which standardizes how to add support for programming languages across a whole ecosystem of development tools. In a similar way, MCP standardizes how to integrate additional context and tools into the ecosystem of AI applications.1
Take that comparison literally rather than as a compliment. Before that protocol, supporting a language in an editor meant a plugin per editor; afterwards, a language team shipped one server and every editor got it. The measure of success was not elegance, it was that the integration count stopped multiplying. The same thing follows here: the value is in the number of implementations, not in the design. A protocol that two products speak is a data format with extra ceremony.
What is actually on the wire
Link to the section: What is actually on the wireMCP messages are JSON-RPC 2.0. A request is an object with jsonrpc, an id, a method and optional params; a response carries the same id and either result or error; a notification is a request with no id and gets no reply. The specification adds three constraints on top: the id must be a string or a number and must not be null, it must not collide with another request still in flight, and every result must carry a resultType field.2
On the stdio transport — the one the command above used — the framing rule is one line per message:
Messages are delimited by newlines, and MUST NOT contain embedded newlines. […] The server MUST NOT write anything to its
stdoutthat is not a valid MCP message.3
That last clause is the most common way a homemade server breaks, and it breaks silently: a stray console.log, a progress bar, a deprecation warning from a dependency, and the client's line parser hits something that is not JSON. The escape hatch is in the same section — the server may write anything it likes to stderr, and the client should not treat that as an error. The reference server above prints Starting default (STDIO) server... on every launch, on stderr, which is why the pipe still worked.
The other standard transport is Streamable HTTP: each message is a POST to a single endpoint, and the reply is either a JSON object or a request-scoped stream of Server-Sent Events — the wire format Chapter 14 parsed by hand. Semantics are identical on both, because a transport is a binding: it defines framing and delivery, not meaning.4
The first thing that was wrong: there was no version
Link to the section: The first thing that was wrong: there was no versionThe command above sent tools/list and nothing else. Under the current revision that request is malformed, and a conformant server must reject it.
Since 2026-07-28, MCP is a stateless protocol, and the specification states it without hedging:
The Model Context Protocol (MCP) is a stateless protocol: all the information needed to process a request is contained in the request itself. A server processes each request independently; no state should be inferred from previous requests, even those on the same connection or stream.2
So every request carries its own protocol version and its own client capabilities, in a reserved _meta object inside params. Two of those fields are required on every single request; a request missing either is malformed and the server must answer -32602:2
_meta key | required | what it is |
|---|---|---|
io.modelcontextprotocol/protocolVersion | yes | the revision this request speaks, e.g. "2026-07-28" |
io.modelcontextprotocol/clientCapabilities | yes | what the client can do for the server on this request |
io.modelcontextprotocol/clientInfo | no (but should) | client name and version, for display and logs only |
io.modelcontextprotocol/logLevel | no | the minimum log level the server should emit for this request |
Written out, a correct tools/list is this — and it is the last time this chapter shows the metadata in full, because it is on every request from here:
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{"elicitation":{"form":{}}},
"io.modelcontextprotocol/clientInfo":{"name":"bare-hands","version":"0.0.1"}}}}The capability object is the negotiation. There is no separate negotiation step any more: the client declares what it can do on each request, the server declares what it can do in the result, and neither side may use a feature the other has not claimed. A server that needs a capability the client did not declare must answer -32021 and name the missing capability in data.requiredCapabilities. A server that does not speak the requested version must answer -32022 and list the versions it does speak.2
Clients that want the answer up front can ask for it: server/discover is a mandatory RPC that returns supported versions, capabilities, identity and an optional block of instructions in one round trip.5 Calling it is optional. Implementing it is not.
The second thing that was wrong: the server was legacy
Link to the section: The second thing that was wrong: the server was legacyThe command worked. Under the current revision it should not have, and the reason it did is worth a measurement rather than a paragraph, because it is the state of the entire ecosystem in one line.
Probe the reference server the way the specification tells a modern client to probe:
echo '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{}}}}' \
| npx @modelcontextprotocol/server-everything stdio{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}That is the third branch of the compatibility rule: a DiscoverResult means modern, a recognised modern error means modern-but-wrong-version, and anything else — including -32601 — means legacy, fall back to the initialize handshake.3 So do that, asking for the current revision:
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28",
"capabilities":{},"clientInfo":{"name":"bare-hands","version":"0.0.1"}}}
← {"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{"listChanged":true},
"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},
"logging":{},"tasks":{…},"completions":{}},"serverInfo":{"name":"mcp-servers/everything",
"title":"Everything Reference Server","version":"2.0.0"},"instructions":"…"}}The client asked for 2026-07-28 and the server answered 2025-11-25. On 7 September 2026, the official reference server — npm package @modelcontextprotocol/server-everything, version 2026.8.31, published 31 August 2026 — does not implement the current revision. Neither, on the dates, does the TypeScript SDK it is built on: release 1.30.0 went out on 27 July 2026, the day before the revision did.
Read the consequence rather than the gossip. Nearly everything written about MCP describes a protocol with an initialize handshake, a session, a roots/list request the server sends to the client, and an HTTP+SSE transport. All four are gone or going. When you read anything about MCP, including this page, the first thing to look for is a revision number.
And the reason the very first command worked is stated in the specification as a hazard, not a feature:
some legacy servers do not validate that a request arrives after
initializeand would process an era-ambiguous method (such astools/call) under legacy semantics. Probing yields a deterministic failure instead.3
Measured: sending tools/list to that server with no handshake at all returns the full catalogue. A method that should have been refused was served, which is exactly why the specification says to probe with server/discover first even when you only support modern versions.
Three roles, and the sentence to quote from the whole document
Link to the section: Three roles, and the sentence to quote from the whole documentMCP has three parties, and the distinction between the first two is the one people collapse:
Host. The application: the chat product, the editor, the agent. It owns the conversation, the model, the credentials and the user's consent. It creates clients and enforces the security boundary between them.
Client. A connector inside the host. Each client talks to exactly one server — a strict 1:1 relationship — and attaches the protocol version and capabilities to every request it routes.
Server. A process or a service that exposes resources, tools and prompts. It can be local or remote, it operates independently, and its whole job is one focused area.6
That "exactly one server" rule is not bookkeeping. It is what makes the design principle below implementable, and this is the sentence to take from the specification if you take only one:
Servers should not be able to read the whole conversation, nor "see into" other servers. Servers receive only necessary contextual information. Full conversation history stays with the host. Each server maintains isolation. Cross-server interactions are controlled by the host.6
That overturns the mental model most people arrive with. A weather server you connect to your assistant does not see what you asked. It sees a tools/call with the arguments the model chose, and nothing else — not the previous turns, not your system prompt, not the results the calendar server returned a moment ago. If two servers need to cooperate, the host carries a value from one to the other, deliberately, because the model asked it to. Which is why isolation is the security property Chapter 30 leans on: a compromised server has a small, defined blast radius, and enlarging it requires the host to cooperate.
The third thing: three primitives, sorted by who is in charge
Link to the section: The third thing: three primitives, sorted by who is in chargeThe first command asked that server for tools and got thirteen. Ask it the other two questions and it answers those too: resources/list returns seven, prompts/list returns four. None of them appeared, because nothing asked. Which brings us to the pedagogical spine of MCP, sitting in the specification as a table that almost nobody quotes:
| Primitive | Control | Description | Example |
|---|---|---|---|
| Prompts | User-controlled | Interactive templates invoked by user choice | Slash commands, menu options |
| Resources | Application-controlled | Contextual data attached and managed by the client | File contents, git history |
| Tools | Model-controlled | Functions exposed to the LLM to take actions | API POST requests, file writing |
Not "three ways to expose a capability". Three answers to who decides that this happens. The model decides to call a tool. The application decides to attach a resource. The person decides to run a prompt. Get that wrong and the feature still works, but it works at the wrong moment and for the wrong reason.
The clearest way to feel it is a calendar. Here is a server that exposes the same calendar three times, once as each primitive, in a hundred lines of plain Node with no dependencies:
const TOOL = {
name: "create_event",
description: "Create a calendar event. Writes to the calendar.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Event title." },
startsAt: { type: "string", format: "date-time", description: "Start, ISO 8601 UTC." },
},
required: ["title"],
},
};
switch (method) {
case "resources/read":
return ok(id, { contents: [{ uri: "calendar://week",
mimeType: "application/json", text: JSON.stringify(EVENTS) }],
ttlMs: 60000, cacheScope: "private" });
case "prompts/get":
return ok(id, { description: PROMPT.description, messages: [{ role: "user",
content: { type: "text", text: `Read calendar://week and draft a plan. ` +
`Focus: ${params.arguments?.focus ?? "balance"}.` } }] });
case "tools/list":
return ok(id, { tools: [TOOL], ttlMs: 300000, cacheScope: "public" });
}Run it and ask it all three ways. Real output, one message per line on the wire, wrapped here for the page, with the request _meta and the server's identity block elided:
→ resources/read {"uri":"calendar://week"}
← {"resultType":"complete","contents":[{"uri":"calendar://week",
"mimeType":"application/json","text":"[{\"id\":\"e1\",\"title\":\"Standup\",
\"startsAt\":\"2026-09-07T09:00:00Z\"},{\"id\":\"e2\",\"title\":\"Design review\",
\"startsAt\":\"2026-09-09T15:00:00Z\"}]"}],"ttlMs":60000,"cacheScope":"private"}
→ prompts/get {"name":"prepare_week","arguments":{"focus":"deep work"}}
← {"resultType":"complete","description":"Read the week and draft a plan.",
"messages":[{"role":"user","content":{"type":"text",
"text":"Read calendar://week and draft a plan. Focus: deep work."}}]}
→ tools/call {"name":"create_event","arguments":{"title":"Dentist",
"startsAt":"2026-09-10T08:30:00Z"}}
← {"resultType":"complete","content":[{"type":"text",
"text":"Created e3: Dentist at 2026-09-10T08:30:00Z"}],
"structuredContent":{"id":"e3","title":"Dentist","startsAt":"2026-09-10T08:30:00Z"},
"isError":false}Three methods, three shapes, one calendar. Now the point:
Reading the week is a resource
Link to the section: Reading the week is a resourceIt is addressed by a URI, it is inert, and the application decides whether to attach it to the conversation. Nothing in the protocol lets the model reach for it on its own. The result carries ttlMs and cacheScope, new in this revision, so the client can cache the week for a minute instead of polling.
Creating an event is a tool
Link to the section: Creating an event is a toolIt has a schema, it has side effects, and the model decides when to call it. Its result carries isError, which is the field Chapter 18 argued for: a validation failure comes back as a tool result the model can read and correct, not as a protocol error.
"Prepare my week" is a prompt
Link to the section: "Prepare my week" is a promptIt is a named, argument-taking template that the person invokes — the slash command in the menu. It returns messages, not an answer. It is a way for a server author to ship the phrasing that works with their own tools, which is exactly the knowledge the server author has and the user does not.
Almost everybody makes all three of these tools. The result is a catalogue where a read the application should have attached silently competes for the model's attention with a write that needs approval, and where the one thing a person wanted a button for is buried in a schema. It costs nothing to get right, and it is decided before you write a line.
The server cannot call you
Link to the section: The server cannot call youThe calendar tool has one required argument, title, and an optional startsAt. Ask it to create an event without a date, and something interesting comes back:
→ tools/call {"name":"create_event","arguments":{"title":"Dentist"}}
← {"resultType":"input_required",
"inputRequests":{"when":{"method":"elicitation/create","params":{"mode":"form",
"message":"When should \"Dentist\" start?",
"requestedSchema":{"type":"object",
"properties":{"startsAt":{"type":"string","format":"date-time"}},
"required":["startsAt"]}}}},
"requestState":"eyJ0aXRsZSI6IkRlbnRpc3QifQ=="}The server did not send a request. It answered the one it was given, with resultType: "input_required" and a description of what it still needs. The client collects the answer from the person, and then re-sends the original call — with a new id, carrying inputResponses and echoing the opaque requestState back:
→ tools/call {"name":"create_event","arguments":{"title":"Dentist"},
"inputResponses":{"when":{"action":"accept",
"content":{"startsAt":"2026-09-10T08:30:00Z"}}},
"requestState":"eyJ0aXRsZSI6IkRlbnRpc3QifQ=="}
← {"resultType":"complete","content":[{"type":"text",
"text":"Created e3: Dentist at 2026-09-10T08:30:00Z"}],"isError":false}This is Multi Round-Trip Requests, introduced in the current revision, and it replaced the older design where servers sent JSON-RPC requests back at clients. The transport specification now states the rule flatly: "servers do not initiate JSON-RPC requests and clients do not send JSON-RPC responses".4 There is one direction of initiative, and it belongs to the host.
Two client-side features ride on that mechanism, and one of them has a name that will trip you.
Elicitation is the server asking the person for something: a form with a deliberately restricted JSON Schema — flat objects, primitive properties, no nesting — so any client can render it without a layout engine. It carries a hard rule: servers must not use form mode to ask for "passwords, API keys, access tokens, or payment credentials", and must use URL mode for those, which sends the user to a page the client never reads.7
Sampling is the server asking the host's model for a generation, so a server can be intelligent without holding an API key. And here is the vocabulary warning, because this word already means something else in this course: this is not Chapter 17's sampling. Nothing here is about temperature, top-p or the shape of a probability distribution. It is a nested model call travelling backwards through a protocol.
There is a second reason not to reach for it: as of this revision, sampling is deprecated, alongside roots and logging, under SEP-2577, with a blunt suggested migration — "integrate directly with LLM provider APIs instead of Sampling".8 The idea did not fail technically; it failed to justify its surface area, and a protocol that can remove things is healthier than one that cannot.
Break it on purpose: connections are not sessions
Link to the section: Break it on purpose: connections are not sessionsStatelessness sounds like a wire-format detail until you test it. Take the three-message exchange above and run each message in a separate process — a fresh node calendar.mjs, no shared memory, nothing carried over:
process A tools/call (no date) → resultType: input_required
requestState: eyJ0aXRsZSI6IkRlbnRpc3QifQ==
process B tools/call (with the answer, same requestState)
→ resultType: complete
"Created e3: Dentist at 2026-09-10T08:30:00Z"
process C resources/read calendar://week
→ events: 2 (Standup, Design review)Process B, which never saw the question, completed a multi-round-trip call that process A started. That is the point of requestState: the continuation travels in the message, so nothing depends on the process being the same one.
Process C is the failure. The event was created and it is not there — because the toy server keeps EVENTS in a module-level array, and a module-level array is connection state. The specification's note names the mistake precisely:
an open connection, such as a STDIO process, is not a conversation or session: clients may interleave unrelated requests on the same transport, and a server must not treat connection or process identity as a proxy for conversation or session continuity.2
The prescribed fix is not a session. It is an explicit handle: a creation tool returns an opaque identifier, and every later call takes it as an ordinary argument. The protocol has no concept of it at all — "from the wire's perspective a handle is an ordinary string in a tool result and an ordinary argument to subsequent tool calls".9 Which puts the model in charge of carrying it, and puts the server in charge of validating that this caller is allowed to use it on every single call, because a handle is a name and not a permission.
What a server costs before it does anything
Link to the section: What a server costs before it does anythingEvery tool a server exposes is a schema that goes into your prompt on every request, and Chapter 24 measured what that does to a window. MCP adds a second line item that is easy to miss, so both are worth counting on the reference server above.
13 tool definitions (name + description + inputSchema): 1,307 tokens
cheapest tool, get-tiny-image 52
costliest tool, gzip-file-as-resource 235
server `instructions`, returned by discovery: 312 tokens
------
one server, connected, before it is used: 1,619 tokensTwo observations. The first is arithmetic: connect five servers of this size and roughly eight thousand tokens of your window are spoken for on every turn, forever, whether or not the model uses any of them — which is the mechanism behind the 150,000-to-2,000 reduction Chapter 24 quoted, and the reason just-in-time tool discovery exists.
The second is a security note wearing an accounting costume. instructions is natural-language text, written by the server author, that lands in the host's prompt, and the tool descriptions beside it are the same. The specification says what to do about that in its own security principles: tool annotations and descriptions "should be considered untrusted, unless obtained from a trusted server", and hosts "must obtain explicit user consent before invoking any tool".1 Connecting an MCP server is not adding a dependency. It is granting a stranger 1,619 tokens of your system prompt and the right to be called. Chapter 30 is what happens when that stranger is hostile.
Dated section: the 2026-07-28 revision, and what it breaks
Link to the section: Dated section: the 2026-07-28 revision, and what it breaksEverything in this section is true of protocol revision 2026-07-28, the current one, read on 7 September 2026. Revisions are dated YYYY-MM-DD and the date is the last time a backwards-incompatible change was made.10 The normative document is a TypeScript file, schema/2026-07-28/schema.ts; the JSON Schema beside it is generated from it, which is why the specification is read here in TypeScript and why teaching MCP from anything else is teaching a translation.
| What changed | Was | Is now | Breaks |
|---|---|---|---|
| The handshake | initialize + notifications/initialized, once per connection | removed; every request carries _meta version and capabilities | every client written before this revision |
| Sessions | Mcp-Session-Id header, connection-scoped state | removed; state travels in explicit, server-minted handles | list endpoints that varied per connection |
| Discovery | inferred from the initialize result | server/discover, which servers must implement | nothing, but it is now mandatory to implement |
| Server-to-client calls | server sent roots/list, sampling/createMessage, elicitation/create | InputRequiredResult and a client retry | every server that pushed a request at a client |
| Result shape | any object | required resultType: "complete" or "input_required" | nothing: an absent field must be read as "complete" |
| Subscriptions | HTTP GET stream, resources/subscribe | one subscriptions/listen stream with opt-in types | the GET endpoint is gone |
| Stream resumption | Last-Event-ID replay on Streamable HTTP | removed; a broken stream loses the request, re-issue with a new id | clients that relied on redelivery |
| Roots | a client feature servers could ask for | deprecated (SEP-2577); pass paths as tool arguments or resource URIs | nothing yet — twelve-month window |
| Sampling and logging | client features | deprecated (SEP-2577) | nothing yet — twelve-month window |
| HTTP+SSE transport | deprecated since 2025-03-26 | Deprecated under the lifecycle policy (SEP-2596) | migrate to Streamable HTTP |
| Client registration | OAuth 2.0 Dynamic Client Registration, RFC 7591 | deprecated in favour of Client ID Metadata Documents | kept for authorization servers without them |
| Error codes | -32002 for resource not found | -32602; -32020–-32099 reserved for the spec | new codes -32020, -32021, -32022 |
The governance change underneath that table matters more than any single row. This revision adopted a feature lifecycle and deprecation policy: features are Active, Deprecated or Removed, a deprecated feature documents its migration path and stays in the specification for at least twelve months before it becomes eligible for removal, and there is a registry listing everything currently in the Deprecated state.8 Before that policy, "deprecated" in an AI protocol meant whatever the last blog post said. Now it means a date.
Show details
Extensions, which are the part nobody has written about yet.
Beyond the core, MCP defines optional extensions — "always opt-in and require explicit support from both client and server", declared through an extensions field in the client's and server's capabilities.1 Three are worth knowing by name:
- Tasks (
io.modelcontextprotocol/tasks), moved out of the core protocol into an official extension in this revision: asynchronous execution of long-running operations, with polling throughtasks/get, mid-flight input throughtasks/update, and durable handles. It is the answer to a tool that takes twenty minutes, which Chapter 23 handled with a progress event and a signal that reaches the tool. - Skills over MCP, a working group making agent skills — Chapter 28's subject — discoverable and consumable through the protocol.
- MCP Apps, interactive UI rendered inline in the conversation: charts, forms, video players.
And note what "negotiated" now means: there is no initialization to negotiate at, so an extension is declared per request like everything else.
Where MCP sits, against everything it gets confused with
Link to the section: Where MCP sits, against everything it gets confused withThis is the vocabulary of the whole block in one place.
| What it is | Who talks to whom | When it is the answer | |
|---|---|---|---|
| A plain API | An interface for a program | your code ↔ a service | You are writing the caller. You control the schema, the auth and the error handling, and there is no discovery problem to solve. |
| MCP | A protocol for exposing tools, data and templates to an AI application | host ↔ server, one client each | Somebody else wrote the capability and many hosts should be able to use it without a bespoke integration. |
| RAG | A technique for finding text and putting it in the prompt | your code ↔ your index | The model needs to know something. Chapter 19. MCP is a way to deliver a retriever; it is not a retriever. |
| Agent skills | A folder with a SKILL.md the model reads | model ↔ a document | The knowledge is procedural — how we do this — and it is prose, not a function. Chapter 28. |
| A2A | A protocol for agents to collaborate as peers | agent ↔ agent | The other side reasons, plans and holds state across a long task, rather than answering a call. |
| ACP | Was a separate agent-communication protocol | — | It is not a live comparison any more. See below. |
Two of those deserve a sentence each, because they are where the confusion actually lives.
MCP against A2A is not a rivalry, and both specifications say so. The A2A documentation draws the line by what is on the other end: MCP "defines how an AI agent interacts with and utilizes individual tools and resources, such as a database or an API", where a tool performs "specific, often stateless, functions"; A2A addresses agents, "more autonomous systems" that "reason, plan, use multiple tools, maintain state over longer interactions, and engage in complex, often multi-turn dialogues". Its own summary is the sentence to remember: "A2A is about agents partnering on tasks, while MCP is more about agents using capabilities."11 The two nest — an application uses A2A to reach other agents, and each agent uses MCP to reach its own tools. Chapter 25 drew that line inside one process, between asking a sub-agent and handing it the conversation; A2A draws it between organisations.
MCP against ACP is a comparison with a stale premise, which is exactly why it is worth answering. The Agent Communication Protocol was a separate open standard for agent-to-agent messaging. Its own documentation now opens with the notice: "ACP is now part of A2A under the Linux Foundation!"12 The honest answer to "MCP or ACP?" in September 2026 is that the question has one fewer option than the pages ranking for it suggest.
And the comparison people ask for most, mcp vs api, has the least interesting answer: MCP is an API. What it adds is not power, it is conventions — a fixed set of method names, a discovery call, a control hierarchy over the primitives, and an isolation model. You give up the freedom to design your own interface and you get every host that speaks the protocol, which is the trade every protocol has ever offered.
Where this goes next
Link to the section: Where this goes nextYou can now read the specification without a translator, tell a resource from a tool from a prompt by who is in charge of it, type a request by hand when a client library is lying to you, and date any MCP article you read by which of the deprecated features it still teaches as current.
What you have not done is ship one. Chapter 27 writes the same server twice — TypeScript and Python, side by side, because MCP is the one genuinely bilingual territory in this course and the numbers say so in both directions. It covers the two live transports properly, the inspector, packaging, and the half of the protocol this chapter deliberately left alone: authorization. Because the moment your server is remote rather than a subprocess on your own laptop, a stranger's client will present a token, and the specification's rule about what you may do with it is unusually strict.
Which raises the question the next chapter has to answer, and it is not a friendly one: if a token arrives at your server and it was issued for somebody else's audience, what exactly stops you from forwarding it?
Sources and method
Link to the section: Sources and methodEvery quotation, method name, error code and rule in this chapter was read from the Model Context Protocol specification, revision 2026-07-28, on 7 September 2026. Every trace was produced locally on Node 22: the toy calendar server is 101 lines with no dependencies, and the reference server is the published npm package named below. No paid API was called to write this chapter — nothing here needs a model, which is itself the point.
The measurements: @modelcontextprotocol/server-everything@2026.8.31, published 31 August 2026, built on @modelcontextprotocol/sdk@1.30.0, published 27 July 2026 — one day before the revision this chapter describes. It answers server/discover with -32601, negotiates 2025-11-25 when asked for 2026-07-28, and serves tools/list with no handshake at all. Its catalogue is 13 tools in 7,663 bytes; token counts are o200k_base via tiktoken, over the name, description and inputSchema of each definition, which is what a provider renders into your prompt and not what the JSON-RPC frame weighs.
Anthropic, Code execution with MCP: building more efficient agents, 4 November 2025, is the source of the 150,000-to-2,000 figure, quoted and used in Chapter 24 and only referenced here.
References
Link to the section: References-
Specification,
modelcontextprotocol.io/specification/latest(redirecting to/2026-07-28), read 7 September 2026. Source of the Language Server Protocol comparison; the statement that the specification is "based on the TypeScript schema inschema.ts"; the base-protocol summary ("Stateless, self-contained requests", "Per-request capability negotiation"); the extension list (Tasks, Skills over MCP, MCP Apps) and the statement that extensions "are always opt-in and require explicit support from both client and server"; and the Security and Trust & Safety principles, including "Hosts must obtain explicit user consent before invoking any tool" and the treatment of tool annotations as untrusted. ↩ ↩2 ↩3 -
Base Protocol,
modelcontextprotocol.io/specification/2026-07-28/basic. Source of the JSON-RPC constraints (non-null id, no id reuse, requiredresultType); the Statelessness section and its note that an open stdio process is not a session; the_metareserved-key table and the required/optional status of each per-request field; the-32602rule for a missing required field; theMissingRequiredClientCapability(-32021) rule; and the error-code allocation policy. ↩ ↩2 ↩3 ↩4 ↩5 -
stdio transport,
modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio. Source of the newline-delimited framing rules, thestdoutpurity requirement, thestderrallowance, and the three-outcome backward-compatibility probe — including the warning that some legacy servers process era-ambiguous methods without a handshake, which the measurement in this chapter reproduces. ↩ ↩2 ↩3 -
Transports overview,
modelcontextprotocol.io/specification/2026-07-28/basic/transports. Source of the "a transport is a binding" framing and of the statement that servers do not initiate JSON-RPC requests and clients do not send JSON-RPC responses. ↩ ↩2 -
Discovery,
modelcontextprotocol.io/specification/2026-07-28/server/discover. Source of the mandatory status ofserver/discover, the shape ofDiscoverResult, and theinstructionsfield described as "optional natural-language guidance for LLMs on how to use this server effectively". ↩ -
Architecture,
modelcontextprotocol.io/specification/2026-07-28/architecture. Source of the host/client/server definitions, the 1:1 client-to-server rule, the four design principles, of which the isolation principle is quoted here without its fifth bullet, "Host process enforces security boundaries", and the capability-negotiation section. ↩ ↩2 -
Elicitation,
.../client/elicitation, and Sampling,.../client/sampling. Source of the two elicitation modes and their restricted schema; the prohibition on requesting credentials through form mode; the sampling definition, its human-in-the-loop requirement, and the deprecation warning attached to it. ↩ -
Key Changes,
modelcontextprotocol.io/specification/2026-07-28/changelog, and Feature lifecycle and deprecation policy,.../community/feature-lifecycle. Source of every row of the change table: removal of sessions and theMcp-Session-Idheader (SEP-2567); statelessness and the removal ofinitialize(SEP-2575);server/discover(SEP-2575);subscriptions/listen(SEP-2575); Multi Round-Trip Requests andresultType(SEP-2322); removal of stream resumability (SEP-2575); the deprecation of Roots, Sampling and Logging (SEP-2577); the reclassification of HTTP+SSE (SEP-2596); the deprecation of Dynamic Client Registration in favour of Client ID Metadata Documents; the error-code renumbering; and the twelve-month deprecation window. ↩ ↩2 -
Tools,
modelcontextprotocol.io/specification/2026-07-28/server/tools, and Server Features,.../server. Source of the control-hierarchy table reproduced above; thetools/listandtools/callshapes; theisErrordistinction between protocol errors and tool execution errors; the tool-name rules and the namespace note recommending "prefixing tool names with a server identifier"; and the non-normative "Stateful Tools" guidance on explicit handles. ↩ -
Versioning,
modelcontextprotocol.io/specification/versioning. Source of theYYYY-MM-DDscheme, the Draft/Current/Final revision states, the confirmation that 2026-07-28 is current, and the per-request negotiation rules. The SDK tier table atmodelcontextprotocol.io/docs/sdklists TypeScript, Python, C#, Go and Rust at Tier 1, Java and Ruby at Tier 2, and Swift, PHP and Kotlin at Tier 3. ↩ -
A2A Protocol, version 1.0.0,
a2a-protocol.org— the specification and the page A2A and MCP: Relationship and Distinction, read 7 September 2026. Source of the tools-against-agents distinction, the statement that the two protocols "address distinct but highly complementary needs", and the partnering/using formulation. ↩ -
Agent Communication Protocol,
agentcommunicationprotocol.dev, read 7 September 2026: "ACP is now part of A2A under the Linux Foundation!", a banner added above a specification that is still served whole — architecture, agent manifest, agent discovery, message structure, stateful agents, run lifecycle and the REST endpoint list all still answer 200. The specification did not go away; the project did. ↩