Research
Migrating an MCP server to 2026-07-28
The 2026-07-28 revision removes sessions and the initialize handshake. If you maintain a server, this is what to actually do about it.
Read this before you start
Do not migrate to the new revision. Support both.
The spec calls a server that does this dual-era, and it matters more than anything else on this page, because of one row in the compatibility matrix:
| Client | Server | Outcome |
|---|---|---|
| Legacy | Modern-only | 🔴 Fails. No fall-forward exists. |
| Legacy | Dual-era | 🟢 Works — served with legacy semantics |
| Modern | Dual-era | 🟢 Works — served statelessly |
| Modern | Modern-only | 🟢 Works |
A legacy client (anything on 2025-11-25 or earlier) has no mechanism to fall forward. It sends initialize, gets an error, and stops. It cannot discover that you speak a newer revision and retry.
So “migrating” your server in the ordinary sense — switching to the new revision — breaks every client that hasn’t moved yet. Today that is very nearly all of them.
The goal is not to be on the new revision. It is to speak both until the clients catch up.
How a dual-era server decides
You don’t detect anything or configure a mode. You answer according to how the client opens:
request carries modern _meta → serve statelessly (2026-07-28)
io.modelcontextprotocol/protocolVersion
request is `initialize` → serve legacy semantics
(session-scoped, as before)
Both can be served concurrently on the same endpoint or process. That’s the whole mechanism.
Server checklist
Required
- Implement
server/discover. Servers MUST implement it. Return supported protocol versions, capabilities, and identity in one response. - Accept per-request
_meta— readio.modelcontextprotocol/protocolVersion,io.modelcontextprotocol/clientCapabilities,io.modelcontextprotocol/clientInfooff each request rather than from session state. - Identify yourself per result —
io.modelcontextprotocol/serverInfoin each result’s_meta. - Return
UnsupportedProtocolVersionError(code-32022) for versions you don’t serve, listing what you do:{ "code": -32022, "message": "Unsupported protocol version", "data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" } } - Add
resultTypeto every result —"complete"for ordinary results. - Add
ttlMsandcacheScopetotools/list,prompts/list,resources/list,resources/read,resources/templates/list. - Accept
Mcp-MethodandMcp-Nameheaders on Streamable HTTP POSTs.
Keep working for legacy clients
- Keep answering
initializewith legacy semantics. This is the dual-era half people skip. - If you ever do go modern-only: name your supported versions in the error you return to
initialize. A legacy client can’t act on it, but it may be the only diagnostic a user ever sees.
Replace
| Remove | Replace with |
|---|---|
Mcp-Session-Id / session state |
Server-minted handles passed as tool arguments |
resources/subscribe / unsubscribe, HTTP GET stream |
subscriptions/listen (one long-lived POST stream) |
ping |
nothing — it’s gone |
logging/setLevel |
io.modelcontextprotocol/logLevel per request in _meta |
Server-initiated roots/list, sampling/createMessage, elicitation/create |
InputRequiredResult + client retry (the MRTR pattern) |
SSE resumability, Last-Event-ID |
nothing — re-issue the request with a new id |
notifications/roots/list_changed |
nothing |
Deprecated — don’t build new work on these (they stay for at least 12 months):
- Roots, Sampling, Logging
- HTTP+SSE transport → use Streamable HTTP
- OAuth Dynamic Client Registration → Client ID Metadata Documents
Security items worth doing while you’re in there
You’re touching the code anyway. These are the ones that actually change your exposure:
- Audit what your handles authorize. State moved from a transport header into tool arguments, which live in the model’s context next to untrusted text. Treat a handle as a bearer token the model is carrying: scope it narrowly, expire it, and don’t let one handle grant more than the call needs.
- Decide about
x-mcp-headerdeliberately. Tool parameters can now set HTTP headers. Before enabling it, answer: which headers can a tool argument set, and what happens if a malicious web page picks their values? Allow-list, never pass through. - Never declare a credential as a tool parameter. Still the most common real finding we see.
api_key,token,passwordbelong in the environment, not in a schema the model fills in. - If you use OAuth, adopt the hardening: validate
isson authorization responses (RFC 9207), key stored client credentials by issuer and never reuse them across authorization servers, and setapplication_typeat registration. - Watch schema composition.
$refandoneOfare allowed now. They also hide parameters from tools that read schemas flatly — including, today, ours. If you nest a credential-shaped parameter, a scanner may not see it, and neither will a reviewer skimming.
Verify it
Quick checks that catch most of it:
# 1. server/discover answers (it is mandatory)
curl -sS https://your-server.example/mcp \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}'
# 2. an unsupported version returns -32022 with a `supported` list
curl -sS https://your-server.example/mcp \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 1900-01-01' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover",
"_meta":{"io.modelcontextprotocol/protocolVersion":"1900-01-01"}}'
# 3. legacy clients still work — initialize must not 4xx on a dual-era server
curl -sS https://your-server.example/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-11-25","capabilities":{},
"clientInfo":{"name":"check","version":"1"}}}'
Check 3 is the one people forget, and it’s the one that breaks your users.
Order of work
- Add
server/discoverand per-request_metahandling alongside the existing handshake. - Add
resultType,ttlMs,cacheScope. - Move session state to explicit handles — and scope them.
- Replace subscriptions and server-initiated requests.
- Leave
initializein place. Remove it only when your clients have moved, which will be a while.
Nothing here is urgent in the “patch tonight” sense. The deprecation window is twelve months minimum, and the ecosystem is early. Of the 9,252 servers whose revision we have actually observed on a live connection, 125 answer with 2026-07-28:
| MCP revision | Servers | Share |
|---|---|---|
2026-07-28 (current) | 125 | 1.4% |
2025-11-25 | 5,347 | 57.8% |
2025-11-05 | 5 | 0.1% |
2025-06-18 | 1,247 | 13.5% |
2025-03-26 | 1,716 | 18.5% |
2024-11-05 | 812 | 8.8% |
Being deliberate beats being first — and right now, being first means being the only server your users’ clients cannot talk to.
We grade every server in the registry at mcpgrade.dev, and record which revision each one actually answers with — see what the revision changes for security.