Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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/protocolVersion and io.modelcontextprotocol/clientCapabilities in _meta, and clients should also send io.modelcontextprotocol/clientInfo. A request missing a required key is rejected with -32602 (HTTP 400);
  • servers should return io.modelcontextprotocol/serverInfo in 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).

MethodDirectionGateTaught in
server/discoverclient → servernone; every server implements itch02, ch29
tools/listclient → serverserver toolsch05, ch06
tools/callclient → serverserver toolsch05, ch06
resources/listclient → serverserver resourcesch06
resources/templates/listclient → serverserver resourcesch05
resources/readclient → serverserver resourcesch05, ch06
prompts/list, prompts/getclient → serverserver promptsch06
completion/completeclient → serverserver completionsspec only
subscriptions/listenclient → serverlistChanged or resources.subscribech06, ch28
elicitation/createserver → client, inside inputRequestsclient elicitationch10
sampling/createMessageserver → client, inside inputRequestsclient sampling (deprecated)ch17
roots/listserver → client, inside inputRequestsclient roots (deprecated)ch17
notifications/{tools,prompts,resources}/list_changedserver → client, on a subscriptions/listen streamlistChanged on that featurech28
notifications/resources/updatedserver → client, on a subscriptions/listen streamresources.subscribech06
notifications/subscriptions/acknowledgedserver → clientan open subscriptions/listenspec only
notifications/progressserver → client, on the request’s own response streamprogressToken in the request’s _metaspec only
notifications/messageserver → client, on the request’s own response streamio.modelcontextprotocol/logLevel in _meta (Logging is deprecated)ch23
notifications/cancelledeither directionnonespec only
tasks/get, tasks/updateclient → serverextension io.modelcontextprotocol/tasksch10

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

FeatureStatus in 2026-07-28Negotiated byChapter
Tools, resources, promptsCoreserver tools, resources, promptsch05, ch06
Structured outputCore; outputSchema may use any JSON Schema 2020-12 keywordper tool definitionch06
Tool annotationsCore; hints, not controlsper tool definitionch06, ch20
Elicitation (form, URL)Core, via MRTRclient elicitationch10
Argument completionCoreserver completionsspec only
Change notificationsCore; subscriptions/listen replaced resources/subscribe and the GET streamlistChanged, resources.subscribech06, ch28
Cache hints (ttlMs, cacheScope)Core; required on list, read and discover resultsalways presentch13
TasksExtension io.modelcontextprotocol/tasksextensionsch10
MCP AppsExtension io.modelcontextprotocol/uiextensionsPart V opener
Sampling, RootsDeprecated; call a provider directly, pass paths as parametersclient sampling, rootsch17
LoggingDeprecated; log to stderr or OpenTelemetryserver loggingch23
HTTP+SSE transportDeprecatedtransport choicech07
Dynamic Client RegistrationDeprecated; use CIMDAS registration_endpointch19

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

TransportBest fitNotes
STDIOLocal desktop or embedded serversKeep logs off stdout; clients probe with server/discover
Streamable HTTPRemote serversOne POST per message; MCP-Protocol-Version, Mcp-Method, Mcp-Name headers
HTTP plus SSELegacy compatibilityDeprecated; 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

CodeNameUsually indicates
-32700Parse errorBroken JSON
-32600Invalid requestBad JSON-RPC shape
-32601Method not foundWrong method or unsupported capability (HTTP 404)
-32602Invalid paramsArgument mismatch, missing _meta keys, bad requestState, or resource not found (was -32002)
-32603Internal errorUnhandled server failure
-32000 to -32019Legacy, implementation-definedDon’t allocate new codes here; a receiver assumes no meaning
-32020HeaderMismatchAn HTTP header disagrees with the body (HTTP 400)
-32021MissingRequiredClientCapabilityThe server needs a capability the client didn’t declare; data.requiredCapabilities names it
-32022UnsupportedProtocolVersiondata.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

RevisionNotable impact for builders
2024-11-05First public revision
2025-03-26Streamable HTTP replaces HTTP+SSE; OAuth authorization; tool annotations
2025-06-18Structured tool output, elicitation, protected resource metadata, the MCP-Protocol-Version header
2025-11-25OpenID Connect discovery, experimental Tasks, Client ID Metadata Documents
2026-07-28Stateless 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 initialize handling. Read the version and capabilities from each request’s _meta, and answer server/discover. → ch02, ch29
  • Return resultType, plus ttlMs and cacheScope on 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 -32002 to -32602, and move any custom codes out of -32020 to -32099. → ch05
  • Replace resources/subscribe and the HTTP GET stream with subscriptions/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-Method and Mcp-Name through 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 iss in authorization responses (RFC 9207). → ch19
  • Remove the logging capability 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/tasks extension, and replace a blocking tasks/result with tasks/get polling. → 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