MCP 2026-07-28: What Changed and How to Migrate
MCP's 2026-07-28 revision drops sessions and the initialize handshake. What breaks, what replaces it, and the exact migration order for your server.
Short answer: on 28 July 2026 the Model Context Protocol shipped its largest revision since launch, and it made MCP a stateless protocol. The initialize handshake is gone. The Mcp-Session-Id header is gone. Every request now carries its own identity, and Streamable HTTP calls must include Mcp-Method and Mcp-Name headers. If you run a remote MCP server, you have real migration work. This guide covers what broke, what replaces it, and the order to fix it in.
As of the 2026-07-28 revision, MCP servers no longer hold sessions. Each request is self-describing: protocol version, client info, and capabilities travel in
_meta, and Streamable HTTP calls must includeMcp-MethodandMcp-Nameheaders. Any request can land on any instance behind a plain round-robin load balancer.
Why the protocol went stateless
The old design held a bidirectional stream open per client. That worked for a local stdio server. It fell apart the moment you put a server behind a load balancer.
You needed sticky sessions. You needed shared storage for session state. A pod restart dropped every connected client. Teams worked around it with routing hacks and Redis, and the protocol maintainers called this out directly: statelessness was the most requested change from people actually running MCP in production.
The new core removes the problem at the protocol level instead of asking you to solve it in infrastructure.
What changed, in one table
| Area | Before 2026-07-28 | From 2026-07-28 |
|---|---|---|
| Handshake | initialize / initialized |
Removed; optional server/discover RPC |
| Session | Mcp-Session-Id header |
Removed entirely |
| Routing | Gateway parses the JSON body | Mcp-Method + Mcp-Name headers |
| Server→client calls | Held-open stream | Multi Round-Trip Requests (MRTR) |
| List responses | Re-fetch every time | ttlMs + cacheScope hints |
| Client registration | DCR common | CIMD; DCR deprecated, still functional |
| Long-running work | Experimental core | io.modelcontextprotocol/tasks extension |
| Deprecated | — | Roots, Sampling, Logging, legacy HTTP+SSE |
1. No handshake, no session
This is the change that breaks most code. A request no longer belongs to a conversation. It stands alone.
Before, a client opened a session and every later call referenced it:
POST /mcp
Mcp-Session-Id: 3f2a9c1e-...
{"jsonrpc":"2.0","id":1,"method":"tools/call", ...}
Now the same call describes itself completely:
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search","arguments":{"q":"otters"},
"_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}}
Note what is not in there: no session header, no prior handshake, no server-side state required to interpret the call. That single property is what lets you put eight replicas behind round-robin and stop worrying about sticky routing.
What if my server needs state?
You still can. But the maintainers are explicit about the pattern: mint an explicit handle from a tool and have the model pass it back as an argument.
That is better than hidden transport state for two reasons. The handle is visible in the transcript, so the model can reason about it. And it survives independently of any connection, so a restart does not strand the user’s workflow.
2. MRTR replaces server-initiated requests
Sampling, elicitation, and roots all needed the server to call back into the client mid-request. Without a persistent stream, that has to be inverted.
Multi Round-Trip Requests do it in plain request/response. When a tool needs something from the user — a confirmation, a missing parameter — the server returns resultType: "input_required" together with the requests it needs answered. The client prompts, collects the answers, then retries the original call with those answers attached in inputResponses.
Two practical consequences:
- Your tool handler must become resumable. It cannot assume it runs to completion in one call.
- Your client must handle
input_requiredas a normal response, not as an error.
This is the part most migrations underestimate. A confirmation flow that used to be three lines of stream plumbing becomes explicit state you own.
3. List responses are now cacheable
tools/list, prompts/list, resources/list, and resources/read return ttlMs and cacheScope.
Why it matters beyond latency: stable tool catalogues keep upstream prompt caches warm. If your tool list reshuffles on every reconnect, you pay for cache misses on every turn. Deterministic ordering plus a TTL hint fixes that without any client-side cleverness.
If you serve a tool catalogue that rarely changes, emit a generous ttlMs. If your tools are user-scoped, scope the cache accordingly.
4. Authorization hardening
Authorization is where implementers spend most of their integration time, and this release tightens several known holes.
- RFC 9207 issuer validation. Authorization servers should return
iss, and clients must validate it before redeeming a code. This closes an authorization-server mix-up attack. - CIMD replaces DCR. Dynamic Client Registration is formally deprecated in favour of Client ID Metadata Documents. DCR still works today and will be removed in a future version.
- Issuer-bound credentials. Client credentials are bound to the issuer that minted them and cannot be reused across authorization servers.
- Desktop and CLI redirects. Clients now set
application_typeduring registration, which stops authorization servers rejectinglocalhostredirects.
If you have been putting off the auth cleanup, this revision is the deadline.
5. Tasks moved out of core
Long-running work is now the io.modelcontextprotocol/tasks extension rather than an experimental part of the core. You poll with tasks/get, update with tasks/update, and receive change notifications through a single subscriptions/listen stream that clients opt into per notification type.
The old HTTP GET notification endpoint is gone. If you built against it, that is a hard break, not a deprecation.
6. What got deprecated
Roots, Sampling, and Logging are deprecated, with a twelve-month minimum window. They still work. New implementations should not adopt them. The legacy HTTP+SSE transport is also officially deprecated with a year-long off-ramp.
That twelve-month floor is itself a change worth noting: MCP now has a formal deprecation policy, so you can plan upgrades instead of reacting to them.
Migration checklist, in order
- Bump the version string. Send
MCP-Protocol-Version: 2026-07-28. Decide now whether you will dual-serve old clients. - Delete the handshake. Remove
initialize/initializedhandling. Addserver/discoveronly if your clients genuinely need capability discovery up front. - Rip out session storage. Delete the session table, the Redis keys, and the sticky-session rule in your load balancer.
- Add the routing headers.
Mcp-MethodandMcp-Nameon every Streamable HTTP request. Wire your gateway and rate limiter to them. - Rewrite server→client flows as MRTR. Start with elicitation, because it is the one users notice when it breaks.
- Emit cache hints. Add
ttlMsandcacheScopeto list and read responses, and make ordering deterministic. - Audit auth. Validate
iss, setapplication_type, and start planning the move from DCR to CIMD. - Load test behind a round-robin balancer. The whole point is that any request can hit any instance. Prove it, including mid-workflow restarts.
Then write regression tests before you ship. A stateless protocol fails differently from a stateful one: the bug is usually a handler that silently depended on something the session used to carry. We wrote a separate guide on building an eval harness that runs in CI for exactly this class of regression.
Should you migrate now?
If your server is remote and runs more than one instance, yes. The session removal is the fix for a problem you are currently paying for in infrastructure.
If you only ship a local stdio server, the urgency is lower. Stdio has no transport session to speak of, so most of this revision costs you nothing. Still schedule it: SDKs have moved, and staying two revisions behind compounds.
Either way, pin the version you speak and write it down. The most common production incident here is not a protocol bug. It is a client and server silently disagreeing about which revision they are on.
Frequently asked questions
Do I still need server/discover?
Only if a client wants capabilities before making its first real call. It is optional by design. Most clients can simply call a tool and read the error if it is unsupported.
Can I support both the old and new revisions at once?
Yes, by routing on MCP-Protocol-Version. In practice the cost is real: two code paths, two test suites. Most teams set a cutover date and run the old path read-only until it retires.
Is Mcp-Session-Id still accepted?
No. It was retired with the handshake. A server that requires it will not work with 2026-07-28 clients.
What replaces Sampling?
MRTR covers the case where a server needs the client or user to produce something mid-call. Sampling is deprecated, not deleted, so nothing breaks today.
How long do I have before deprecations bite?
Twelve months minimum, by policy. Roots, Sampling, Logging, and HTTP+SSE all fall under it.
Does going stateless mean my server cannot remember anything?
No. It means the transport carries no state. Application state is fine — store it explicitly and hand the model a handle.
Sources
Protocol specification and release notes, modelcontextprotocol.io, revision 2026-07-28. Release announcement published 28 July 2026, checked 22 October 2026. SDK availability and migration notes for TypeScript, Python, Go, and C# live in the official repositories; the Rust SDK supports this revision in beta.
Last updated 22 October 2026. Protocol revisions move fast — verify against the specification before you ship.