Writing ·
How to migrate an MCP server to the 2026-07-28 revision
MCP 2026-07-28 removes the initialize handshake for a stateless, per-request protocol — detect a peer's era with the documented probes, keep authorization failures separate from version negotiation, and hold traffic until the still-unreleased revision is verified.
By Youssef Hemimy · MCP · protocol · compatibility
MCP's 2026-07-28 revision is not an incremental field change — it removes the handshake. The modern era carries protocol version, capabilities, and identity on every request instead of negotiating them once at connection time. Treat migration as a compatibility rollout: detect a peer's era with the documented probes, keep authorization failures out of that detection, and hold traffic on any connection you have not verified — the specification itself is still unreleased.
- 0
- initialize handshakes required per request in the modern era
- 3
- new JSON-RPC error codes reserved for this revision
- 4
- standard HTTP headers Streamable HTTP now requires
- 2
- mechanisms removed from Streamable HTTP: sessions and the GET stream
This is a wire redesign, not a dependency bump
It is easy to read “protocol revision” and picture a few renamed fields. The MCP specification itself frames 2026-07-28 differently: it defines a “modern” era (2026-07-28 and later) that drops the initializehandshake entirely, and a “legacy” era (2025-11-25 and earlier) that keeps it. In the modern era, every request is self-contained — it carries its own protocol version, capabilities, and optional client identity in _meta, and a server processes each request independently with no state inferred from prior requests on the same connection.
The Streamable HTTP binding drops two things that shaped a lot of existing MCP deployments: the standalone GET SSE stream, and protocol-level sessions (Mcp-Session-Id). Server-to-client interactions — sampling, elicitation, roots — no longer arrive as independent JSON-RPC requests on an open stream; a server instead returns an InputRequiredResult, and the client retries the original request carrying the answer.
What changed on the wire
| Dimension | Legacy (2025-11-25 and earlier) | Modern (2026-07-28+) |
|---|---|---|
| Connection setup | An initialize request establishes a session before other requests are valid. | No handshake. Every request declares its protocol version, capabilities, and (optionally) client identity in _meta. |
| Session state | Streamable HTTP servers can assign a session via Mcp-Session-Id. | No protocol-level sessions. A server must not infer state from prior requests on the same connection. |
| Server-initiated requests | The server can send its own JSON-RPC requests (sampling, elicitation, roots) on an open SSE stream. | The server returns an InputRequiredResult; the client retries the original request with the answer (MRTR). |
| Server identity | serverInfo is part of the initialize result body. | serverInfo is optional, self-reported, and carried in result _meta under io.modelcontextprotocol/serverInfo. |
| Version mismatch | Handled by handshake-specific, implementation-defined behavior. | A dedicated UnsupportedProtocolVersionError (-32022) lists the versions the server actually supports. |
Detect a peer's era with the documented probe, not a guess
The specification treats era detection as a first-class, transport-specific mechanism — not something an integrator should reinvent per project.
- stdio: send
server/discoverfirst. ADiscoverResultmeans the server is modern; a recognized modern error (such asUnsupportedProtocolVersionError) means the server is modern but does not support the requested version — do not fall back toinitialize; any other error, or no response within a reasonable timeout, means the server is legacy. - Streamable HTTP: attempt a modern request first. On
400 Bad Request, inspect the response body. A recognized modern JSON-RPC error (UnsupportedProtocolVersionError,HeaderMismatch,MissingRequiredClientCapability) means the server is modern; an empty or unrecognized body means legacy — fall back toinitialize.
Streamable HTTP also now mirrors several body fields into required headers — MCP-Protocol-Version, Mcp-Method, and, for tool, resource, or prompt calls, Mcp-Name — so that a gateway or load balancer can route and validate a request without parsing JSON. A mismatch between a header and the corresponding body value is rejected with HeaderMismatch (-32020), which means a proxy that rewrites one without the other will start breaking modern-era traffic it previously passed through untouched.
Do not let an authorization failure masquerade as a legacy peer
Era detection and authorization are separate facts in the specification, and it is worth keeping them separate in your own instrumentation too. The backward-compatibility mechanism above is scoped strictly to inspecting a 400 response body (HTTP) or a server/discover outcome (stdio). HTTP 401 and 403are not part of that mechanism at all — they are authorization outcomes, defined in MCP's separate authorization specification: 401 means authorization is required or the token is invalid, 403means the token's scopes are insufficient.
Build a compatibility matrix before you change traffic
Before shifting any real connection onto the modern era, inventory every peer that matters and record what you actually verified for it — not what the specification permits in principle.
| Connection | What to record | Release decision |
|---|---|---|
| HTTP client → server | Client version, MCP endpoint, era-probe result, auth boundary | Does the client correctly distinguish a 400 era signal from a 401/403 auth failure? |
| stdio host → server process | Host version, process command, server/discover response | Does the server implement server/discover and answer correctly? |
| Proxy or gateway → server | Header forwarding for MCP-Protocol-Version, Mcp-Method, Mcp-Name | Does it forward headers unmodified, or does it risk a HeaderMismatch? |
| Test fixture or replay harness → server | Fixed wire fixtures for both eras, including the removed session/GET-stream paths | Does it cover the modern _meta shape and the legacy initialize shape? |
This table is AgentOps Hardening: compatibility becomes a release decision backed by evidence, not an inference from a changed dependency. It is Bonfire's recommended method — the specification does not publish a universal matrix for every host or deployment topology.
Use a release gate that can hold or roll back
- Record which era each connection actually negotiated, not which era you intended.
- Assert the changed metadata shape directly: serverInfo in result _meta, not the body; clientInfo optional but rejected if malformed.
- Exercise the required authorization path as its own test, separate from any era probe.
- Keep the legacy handshake path passing until every required peer has verified modern-era evidence.
- Give any failing matrix row an owner and a hold/rollback action before broadening traffic.
Pair this with the operational discipline in how to build a custom MCP server that survives production and the boundary questions in how to securely expose a private MCP server. Neither of those depends on which era a given connection speaks; this migration should not force you to relitigate them.
Scope and caveats
This is not a claim that 2026-07-28 is final, that every MCP implementation or SDK has adopted it, or that adoption will happen on any particular timeline. The specification content used here was read directly from the unreleased draft path of the official specification; it does not describe any specific SDK's package version, default client configuration, or API surface. Verify your own client, server, proxy, and transport combinations before treating any of them as migrated.
Sources
FAQ
Is the 2026-07-28 MCP revision the current, finalized specification?
No. As of this writing, 2026-07-28 is documented under the specification's unreleased draft path, and the spec's own changelog describes it as changes accumulating since the most recent release — not the most recently published, dated revision.
Does the 2026-07-28 revision still use an initialize handshake?
Not for the modern era. Every request instead declares its own protocol version, capabilities, and (optionally) client identity in _meta. The initialize handshake is retained only for interoperating with legacy peers (2025-11-25 and earlier).
How should a client detect whether a server speaks the modern or legacy era?
The mechanism differs by transport. On stdio, the client sends server/discover first; a DiscoverResult means modern, a recognized modern error means modern-but-unsupported-version, and any other error or a timeout means legacy. On Streamable HTTP, the client attempts a modern request and inspects the body of a 400 Bad Request: a recognized modern JSON-RPC error means modern, anything else means legacy.
Does an HTTP 401 or 403 during a compatibility probe mean the server is legacy?
No. The specification's backward-compatibility mechanism is scoped to inspecting the body of a 400 response (HTTP) or the server/discover outcome (stdio). 401 and 403 are authorization outcomes defined separately in the authorization specification, not era signals — treat them as a credential or scope problem to investigate.
Where does serverInfo live in the modern era?
In result _meta, under the key io.modelcontextprotocol/serverInfo — not as a top-level field on the DiscoverResult body. A client that still looks for a top-level serverInfo field will not find it on a conforming modern server.
Building something that has to hold up?
We do this work for teams — agent reliability hardening, custom MCP servers, and full-stack AI systems built to survive production.