JSON-RPC and the life cycle
Every message of a session, both protocol eras: the handshake and its version negotiation, the stateless 2026-07-28 revision, progress, errors, pagination and cancellation.
Loading the animation…
Concept
Every MCP message is a JSON-RPC 2.0 message (JSON-RPC 2.0): a request has a method, params and an id, and must be answered by a result or an error carrying the same id; a notification has no id and gets no answer. That is the whole wire alphabet. Pick a session in the animation and step through it: every arrow is one of those four kinds, and the JSON under the chart is the message exactly as sent.
The handshake era (2024-11-05 to 2025-11-25). A session starts with three messages. The client sends initialize with the version it wants, its capabilities and its name; the server answers with the version it will speak and its own capabilities; the client confirms with notifications/initialized (lifecycle, 2025-11-25, accessed 2026-10-08). After that, requests carry no envelope: the connection remembers what was agreed. In the first session that is 17 messages for a handshake, a ping, and one use of each primitive.
The stateless revision (2026-07-28). The current revision removed the handshake, protocol-level sessions and ping (changelog, accessed 2026-10-08). Instead every request carries its protocol version, the client's identity and its capabilities in params._meta, and every result says resultType and names the server. A client that wants to choose a version up front calls server/discover first. The price is bytes: here the envelope adds 194 bytes to every request on average. The gain is that any request can go to any server instance, with nothing to remember between them.
Negotiation. In the handshake era the client proposes a version ; the server replies with if it speaks it, or with the newest version it does speak, and the client must disconnect if it cannot use that. Try An old client (2024-11-05): the server agrees to the old version. Try Out of order, unknown version: a client asking for 1999-01-01 gets 2025-11-25 back. In 2026-07-28, server/discover returns the server's versions and the client takes the newest one both sides speak; a request with a version the server does not speak gets error -32022 listing the ones it does. Capabilities work the same way in both eras: a feature is usable only if the side that provides it declared it (the server for tools, resources and prompts; the client for elicitation and sampling).
Errors. Two kinds, and the difference matters to an agent. A protocol error is a JSON-RPC error: the method does not exist (-32601), the parameters are wrong (-32602), the version is not supported (-32022). A tool error is a normal result with isError: true and text the model can read and react to. Try Tool errors vs protocol errors: a tool that fails, and an unknown tool, come back as results; an unknown resource is a protocol error. The specification asks for input-validation failures to be tool errors so the model can correct itself (tools, accessed 2026-10-08).
Maths
Let and be the sets of revisions the client and the server speak. In 2026-07-28 the client picks
with dates compared as strings (they sort as dates). If there is no common version and the client reports an error. In the handshake era the client proposes one , and the server answers if , otherwise ; the client then needs that answer to be in . Here the official SDK's client speaks all five revisions, while the fixture server speaks the four handshake-era ones on a handshake connection and only 2026-07-28 on a stateless one.
The cost of statelessness is linear in the number of requests : with an envelope of bytes, a session sends more bytes than its handshake-era equivalent, and saves the three handshake messages.
Code
The server's state machine decides the era from the first valid request, cut from the vendored engine (the same branching the SDK's recordings show):
if (method === "initialize") {
this.era = "handshake";
const asked = params.protocolVersion;
this.version = HANDSHAKE_VERSIONS.includes(asked) ? asked : LATEST_HANDSHAKE;
The handshake-era opening request, as the engine and the SDK send it:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "fixture-client", "version": "1.0.0" }
}
}
The first request of a 2026-07-28 session, with no handshake before it:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "fixture-client", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
What changed, revision by revision
| Revision | What it brought (from each revision's changelog) |
|---|---|
| 2024-11-05 | The first release: JSON-RPC, stdio and HTTP+SSE, tools, resources, prompts, sampling |
| 2025-03-26 | Streamable HTTP replaces HTTP+SSE; OAuth 2.1 authorisation |
| 2025-06-18 | Structured tool output, elicitation, the MCP-Protocol-Version header; JSON-RPC batching removed |
| 2025-11-25 | URL-mode elicitation, tool calling in sampling, Client ID Metadata Documents, experimental tasks (changelog) |
| 2026-07-28 | No handshake or sessions; _meta on every request; server/discover; input_required results replace server-to-client requests; no stream resumption |
Sources: the specification's changelogs, accessed 2026-10-08 (2026-07-28). The rows for the first three revisions summarise the MCP series' timeline slide.