# 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.

- Source: /learn/mcp-2026-07-28-migration
- Updated: 2026-11-01
- Reading time: 7 minutes
- Tags: 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](https://modelcontextprotocol.io/specification/2026-07-28/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](https://modelcontextprotocol.io/specification/2026-07-28/deprecated) 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](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning) 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](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) 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

1. **Find out what you answer today.** Run the probes in the next section and write down the revisions your endpoint speaks.
2. **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 answers `initialize`.
3. **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](https://blog.modelcontextprotocol.io/posts/2026-07-28/) recommends this.
4. **Implement `server/discover`.** Return `supportedVersions` and `capabilities`, and put the name and version in `_meta["io.modelcontextprotocol/serverInfo"]`. `instructions` is optional.
5. **Validate every POST.** Compare `MCP-Protocol-Version` with the version in `_meta`, and `Mcp-Method` and `Mcp-Name` with the body. A mismatch is `400` with `-32020`. A missing required `_meta` field is `400` with `-32602`.
6. **Add `resultType`, `ttlMs` and `cacheScope`.** The [caching page](https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching) says `"public"` suits lists that are identical for every caller. Use `"private"` for results that vary by user.
7. **Retire the GET stream.** Answer GET and DELETE with `405`. [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html) §15.5.6 requires an `Allow` header on a 405. Move change notifications to `subscriptions/listen`.
8. **Update the server card.** List the revisions you serve in `supportedProtocolVersions`, which [SEP-2127](https://modelcontextprotocol.io/seps/2127-mcp-server-cards) defines. [MCP server cards](/learn/mcp-server-card) has the format.
9. **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](/learn/test-mcp-server-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`.**

```sh
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](https://modelcontextprotocol.io/specification/2026-07-28/server/discover)’s own example, trimmed. No live server is shown, because neither of the 2 MCP servers in the measured corpus speaks 2026-07-28.

```json
{"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](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning) 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
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](https://modelcontextprotocol.io/specification/2025-11-25/basic/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](/checks/mcp-server) 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:

```text
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/list` are not sent.
- The row is marked a defect only for an HTML page or a `200` with no JSON-RPC answer. A `4xx` error or a JSON-RPC error is not.
- A fail holds neither [level 4 nor level 5](/methodology#readiness-levels) through the MCP path. Level 4 stays open through a documented API: `openapi` and `docs`.

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](/tools/mcp-server-tester) runs this same check on a domain, and [scanning your site](/scan) shows the 3 requests and their status codes. [Tools, not pages](/learn/agent-tools-not-pages) covers what a tool declaration should contain.
