What MCP actually is on the wire — JSON-RPC primitives, host/client/server topology, capability negotiation, lifecycle, and the version timeline from 2024-11-05 to 2026-07-28, the revision that made MCP stateless. Both eras are covered; handshake-era material is tagged “before 2026-07-28”.
By late 2024 every serious LLM application had reinvented the same wheel: a bespoke way to plug a model into a database, a filesystem, a ticketing system, an internal API. M AI applications times N data sources is M×N integrations — each one a brittle bag of function-calling JSON, prompt scaffolding, and authentication glue.
Anthropic open-sourced the Model Context Protocol in November 2024 to collapse this to M+N: each application speaks one protocol, each tool ships one server. The marketing line is “USB-C for AI” — a single shaped plug that any compliant host can talk to.
Every host wrote its own connector for every tool. Cursor + Postgres, Claude Desktop + Postgres, Continue + Postgres, … same job, three implementations, three sets of bugs.
The Postgres team writes one server. Every compliant host gets it for free. The work moves from N-app-teams to one tool-team.
MCP is small enough to read in an afternoon, JSON-RPC over a stream that any language can speak, and it ships with reference SDKs in Python, TypeScript, Java, C#, Kotlin, Swift, Go and Ruby. The barrier to writing a server is “define a function and a JSON Schema”.
The MCP architecture has three named participants. Mixing them up is the single most common source of confusion when reading the spec, so it's worth pinning down up front:
| Role | Who plays it | Job |
|---|---|---|
| Host | Claude Desktop, Claude Code, Cursor, Zed, Continue, VS Code, your own agent runtime | Owns the LLM. Orchestrates the conversation. Decides which servers to load. |
| Client | A library inside the host — one client instance per connected server | Speaks the JSON-RPC protocol. 1:1 with a server. Stateful. |
| Server | The Postgres server, the GitHub server, your own internal data server | Exposes tools, resources, prompts. Knows nothing about the LLM. Pure protocol citizen. |
Think of the host as the operating-system shell, the clients as file descriptors, and the servers as long-running daemons. The host is where trust lives; the servers are dumb actuators that the host has chosen to load.
MCP doesn't invent a new wire format. Every message is a JSON-RPC 2.0 envelope, defined by the 5-page spec from 2010. There are exactly three message shapes:
Has an id. Expects a response. {"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
Carries the same id. Either result or error, never both. {"jsonrpc":"2.0","id":1,"result":{...}}
Has no id. Fire-and-forget. {"jsonrpc":"2.0","method":"notifications/progress"} (params omitted; the deck's earlier example, notifications/, exists only before 2026-07-28).
JSON-RPC defines reserved codes; MCP layers a small set on top. Anything outside -32768..-32000 is yours to define for tool-level failures.
| Code | Name | When |
|---|---|---|
-32700 | Parse error | The JSON itself was malformed |
-32600 | Invalid request | Not a valid JSON-RPC envelope |
-32601 | Method not found | The method name is unknown |
-32602 | Invalid params | The params didn't match the schema |
-32603 | Internal error | Catch-all for server bugs |
-32002 | Resource not found before 2026-07-28 | MCP-specific (resources/read on a missing URI). In 2026-07-28 a missing resource is -32602; clients should still accept -32002 from older servers. |
-32020 | HeaderMismatch 2026-07-28 | An HTTP header (Mcp-Method, Mcp-Name…) disagrees with the JSON body |
-32021 | MissingRequiredClientCapability 2026-07-28 | The request needs a client capability it did not declare in _meta |
-32022 | UnsupportedProtocolVersion 2026-07-28 | The server does not speak the version the request declared; data.supported lists the ones it does |
If a tool runs and the operation fails (e.g. create_issue hit a 401), the JSON-RPC call still succeeds with a result. The tool result carries isError: true and the failure message. JSON-RPC errors are reserved for protocol-level problems — bad params, missing methods, unmarshalable JSON. Mixing the two is the bug every new server author writes once.
Source: MCP specification 2026-07-28 — Overview: error codes; Key changes (accessed 2026-10-08).
The protocol's surface area is small: three primitives a server can offer, two that a client can offer back. Everything else in MCP is plumbing for these.
Side-effecting functions the model can call. tools/list, tools/call. Each tool ships a JSON Schema for its arguments and (since 2025-06-18) for its structured output. Annotations declare read-only / destructive / open-world.
Read-only data identified by URI. resources/list, resources/read, resources/subscribe. Subscriptions push resources/updated notifications — how a server tells the client “the file you were watching changed”.
Reusable, parameterised templates the user can invoke explicitly — typically as slash commands or quick actions. prompts/list, prompts/get. Different from a tool: a prompt is composed and shown to the user, a tool is executed.
sampling/createMessage. The server asks the client to call its LLM. Lets a server build agentic behaviour without bringing its own model. The host MUST gate this with explicit user approval.
elicitation/create, standardised in 2025-06-18. The server presents a typed, schema-constrained question (“destination zip code?”) and the client renders a form. Returns accept, decline, or cancel.
Roots are URIs the host has authorised the server to touch (e.g. a project directory). roots/list is a client-side request.
Logging is the notifications/message stream and logging/setLevel request — the server can emit structured log records to the client at standard syslog levels.
No more server-initiated requests. Sampling, elicitation and roots/list still exist, but a server now asks for them by returning a result with resultType: "input_required"; the client gathers the answers and retries the original tools/call, prompts/get or resources/read with inputResponses (multi round-trip requests, deck 03).
Deprecated: Roots, Sampling and Logging (still functional, earliest removal in the first revision on or after 2027-07-28). logging/setLevel is gone; a client sets io.modelcontextprotocol/logLevel per request. resources/subscribe is replaced by subscriptions/listen.
Source: MCP specification 2026-07-28 — Multi round-trip requests; Deprecated features; Subscriptions (accessed 2026-10-08).
before 2026-07-28 The handshake form is shown first; the callout at the end gives the 2026-07-28 form.
MCP explicitly avoids feature-detection by trial-and-error. At handshake time both sides exchange a capabilities object; nothing else can be assumed. This is what lets a Python server written against the 2024-11-05 spec keep working with a Claude Desktop running 2025-06-18.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true,
"listChanged": true },
"prompts": { "listChanged": true },
"logging": {},
"completions": {} // argument autocompletion
},
"serverInfo": { "name": "my-server", "version": "0.4.1" },
"instructions": "Use the ‘query’ tool to read the database."
}
}
An empty object ("logging": {}) means “I support this primitive but with no sub-options”. An absent key means “I do not support this primitive”. The client MUST NOT call logging/setLevel on a server that didn't declare logging. Calling an undeclared primitive returns -32601 Method not found.
| Flag | Meaning |
|---|---|
tools.listChanged | Server may emit notifications/tools/list_changed when its tool catalogue changes at runtime |
resources.subscribe | Server supports resources/subscribe — per-URI change watching |
resources.listChanged | Server may emit a global resources-changed notification (catalogue-level) |
sampling (client side) | Client has an LLM available and will service sampling/createMessage |
elicitation (client side) | Client can render schema-constrained user prompts |
roots.listChanged | Client will tell the server when the root list changes (e.g. user opened a new project) — the notification is removed in 2026-07-28 |
There is no initialize, so nothing is negotiated once. Every request carries io.modelcontextprotocol/clientCapabilities (and its protocol version) in _meta; a server that needs an undeclared client capability returns -32021 MissingRequiredClientCapability. The server advertises its own capabilities through server/discover, which every server MUST implement and a client MAY call first. “Absent ≠ empty” still holds.
Source: MCP specification 2026-07-28 — Overview: _meta; Discovery (accessed 2026-10-08).
initialize → operate → shutdown before 2026-07-28Up to and including 2025-11-25 (the “legacy” era in the spec's own terms), every MCP session goes through the same three phases. The protocol is strict about ordering: a request issued in the wrong phase is a protocol error.
The client proposes a version. The server picks one it supports (typically the highest mutually-supported value) and returns it in the initialize result. From 2025-06-18 onward the agreed version MUST also be sent on every subsequent HTTP request as MCP-Protocol-Version: 2025-06-18 — this is what catches misconfigured proxies and stale clients.
Revision 2026-07-28 removed initialize, notifications/initialized, ping and protocol-level sessions. MCP is now stateless: every request carries its protocol version and client capabilities in _meta, and a server must not infer anything from earlier requests on the same connection.
initializeThe client declares a version on every request (_meta, plus the MCP-Protocol-Version header on HTTP). A server that does not implement it answers UnsupportedProtocolVersionError (-32022) listing supported; the client retries with a common version. server/discover lets it learn the list up front, and doubles as a probe for legacy servers on stdio.
State that must span calls is an explicit handle the server mints and the client passes back as an ordinary argument. Long-lived streams are requests too: subscriptions/listen opens one response stream of opted-in change notifications (deck 02). A server that needs more input mid-call returns resultType: "input_required" (deck 03).
The spec calls handshake-based revisions (2025-11-25 and earlier) legacy and 2026-07-28 onwards modern; a dual-era implementation speaks both. The official Python SDK (mcp 2.x) is dual-era. Chapter 2 of Agent Protocols Explained animates both life cycles side by side from a simulator checked against that SDK.
Source: MCP specification 2026-07-28 — Key changes; Versioning and compatibility; Overview: statelessness; Discovery (accessed 2026-10-08).
MCP versions are date-stamped, not semver. The string is literally the date the spec was frozen. Five revisions have shipped; as of October 2026 the current one is 2026-07-28:
| Version | Headline | What changed |
|---|---|---|
2024-11-05 | Initial release | Open-sourced by Anthropic. JSON-RPC + stdio + HTTP/SSE. Tools, resources, prompts, sampling, roots, logging. |
2025-03-26 | Streamable HTTP & auth | New Streamable HTTP transport (HTTP+SSE deprecated). OAuth 2.1 normative. Audio content type. Tool annotations (read-only/destructive/etc). |
2025-06-18 | Handshake era before 2026-07-28 (this deck's original “current production target”) | JSON-RPC batching removed. Elicitation standardised. Structured tool output (separate structuredContent). Resource links in tool results. Mandatory MCP-Protocol-Version header. OAuth resource server clarifications (RFC 8707 audience binding). |
2025-11-25 | Last handshake revision before 2026-07-28 | OpenID Connect discovery for auth servers. Client ID Metadata Documents recommended for registration. URL-mode elicitation. Tool calling inside sampling. Icons. Experimental tasks. Incremental scope consent. |
2026-07-28 | Current — stateless 2026-07-28 | initialize/initialized, sessions (Mcp-Session-Id), ping and Last-Event-ID resumption removed. Version + capabilities in every request's _meta; server/discover. Server→client requests replaced by resultType: "input_required" + retry. subscriptions/listen. Tasks moved to an extension. Roots, Sampling, Logging and Dynamic Client Registration deprecated. |
Date-stamping makes the spec trivially comparable (string sort ≡ chronological order) and dodges the semver fight over what counts as “breaking”. The compatibility contract is: servers SHOULD support a window of recent versions; clients negotiate the highest both sides know — through initialize before 2026-07-28, and per request (retry on -32022, or server/discover first) from 2026-07-28.
Batched JSON-RPC died. If you have a 2024-11-05 server happily returning [{...}, {...}], a 2025-06-18 client will refuse it. The reasoning was that batching never composed cleanly with SSE streams and almost no SDKs implemented it correctly.
HTTP+SSE is deprecated, not removed. Existing servers keep working; new ones SHOULD use Streamable HTTP. Deck 02 covers the wire-level differences.
2026-07-28 is the biggest break since launch. A modern client talking to a legacy server must fall back to initialize; a modern server must not expect it. Features are now retired through a published lifecycle: Deprecated features keep working for at least twelve months.
Source: MCP specification 2026-07-28 — Key changes; Versioning and compatibility; Deprecated features; 2025-11-25 key changes (accessed 2026-10-08).
Protocol version 2025-06-18. End-to-end: a Claude Desktop client connects to a tiny weather server, lists tools, calls one, receives a structured result. Every line on the wire is shown.
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18",
"capabilities":{
"sampling":{},
"elicitation":{},
"roots":{"listChanged":true}
},
"clientInfo":{"name":"claude-desktop","version":"0.13.0"}
}}
{"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2025-06-18",
"capabilities":{
"tools":{"listChanged":true},
"resources":{"subscribe":false,"listChanged":true}
},
"serverInfo":{"name":"weather-server","version":"1.2.0"},
"instructions":"Provide forecasts via the ‘forecast’ tool. Locations must be ISO 3166-2 codes."
}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
// → client
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
// ← server
{"jsonrpc":"2.0","id":2,"result":{
"tools":[{
"name":"forecast",
"title":"Weather Forecast",
"description":"Return a 24-hour forecast for an ISO 3166-2 region.",
"inputSchema":{
"type":"object",
"properties":{ "region":{"type":"string","pattern":"^[A-Z]{2}-[A-Z0-9]{1,3}$"} },
"required":["region"]
},
"outputSchema":{
"type":"object",
"properties":{
"tempC":{"type":"number"},
"windKph":{"type":"number"},
"summary":{"type":"string"}
},
"required":["tempC","windKph","summary"]
},
"annotations":{ "readOnlyHint":true, "openWorldHint":false }
}]
}}
// → client
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
"name":"forecast",
"arguments":{"region":"GB-LND"}
}}
// ← server
{"jsonrpc":"2.0","id":3,"result":{
"content":[{"type":"text","text":"London: 12°C, 14 km/h, light rain easing this evening."}],
"structuredContent":{"tempC":12.0,"windKph":14.0,"summary":"light rain easing"},
"isError":false
}}
The model still sees the human-readable content — that's what gets concatenated into the LLM's context. The structuredContent is for downstream code: a workflow engine that wants to branch on tempC doesn't have to regex it out of the prose. Validation against outputSchema is the host's responsibility — but if the schema is published, both sides can rely on it.
The same weather tool under 2026-07-28. There is no initialize and no notifications/initialized: the first message can be the tool call itself. Steps 1–3 of the previous slide disappear; what is left is longer per message and shorter overall.
// → client
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{
"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"example-host","version":"1.0.0"},
"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}
}
}}
// ← server
{"jsonrpc":"2.0","id":1,"result":{
"resultType":"complete",
"tools":[{ "name":"forecast", ... }],
"ttlMs":300000,
"cacheScope":"public",
"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"weather-server","version":"1.2.0"}}
}}
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: forecast
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
"name":"forecast",
"arguments":{"region":"GB-LND"},
"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}
}
}}
// ← server
{"jsonrpc":"2.0","id":2,"result":{
"resultType":"complete",
"content":[{"type":"text","text":"London: 12°C, 14 km/h, light rain easing this evening."}],
"structuredContent":{"tempC":12.0,"windKph":14.0,"summary":"light rain easing"},
"isError":false
}}
_meta on every request replaces the handshake (version and capabilities are required; clientInfo is a SHOULD). resultType is required on every result ("complete" here; a client treats a legacy result without it as complete). List results carry ttlMs/cacheScope caching hints. On HTTP, Mcp-Method/Mcp-Name headers mirror the body so gateways can route without parsing JSON, and there is no Mcp-Session-Id. The ttlMs value here is illustrative.
Source: MCP specification 2026-07-28 — Overview: _meta and resultType; Tools; Caching; Streamable HTTP (accessed 2026-10-08).
id; notifications don't. Tool failures live in result.isError, not in JSON-RPC errors.initialize result. From 2026-07-28: they ride in every request's _meta, and server/discover lists the server's.Deck 02 dives into the transports — stdio, the deprecated HTTP+SSE, and the modern Streamable HTTP. Deck 03 walks through writing a real server in Python and TypeScript. Deck 04 is the one your security team will want to read.
For animated, simulator-driven versions of these slides in both eras, see Agent Protocols Explained (chapters 1–3).