The short version
- There's no
initializestep anymore. Every request carries its protocol version and client capabilities inparams._meta, mirrored in theMCP-Protocol-Versionheader. - Protocol sessions are gone, so anything you kept in a session (a selected project, half-finished work) needs an explicit handle that any replica can resolve.
- A server that needs input mid-call returns
resultType: "input_required". The client retries the same call withinputResponsesand the opaquerequestState. - A 400 isn't a sign you've hit an old server.
-32020,-32021and-32022are modern errors that need a fixed request, not a fallback to the old handshake. - Roots, Sampling, Logging and Dynamic Client Registration are deprecated, not removed. The registry gives an earliest eligibility, not a shutdown date.
What breaks, side by side#
Every row below is an assumption a server or client built for 2025-11-25 can reasonably hold. The middle column is how 2026-07-28 behaves instead. The last column is where to start looking in your code.
| You assumed | 2026-07-28 does this | Look at first |
|---|---|---|
| Initialize once, and the server remembers your capabilities | Version and client capabilities travel on every request | How each call builds _meta |
| The session ID carries what the user selected | Protocol sessions are removed | An explicit project or work handle, with access checks |
| The server sends its own approval request to the client | The input request comes back inside the original result | Retry with inputResponses and the opaque requestState |
| Any response means the operation finished | A required resultType separates finished from interim | Branch on complete and input_required |
| Any 400 means you reached an older server | Modern failures use 400 as well | The error code and the supported-version data |
A GET stream or Last-Event-ID recovers lost messages | Both are removed from the target Streamable HTTP | Response streams, subscriptions and operation recovery |
| Deprecated means it's already gone | Deprecated features stay in the spec during their window | The per-feature registry entry and its notice |
Context on every request#
Under 2025-11-25, a client sent initialize, got the server's capabilities back, sent notifications/initialized, and only then made ordinary calls. The server remembered what was agreed. Behind a load balancer that memory lived in one process, which is exactly where the trouble started.
In 2026-07-28 each request is self-contained. The version and the client's capabilities go in params._meta, and the HTTP headers mirror the body so a gateway can route without parsing JSON. A server has to reject a request whose headers and body disagree.
A tools/call under 2026-07-28
POST /mcp MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: publish_note { "jsonrpc": "2.0", "id": "request-a", "method": "tools/call", "params": { "name": "publish_note", "arguments": { "target": "team-board", "text": "Review the fixture" }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } } } } }
Three details catch people out:
clientInfois optional. Version and capabilities are required. A rejection for missing metadata points at the required pair, not at every field you've seen in an example.Mcp-Nameis required ontools/call,prompts/getandresources/read, not on every method. Header names are case-insensitive; the method and name values aren't.- Servers have to implement
server/discover, but clients don't have to call it before working. It answers on the first request, with no handshake in front of it.
Sessions and your state#
Deleting the session header doesn't move the state that was hiding behind it. If your server kept "the project this user selected" in connection memory, a request that now lands on a different replica has no idea what was selected. The request will still pass a schema check. It'll just do the wrong thing.
The fix is application design, not protocol plumbing: return an explicit handle when the user selects something, store it where every replica can read it, and check on each use that the caller owns it and that it hasn't expired. A request ID isn't that handle. It correlates a response with its request, and a retry gets a new one.
Approval without a callback#
Before, a server that needed the user to confirm something sent its own elicitation/create request to the client over the open connection and waited. With no sessions, there's no connection to wait on. The business rule doesn't go away, though: the server still mustn't add the note until someone says yes.
Now the server answers the original call with an interim result, and the client calls again with the answer:
Server: the call needs input
{
"resultType": "input_required",
"requestState": "OPAQUE_STATE_FROM_RESULT",
"inputRequests": {
"approval": { "method": "elicitation/create", "params": { "mode": "form", … } }
}
}
Client: same call, with the answer
"params": { "name": "publish_note", "arguments": { … unchanged … }, "requestState": "OPAQUE_STATE_FROM_RESULT", "inputResponses": { "approval": { "action": "accept", "content": { "confirm": true } } }, "_meta": { … version and capabilities, as before … } }
Echo requestState exactly as you got it. Don't decode it, change it or reuse it for another operation. And inputResponses sits next to arguments under params; it's not a new argument in the tool's schema.
The flip side is on the client: a response that arrived isn't necessarily a finished operation. Branch on resultType. When the result comes from an earlier server that doesn't send the field at all, the compatibility rule says treat it as complete, so don't flag it as malformed.
Reading a 400#
The tempting shortcut during a migration is "400 means old server, fall back to initialize". In 2026-07-28 that's wrong, because the new revision uses 400 for its own errors. Falling back won't fix a header that disagrees with its body.
| You see | It means | Do this |
|---|---|---|
HTTP 400, -32020 | A header is missing, malformed or disagrees with the body | Compare the exact headers with the body fields they mirror |
HTTP 400, -32021 | The call needs a client capability you didn't declare | Read data.requiredCapabilities |
HTTP 400, -32022 | The server doesn't support the version you asked for | Pick a version from error.data.supported that you also support |
HTTP 404, -32601 | Unknown method | Check what the server supports |
| HTTP 404, no MCP body | Wrong endpoint, or something in between answered | Destination and routing logs |
result.isError: true | The tool ran and failed | The tool result and your application's own evidence |
A timeout tells you nothing about whether a write happened. Don't infer "committed" from a status code, and don't infer "nothing happened" from silence.
What happened to SSE#
Server-sent events didn't disappear, which is worth saying because "delete all the streaming code" is a common misreading. Three different things changed in three different ways:
- Response streams stay. A server can still answer a POST with an SSE stream, and
subscriptions/listenhas its own SSE response stream. - The standalone GET stream and
Last-Event-IDresumability are removed from the target Streamable HTTP. Recovering work now means request-level and application-level outcomes, not replaying a header the server no longer reads. - The historical HTTP+SSE transport (the one before Streamable HTTP) is a separate compatibility path with its own endpoint discovery and its own transition rule. Changing a version string doesn't implement it.
In a migration inventory, "this client uses streaming" is too vague to act on. "This client opens a standalone GET stream" is a task.
Deprecated isn't removed#
Four features are deprecated in 2026-07-28 and are still in the spec. Two related methods were removed outright, which is easy to mix up with the deprecations.
| Feature | Status | Move toward | Earliest removal |
|---|---|---|---|
| Roots | Deprecated | Explicit tool arguments, resource URIs or server config | First Current revision on or after 2027-07-28 |
| Sampling | Deprecated | A direct provider integration, with its own tested policy | First Current revision on or after 2027-07-28 |
| Logging | Deprecated | stderr for stdio, or your observability stack | First Current revision on or after 2027-07-28 |
| Dynamic Client Registration | Deprecated, compatibility kept | Client ID Metadata Documents where supported | First Current revision on or after 2027-07-28 |
includeContext thisServer / allServers | Deprecated | Omit it, or use none | Follows Sampling |
| Historical HTTP+SSE | Under transition provisions | Streamable HTTP | Three months after SEP-2596 reaches Final |
Already removed: logging/setLevel and notifications/roots/list_changed. The log level is now set per request, and protocol log messages need that opt-in.
A staged rollout#
The protocol change is the easy part. The risk is in the work that's already running when you switch. A rollout that holds up has a record and a stop condition at each stage:
| Stage | Keep a record of | Stop if |
|---|---|---|
| Inventory | Real client builds, their transport and revision, and how you know | Any compatibility assumption is unknown |
| Baseline | What's accepted and refused today, and the exact effects | You can't reproduce current behavior |
| Conversion | Explicit handles, input retry, identity binding | A new call depends on old process memory |
| Candidate | Acceptance per client cohort, and the state it leaves | A result mismatch or an unauthorized effect |
| Pending work | How many old callbacks are live, and what happens to each | Old operations are unresolved |
| Rollback | The earlier reader recovers what the candidate wrote | A repeated effect, unreadable state or lost work |
| Approval | A named operator, a change window, a tested restore | No owner, or a restore nobody has run |
One thing worth remembering when it goes wrong: rolling back code isn't rolling back a transaction. If a note was committed before a bad response went out, the old adapter will find that note. Check what was written before you repeat an operation whose answer got lost.
Free · four printable pages
Keep this next to the diff.
The cheatsheet has the side-by-side table, the error codes, the deprecation clock and the rollout checklist, laid out to print and pin up. We'll email it to you.
Sources#
Everything above was checked against these pages on 10 September 2026. The spec moves; if something here disagrees with them, they win.
- Revision 2026-07-28 changelog
- 2025-11-25 lifecycle
- Versioning and compatibility
- Streamable HTTP binding
- Multi round-trip requests
- Deprecated features registry
- Feature lifecycle policy
Go further
Module 9 of MCP Production is this guide, with a lab.
Compatibility fixtures for both revisions, a session-to-handle conversion with regression tests, and a promotion gate that refuses a rollout until the evidence is there.