Appendix A: Protocol Specification Reference
Target revision: 2026-07-28. Every method, key and error code on this page was checked against that revision’s changelog, basic/index, basic/versioning, the MRTR pattern page, server/discover, server/tools, the Streamable HTTP transport page and schema.ts. The wire examples were captured from the notes server in Appendix C, running on mcp 2.2.0 over Streamable HTTP.
Requests Without a Handshake
Revisions up to 2025-11-25 opened every connection with an initialize request and a notifications/initialized notification, and HTTP servers could pin a session with Mcp-Session-Id. 2026-07-28 removes both. Instead:
- every request carries
io.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilitiesin_meta, and clients should also sendio.modelcontextprotocol/clientInfo. A request missing a required key is rejected with-32602(HTTP 400); - servers should return
io.modelcontextprotocol/serverInfoin every result’s_meta; - servers must implement
server/discover, which advertises supported versions, capabilities and identity. Clients may call it first, and on stdio it doubles as a backward-compatibility probe; - a version the server does not support returns
UnsupportedProtocolVersionError(-32022), listing the versions it does support; - state that must survive across calls is an explicit, server-minted handle passed as an ordinary tool argument, not a protocol session.
Capability confusion usually starts with a request that omits or misstates its _meta.
Messages are JSON-RPC 2.0 requests, result responses, error responses and notifications (spec: Messages). MCP adds one rule: every result carries a resultType of "complete" or "input_required", and a client treats a missing resultType from an older server as "complete".
Method Index
“Direction” is who sends the method. “Gate” is what must be declared before the method is used: a server capability (from server/discover), or a client capability (from the request’s _meta).
| Method | Direction | Gate | Taught in |
|---|---|---|---|
server/discover | client → server | none; every server implements it | ch02, ch29 |
tools/list | client → server | server tools | ch05, ch06 |
tools/call | client → server | server tools | ch05, ch06 |
resources/list | client → server | server resources | ch06 |
resources/templates/list | client → server | server resources | ch05 |
resources/read | client → server | server resources | ch05, ch06 |
prompts/list, prompts/get | client → server | server prompts | ch06 |
completion/complete | client → server | server completions | spec only |
subscriptions/listen | client → server | listChanged or resources.subscribe | ch06, ch28 |
elicitation/create | server → client, inside inputRequests | client elicitation | ch10 |
sampling/createMessage | server → client, inside inputRequests | client sampling (deprecated) | ch17 |
roots/list | server → client, inside inputRequests | client roots (deprecated) | ch17 |
notifications/{tools,prompts,resources}/list_changed | server → client, on a subscriptions/listen stream | listChanged on that feature | ch28 |
notifications/resources/updated | server → client, on a subscriptions/listen stream | resources.subscribe | ch06 |
notifications/subscriptions/acknowledged | server → client | an open subscriptions/listen | spec only |
notifications/progress | server → client, on the request’s own response stream | progressToken in the request’s _meta | spec only |
notifications/message | server → client, on the request’s own response stream | io.modelcontextprotocol/logLevel in _meta (Logging is deprecated) | ch23 |
notifications/cancelled | either direction | none | spec only |
tasks/get, tasks/update | client → server | extension io.modelcontextprotocol/tasks | ch10 |
Gone in 2026-07-28: initialize, notifications/initialized, ping, logging/setLevel, resources/subscribe, resources/unsubscribe, notifications/roots/list_changed, notifications/elicitation/complete, and the Tasks extension’s tasks/result and tasks/list.
Wire Examples
All five come from the notes server. The HTTP headers are shown once, on the first request, but every POST carries them.
1. A tools/call request with _meta. The MCP-Protocol-Version header must match the _meta version, and Mcp-Method/Mcp-Name must match the body. If they don’t, the server returns HeaderMismatch (-32020). traceparent is the OpenTelemetry key that Chapter 23 propagates.
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: search_notes
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "search_notes", "arguments": {"query": "plan"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "curl", "version": "8"},
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"}}}
2. A complete result with structuredContent. search_notes declares an outputSchema, so the result carries both the structured value and, for older clients, its serialized text (reflowed here onto one line). A tool error has the same shape, with "isError": true and the message in content.
{"jsonrpc": "2.0", "id": 2, "result": {
"resultType": "complete",
"isError": false,
"content": [{"type": "text",
"text": "{\"count\": 1, \"items\": [{\"id\": 1, \"title\": \"Plan\", \"tag\": \"work\", \"snippet\": \"Ship v2\"}]}"}],
"structuredContent": {"count": 1,
"items": [{"id": 1, "title": "Plan", "tag": "work", "snippet": "Ship v2"}]},
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "notes", "version": ""}}}}
3. An InputRequiredResult. This is a delete_note_confirmed variant that asks the user first, using the Resolve/Elicit pattern from Chapter 10. The request declared "clientCapabilities": {"elicitation": {"form": {}}}; without that declaration the server returns MissingRequiredClientCapability (-32021) instead. The server assigns the key (the Python SDK uses module:resolver). requestState is shortened here: it’s an integrity-protected blob, and the client must echo it back untouched.
{"jsonrpc": "2.0", "id": 10, "result": {
"resultType": "input_required",
"inputRequests": {
"server_confirm:confirm_delete": {
"method": "elicitation/create",
"params": {"mode": "form",
"message": "Permanently delete note 1?",
"requestedSchema": {"type": "object",
"properties": {"confirm": {"title": "Confirm", "type": "boolean"}},
"required": ["confirm"]}}}},
"requestState": "v1.NLdEzlN0Z26Y…"}}
4. The retry with inputResponses. The retry uses a new JSON-RPC id and repeats the original name and arguments. The answer is keyed the same way as the request, and requestState is echoed. A tampered requestState gets -32602 “Invalid or expired requestState”.
{"jsonrpc": "2.0", "id": 11, "method": "tools/call",
"params": {"name": "delete_note_confirmed", "arguments": {"note_id": 1},
"inputResponses": {
"server_confirm:confirm_delete": {"action": "accept", "content": {"confirm": true}}},
"requestState": "v1.NLdEzlN0Z26Y…",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}}}}}}
The reply is an ordinary "resultType": "complete" result. If an answer is missing, the server sends another InputRequiredResult, not an error.
5. Error -32022. It is sent with HTTP 400. The client picks a version from supported and retries.
{"jsonrpc": "2.0", "id": 4, "error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {"supported": ["2026-07-28"], "requested": "1900-01-01"}}}
Features and How They Are Negotiated
| Feature | Status in 2026-07-28 | Negotiated by | Chapter |
|---|---|---|---|
| Tools, resources, prompts | Core | server tools, resources, prompts | ch05, ch06 |
| Structured output | Core; outputSchema may use any JSON Schema 2020-12 keyword | per tool definition | ch06 |
| Tool annotations | Core; hints, not controls | per tool definition | ch06, ch20 |
| Elicitation (form, URL) | Core, via MRTR | client elicitation | ch10 |
| Argument completion | Core | server completions | spec only |
| Change notifications | Core; subscriptions/listen replaced resources/subscribe and the GET stream | listChanged, resources.subscribe | ch06, ch28 |
Cache hints (ttlMs, cacheScope) | Core; required on list, read and discover results | always present | ch13 |
| Tasks | Extension io.modelcontextprotocol/tasks | extensions | ch10 |
| MCP Apps | Extension io.modelcontextprotocol/ui | extensions | Part V opener |
| Sampling, Roots | Deprecated; call a provider directly, pass paths as parameters | client sampling, roots | ch17 |
| Logging | Deprecated; log to stderr or OpenTelemetry | server logging | ch23 |
| HTTP+SSE transport | Deprecated | transport choice | ch07 |
| Dynamic Client Registration | Deprecated; use CIMD | AS registration_endpoint | ch19 |
In 2026-07-28 the server no longer sends its own requests to the client. When it needs elicitation, sampling or roots, it returns an InputRequiredResult listing what it needs, and the client retries the original request with inputResponses. This is the Multi Round-Trip Request (MRTR) pattern. Only tools/call, resources/read and prompts/get may return an InputRequiredResult.
Transport Summary
| Transport | Best fit | Notes |
|---|---|---|
| STDIO | Local desktop or embedded servers | Keep logs off stdout; clients probe with server/discover |
| Streamable HTTP | Remote servers | One POST per message; MCP-Protocol-Version, Mcp-Method, Mcp-Name headers |
| HTTP plus SSE | Legacy compatibility | Deprecated; use only when a client you must support requires it |
Streamable HTTP in 2026-07-28 is a plain HTTP workload. There is no session header and no resumability: if a response stream breaks, the client re-issues the request with a new ID. Servers must validate Origin, answering an invalid one with 403. A local server should bind to 127.0.0.1. A method the server doesn’t implement gets HTTP 404 with -32601.
Error Codes
| Code | Name | Usually indicates |
|---|---|---|
-32700 | Parse error | Broken JSON |
-32600 | Invalid request | Bad JSON-RPC shape |
-32601 | Method not found | Wrong method or unsupported capability (HTTP 404) |
-32602 | Invalid params | Argument mismatch, missing _meta keys, bad requestState, or resource not found (was -32002) |
-32603 | Internal error | Unhandled server failure |
-32000 to -32019 | Legacy, implementation-defined | Don’t allocate new codes here; a receiver assumes no meaning |
-32020 | HeaderMismatch | An HTTP header disagrees with the body (HTTP 400) |
-32021 | MissingRequiredClientCapability | The server needs a capability the client didn’t declare; data.requiredCapabilities names it |
-32022 | UnsupportedProtocolVersion | data.supported lists the alternatives |
The rest of -32020 to -32099 is reserved for the specification. -32002 and -32042 (URL elicitation required, 2025-11-25 only) are retired: a 2026-07-28 server must not emit them, though a client should still accept -32002 from older servers. Application-defined codes go outside -32768 to -32000. An expected tool failure is not a protocol error at all. It is a result with isError: true (Chapter 5).
Protocol Revision Landmarks
| Revision | Notable impact for builders |
|---|---|
2024-11-05 | First public revision |
2025-03-26 | Streamable HTTP replaces HTTP+SSE; OAuth authorization; tool annotations |
2025-06-18 | Structured tool output, elicitation, protected resource metadata, the MCP-Protocol-Version header |
2025-11-25 | OpenID Connect discovery, experimental Tasks, Client ID Metadata Documents |
2026-07-28 | Stateless protocol, server/discover, MRTR, Tasks as an extension, subscriptions/listen, DCR/Roots/Sampling/Logging deprecated, RFC 9207 iss validation |
Migrating a 2025-11-25 Server to 2026-07-28
Upgrading to an SDK release that speaks 2026-07-28 takes care of the first three items. The rest are design changes an SDK can’t make for you.
- Delete
initializehandling. Read the version and capabilities from each request’s_meta, and answerserver/discover. → ch02, ch29 - Return
resultType, plusttlMsandcacheScopeon the results that require them. → ch13 - Reject unsupported versions with
-32022, and probe the deployed endpoint for it. → ch22 - Remove
Mcp-Session-Id. Anything that lived in the session becomes a server-minted handle, passed as a tool argument and bound to the caller. → ch10 - Replace server-initiated requests with MRTR.
ctx.elicit()inside a tool body fails on a 2026-07-28 connection. Move the question into a resolver with no side effects, because the resolver runs on every round. → ch10 - Change resource-not-found from
-32002to-32602, and move any custom codes out of-32020to-32099. → ch05 - Replace
resources/subscribeand the HTTP GET stream withsubscriptions/listen. → ch06 - Drop SSE resumability (
Last-Event-ID). A broken stream now means the client re-sends, so make re-sendable tools idempotent. → ch07 - Move off HTTP+SSE to Streamable HTTP, and make every proxy pass
MCP-Protocol-Version,Mcp-MethodandMcp-Namethrough unchanged. → ch07 - Prefer CIMD over DCR in any client you ship. If you keep DCR, send
application_type. Key stored credentials by issuer. → ch19 - Validate
issin authorization responses (RFC 9207). → ch19 - Remove the
loggingcapability and log to stderr or OpenTelemetry instead. → ch23 - Stop using Roots and Sampling. Paths become tool parameters or configuration, and model calls go directly to a provider. → ch17
- Move Tasks to the
io.modelcontextprotocol/tasksextension, and replace a blockingtasks/resultwithtasks/getpolling. → ch10 - Test against the clients you support. A modern-only server fails every legacy client, so decide whether to run dual-era. → Part V opener