Learn
Migrating an MCP server to 2026-07-28: no initialize, no sessions, server/discover
MCP 2026-07-28 drops initialize and Mcp-Session-Id and requires server/discover. A checklist, curl tests for both revisions, and how this scanner scores it.
7 min read 1402 words
- MCP
- 2026-07-28
- server/discover
- Streamable HTTP
- migration
MCP revision 2026-07-28 removes the initialize handshake and the Mcp-Session-Id header, and requires every server to answer server/discover. A server may speak both revisions on one endpoint, and the specification names no date by which it must stop answering initialize. This scanner’s MCP check still opens with initialize, so it records a server that speaks only 2026-07-28 as a failed handshake.
What changed for a server owner in MCP 2026-07-28?
The changelog lists 9 major changes. This table keeps the ones that change what a server accepts or sends.
| Change in 2026-07-28 | What the server does |
|---|---|
initialize and notifications/initialized are gone |
Reads the version, client info and capabilities from each request’s _meta |
Mcp-Session-Id and protocol-level sessions are gone |
Keeps no state per session; passes server-minted handles as tool arguments |
server/discover is required |
Returns supportedVersions, capabilities and its own serverInfo |
Mcp-Method is required on every request, and Mcp-Name on tools/call, resources/read and prompts/get |
Rejects a header that disagrees with the body: 400 and error -32020 |
| A request names a version the server lacks | Answers 400 and error -32022, with the supported versions in data.supported |
Results carry resultType, and list and read results carry ttlMs and cacheScope |
Adds "complete", a freshness time in milliseconds, and "public" or "private" |
The GET stream and Last-Event-ID resumption are gone |
Answers GET and DELETE with 405, and ignores Last-Event-ID |
| Sampling, elicitation and roots requests become multi round-trip requests | Returns an InputRequiredResult instead of sending a request on a stream |
Roots, Sampling, Logging and Dynamic Client Registration are deprecated in the same revision. The deprecated-features registry dates their earliest removal to the first revision released on or after 2027-07-28. initialize and sessions are not in that registry. The revision removes them, and the next section says what that means for older clients.
Do you have to keep answering initialize?
The specification lets you choose. Its versioning page names three kinds of implementation: modern (2026-07-28 and later), legacy (initialize, 2025-11-25 and earlier) and dual-era. A dual-era server picks its behavior from how the client opens. A request with per-request _meta is served statelessly, and an initialize starts a legacy session.
The cost of dropping initialize falls on older clients. In the page’s compatibility matrix, a legacy client against a modern server fails, because the server rejects initialize and the client has no way forward. The page asks a modern-only server to name its supported versions in any error it returns to initialize. That message may be the only diagnostic a legacy client can show.
A client that speaks both revisions tries a modern request first. On a 400, the Streamable HTTP page has it read the body. A recognized modern error, such as -32022, means a modern server, and the client retries with a listed version. Anything else means a legacy server, and the client falls back to initialize.
Keep initialize working while any client you care about still opens with it. This scanner is one.
A migration checklist for a server owner
- Find out what you answer today. Run the probes in the next section and write down the revisions your endpoint speaks.
- Choose dual-era or modern-only. Dual-era keeps every client that opens with
initialize. Modern-only is simpler to run and drops them. The release post says the TypeScript, Python, Go and C# SDKs speak 2026-07-28, but not whether a server built on them also answersinitialize. - Remove per-session state. Anything keyed on
Mcp-Session-Id, such as a cart or a cursor, moves to a handle. Your server mints it in one call and takes it as an argument in the next. The release post recommends this. - Implement
server/discover. ReturnsupportedVersionsandcapabilities, and put the name and version in_meta["io.modelcontextprotocol/serverInfo"].instructionsis optional. - Validate every POST. Compare
MCP-Protocol-Versionwith the version in_meta, andMcp-MethodandMcp-Namewith the body. A mismatch is400with-32020. A missing required_metafield is400with-32602. - Add
resultType,ttlMsandcacheScope. The caching page says"public"suits lists that are identical for every caller. Use"private"for results that vary by user. - Retire the GET stream. Answer GET and DELETE with
405. RFC 9110 §15.5.6 requires anAllowheader on a 405. Move change notifications tosubscriptions/listen. - Update the server card. List the revisions you serve in
supportedProtocolVersions, which SEP-2127 defines. MCP server cards has the format. - Test, then scan. Re-run the probes. A modern-only server will read as a failed handshake in the scan, as the last section shows.
How to test an MCP server with curl on both revisions
Testing an MCP server with curl walks through the initialize handshake. Add these two probes for the new revision. Each POST carries the version twice, in the header and in _meta, plus an Mcp-Method header.
Probe 1: server/discover.
MCP=https://example.com/mcp
curl -sS -i -X POST "$MCP" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":"discover-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
A 2026-07-28 server answers 200 with the versions it speaks. This is the discovery page’s own example, trimmed. No live server is shown, because neither of the 2 MCP servers in the measured corpus speaks 2026-07-28.
{"jsonrpc":"2.0","id":"discover-1","result":{
"resultType":"complete",
"supportedVersions":["2026-07-28"],
"capabilities":{"tools":{},"resources":{}},
"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"ExampleServer","version":"1.0.0"}},
"ttlMs":3600000,"cacheScope":"public"}}
Probe 2: a version no server speaks. Repeat the request with 1900-01-01 in the header and in _meta. A modern server answers 400 and error -32022, and its data.supported lists the versions it speaks. The versioning page uses this same example.
| Answer to a modern request | What it means |
|---|---|
200 with supportedVersions |
A modern or dual-era server |
400 with -32022 or -32020, or 404 with -32601 |
A modern server: retry with a listed version, or fix the header |
Any other 4xx, with an empty body or another error |
A legacy server: open with initialize |
This site’s MCP server speaks 2025-11-25 and 2025-06-18 only. On 31 October 2026 (UTC), a modern tools/list sent to https://agenticwebscore.com/mcp got this answer:
HTTP/2 400
content-type: application/json; charset=utf-8
cache-control: no-store
{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Unsupported MCP-Protocol-Version. This server speaks 2025-11-25, 2025-06-18."}}
Error -32600 is not a modern error, so a client that speaks both revisions falls back to initialize. The same server answers an initialize that offers 2026-07-28 with protocolVersion 2025-11-25. The 2025-11-25 lifecycle has a server do this for a version it lacks.
What does this scanner do with a server that speaks only 2026-07-28?
The MCP server check speaks 2025-11-25 and nothing else. It runs only when the scan has already seen an agent surface. A server card, an AI catalog entry or a homepage Link with the relation mcp each count. In a local test, a bare /mcp route got “Not scored: the scan found no API or agent surface”.
With an endpoint, the session is initialize, notifications/initialized and tools/list. The initialize request offers 2025-11-25. It carries no MCP-Protocol-Version header, no Mcp-Method header and no _meta. The scan never sends server/discover or any other 2026-07-28 request.
A server that speaks only 2026-07-28 rejects that request. A local test server that answered initialize with 400 and error -32020 produced this record:
The server card at /.well-known/mcp/server-card.json names http://127.0.0.1:51320/mcp, and the handshake fails: initialize got the JSON-RPC error -32020 "Header mismatch: Mcp-Method header is missing. Supported: 2026-07-28" (HTTP 400).
Local test servers that answered with -32022, with a bare 400 and with 404 and -32601 gave the same result. In each case:
- The row is
fail. The check is scored, weight 3 in the essential tier, and earns 0 points. - The session stops after that 1 request. The notification and
tools/listare not sent. - The row is marked a defect only for an HTML page or a
200with no JSON-RPC answer. A4xxerror or a JSON-RPC error is not. - A fail holds neither level 4 nor level 5 through the MCP path. Level 4 stays open through a documented API:
openapianddocs.
The record keeps the version the server chose. Both MCP records in the measured corpus name 2025-11-25. A scan record cannot tell a legacy-only server from a dual-era one, because the scan never sends a modern request.
This is how the check works today. This article does not promise a change to it. If you run a modern-only server, expect the row to read “Fails”, and judge the server with the probes above. If you want the scan to pass, keep initialize working next to the modern requests.
The MCP server tester runs this same check on a domain, and scanning your site shows the 3 requests and their status codes. Tools, not pages covers what a tool declaration should contain.