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 — read io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities, io.modelcontextprotocol/clientInfo off each request rather than from session state.
  • Identify yourself per resultio.modelcontextprotocol/serverInfo in 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 resultType to every result — "complete" for ordinary results.
  • Add ttlMs and cacheScope to tools/list, prompts/list, resources/list, resources/read, resources/templates/list.
  • Accept Mcp-Method and Mcp-Name headers on Streamable HTTP POSTs.

Keep working for legacy clients

  • Keep answering initialize with 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-header deliberately. 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, password belong in the environment, not in a schema the model fills in.
  • If you use OAuth, adopt the hardening: validate iss on authorization responses (RFC 9207), key stored client credentials by issuer and never reuse them across authorization servers, and set application_type at registration.
  • Watch schema composition. $ref and oneOf are 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

  1. Add server/discover and per-request _meta handling alongside the existing handshake.
  2. Add resultType, ttlMs, cacheScope.
  3. Move session state to explicit handles — and scope them.
  4. Replace subscriptions and server-initiated requests.
  5. Leave initialize in 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 revisionServersShare
2026-07-28 (current)1251.4%
2025-11-255,34757.8%
2025-11-0550.1%
2025-06-181,24713.5%
2025-03-261,71618.5%
2024-11-058128.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.

← All research