Skip to main content
Preview. This server is dual-era: it answers the current MCP revision and the three handshake-era ones, on the same URL, at the same time. Serving both is explicitly permitted by the spec, and here it is the only honest option — every shipping client today speaks 2025-06-18, and a cutover would break all of them on the day of deploy.

The two eras

The authoritative list is mcpserver/protocol.py: SUPPORTED_PROTOCOL_VERSIONS, and server/discover reads it out over the wire — the section below shows how to ask. Adding a revision there is a decision about behaviour, not a string appended to a tuple, so that list is short on purpose.

Which era you are in

The MCP-Protocol-Version request header decides, and nothing else does. That is deliberate: the era is known before the body is parsed, before the credential is touched, and therefore before anything can run under an assumption that turns out not to hold. The body’s own declaration is then checked against the header, rather than being a second, competing source of the answer.
2026-07-28 permits a server to reject a request with no MCP-Protocol-Version header at all. This one does not. Keeping the fallback is most of the backward-compatibility guarantee, in one line: a client that has never sent that header is served today exactly as it was yesterday.

If your client already works, do nothing

There is no migration here, and that is a property of the code rather than an intention. Nothing in the modern era’s rules can be reached by a request that did not declare 2026-07-28 in the header:
  • initialize still works, and still negotiates. A client asking for a revision we speak is answered in it; one asking for anything else is answered with the newest handshake revision rather than being refused over a string. It is never answered 2026-07-28 — that would name a revision in which initialize does not exist, to a client that just proved it expects one in which it does.
  • notifications/initialized is still 202 with an empty body, in either era, and notifications are held to none of the header rules below.
  • Handshake-era results are unchanged. No resultType, no _meta server identity, no cache hints — a result that grew fields for everybody would still parse for most clients, which is exactly why nothing would notice until one strict client somewhere stopped connecting.

What a modern client must send

Every request. There is no handshake in which any of it could have been said once.
1

The MCP-Protocol-Version header

2026-07-28. A request carrying the modern _meta but no header is refused, not downgraded — served silently it would be answered as a 2025-03-26 client.
2

Two required _meta keys, under params

io.modelcontextprotocol/protocolVersion — the revision this one request is written against — and io.modelcontextprotocol/clientCapabilities, what the client can be asked to do. This server asks nothing of a client, so capabilities are read only to enforce their presence. io.modelcontextprotocol/clientInfo is optional, and self-reported: it is logged and shown, never trusted, because the bearer token is what says who this is.
3

Mirrored headers

Mcp-Method on every request, matching the body’s method. Mcp-Name on tools/call, matching params.name. Both are validated against the body and a mismatch is refused — see Why the headers are mirrored.
Header values are ASCII-only, so a name that is not gets a Base64 sentinel. Mcp-Name: =?base64?ZWNobw==?= is decoded before it is compared to the body, so a tool name with an accent or leading whitespace round-trips intact. The prefix and suffix are exact and case-sensitive; a wrapper that does not decode is compared as-is and therefore refused.

A modern request, beside the one that works today

server/discover

The modern way to ask what this server speaks, and the method that replaces the half of initialize’s job that survived: which revisions, what capabilities, who we are. It is a MUST in 2026-07-28. It agrees nothing. There is no session to agree it in. A client may call it, or skip it and let a wrong version come back as -32022 instead.
The result carries supportedVersions — every revision this server will answer on, which is the live reading of SUPPORTED_PROTOCOL_VERSIONS — plus capabilities and the same instructions the handshake era gets from initialize.
This server answers server/discover in both eras, deliberately. A handshake-era client has no reason to call it and none will — but a modern client that forgot the MCP-Protocol-Version header is read here as a 2025-03-26 client, and refusing it would answer “what do you speak?” with “no such method”. Answering says only what a public spec page says, and is the one reply that gets that client unstuck.supportedVersions lists the handshake revisions too, for the same reason: a client that discovers us and finds it cannot speak 2026-07-28 learns from that list that initialize is still an option here.
It needs a credential like every other method. An unauthenticated server/discover is 401 with a WWW-Authenticate challenge — which is a better answer than handing a passer-by this server’s capability list.

What a modern result looks like

The era changes the shape of an answer and never its behaviour. No authority, billing or tenancy decision anywhere in this server branches on the revision a request declared.
Those hints are worth having rather than ceremonial. The server declares listChanged: false — the registry is fixed at import, so there is no notification it could ever send — which makes the TTL the only thing standing between a client and a tools/list on every turn.

Errors this revision added

2026-07-28 partitions JSON-RPC’s implementation-defined range and reserves -32020 to -32099 for the spec’s own meanings, so these are not numbers this server was free to choose. The two it emits both travel on HTTP 400. Two more refusals are worth knowing:
  • A missing required _meta field is -32602, not -32020, and the error names the field. The spec splits them by what is missing: a missing header is a mirroring failure, a missing required _meta field is a malformed request.
  • A body that names a different revision than the header is -32020 in either era. A handshake-era client never writes that key, so the rule cannot affect it — but a body saying 2026-07-28 behind a header saying 2025-06-18 is precisely the split-brain the mirroring exists to catch.

Unknown methods are 404 for a modern client

A method this server does not implement is HTTP 404 carrying -32601, so a client can tell “this MCP server has no such method” from a bare 404 served by something that is not an MCP endpoint at all. A handshake-era client still gets -32601 on a 200, because that is what it was built to read and a 404 would look to it like a missing endpoint. initialize is one of those methods once you have declared 2026-07-28. It is refused rather than quietly answered — a client that got a plausible-looking handshake result would carry on believing it had a negotiated session, and this server keeps none. The refusal names server/discover and names the revisions in which initialize still works.

Why the headers are mirrored

Mcp-Method and Mcp-Name exist so a gateway can route, rate-limit or authorise without parsing the body. The moment the header and the body disagree, that gateway and this server are enforcing policy on two different requests — a proxy authorising a tools/list in front of a server executing a tools/call. There is no way to tell which one is the client’s intent, so refusing is the only answer that cannot be wrong.
There is no Mcp-Param-* check. Those headers mirror tool arguments a server asked for with x-mcp-header in its inputSchema, and no tool in this registry asks — so an unrecognised Mcp-Param-* is one an intermediary added, which RFC 9110 says to forward and ignore.

Still stateless, in both eras

2026-07-28 removed sessions as a concept, which changed nothing here: this endpoint has never minted an Mcp-Session-Id. One arriving from an older client is ignored and never echoed, which is what that revision tells a server to do with it. GET /mcp and DELETE /mcp are 405 in both eras. See Limits. JSON-RPC batching is not accepted in either era — it was removed in 2025-06-18 and did not come back. One message per POST.
The platform’s MCP client deliberately stayed on 2025-06-18. When the platform calls a tool server you run, it sends MCP-Protocol-Version: 2025-06-18 and runs initialize — that is core/mcp.py: PROTOCOL_VERSION, and it is a different half of the protocol from everything on this page. Nothing here asks you to implement 2026-07-28 on a server you host. See Building a tool server.

Any MCP client

The four facts that connect anything, and what an SDK on either era does when it meets this server.

Limits

Rate limits, and the transport answers that look like faults and are not.