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
TheMCP-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 declare2026-07-28 in the header:
initializestill 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 answered2026-07-28— that would name a revision in whichinitializedoes not exist, to a client that just proved it expects one in which it does.notifications/initializedis 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_metaserver 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.
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.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
_metafield 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_metafield is a malformed request. - A body that names a different revision than the header is
-32020in either era. A handshake-era client never writes that key, so the rule cannot affect it — but a body saying2026-07-28behind a header saying2025-06-18is 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.
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.

