{"slug": "mcp-2-0-the-release-that-deleted-the-handshake", "title": "MCP 2.0: the release that deleted the handshake", "summary": "The MCP working group released protocol revision 2026-07-28, described as the largest revision since launch, which removes the initialize handshake, session IDs, and held-open streams in favor of stateless per-request negotiation. Clients now send protocol version, method, and tool name as HTTP headers and carry client info in _meta on every request, while a mandatory server/discover method returns supported versions and capabilities in a single call. Existing deployments continue to work, but the connection model that defined MCP since its November 2024 launch is gone.", "body_md": "MCP versions are dates. `2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`, and now `2026-07-28`. From the outside they all look like the same kind of event.\n\nThe last one isn't. The working group called the release candidate \"the largest revision of the protocol since launch,\" which usually means a long changelog. Here it means the connection model is gone. No `initialize`. No session id. No held-open stream. If you'd asked me a year ago to describe MCP in one sentence, that sentence would be wrong now.\n\nNothing you have deployed today stops working, which is exactly why it's easy to skim past.\n\nEvery MCP revision up to and including `2025-11-25` opened the same way. The client sends `initialize` with its protocol version, its capabilities and its `clientInfo`. The server answers with the version it picked, its own capabilities and its `serverInfo`. The client sends `notifications/initialized`. Only then is the connection allowed to carry anything useful.\n\nThat exchange establishes a session, and everything after it is defined relative to that session. Over Streamable HTTP the server could mint an `Mcp-Session-Id`, which the client then echoed on every request; `DELETE` ended it, `Last-Event-ID` resumed a dropped SSE stream.\n\nThe design makes complete sense if you remember where MCP started. In November 2024 it was a thing you ran as a subprocess of your editor over stdio. One client, one server, one pipe, and the pipe's lifetime *is* the session. Negotiating capabilities once at the start costs nothing when there is exactly one of everything.\n\nStreamable HTTP arrived in `2025-03-26` and carried that model onto the network, where \"the connection has a memory\" stops being free.\n\n*Interactive version on [the original post](https://www.chiragaggarwal.tech/blog/mcp-2-0-the-release-that-deleted-the-handshake).*\n\nSo you pick one: sticky routing, or a shared session store. Same problem, different bill. Every deploy drops live sessions. A crashed replica takes conversations with it. And since the session id tells a gateway nothing about what the request wants, the gateway has to open the JSON body to find out whether it's metering a `tools/list` or a database drop.\n\n*Four revisions of adding things to a connection, then one that removed the connection.*\n\n**The handshake is gone.** No `initialize`, no `initialized`, no `Mcp-Session-Id`. Everything that used to be negotiated once now travels on every request, in `_meta`. The same tool call, before and after:\n\n**Before: 2 round trips**\n\n```\nPOST /mcp HTTP/1.1\nContent-Type: application/json\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\n \"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{},\n           \"clientInfo\":{\"name\":\"my-app\",\"version\":\"1.0\"}}}\n\nPOST /mcp HTTP/1.1\nMcp-Session-Id: 1868a90c-3a3f-4f5b\nContent-Type: application/json\n{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\n \"params\":{\"name\":\"search\",\"arguments\":{\"q\":\"otters\"}}}\n```\n\n**After: 1 request**\n\n```\nPOST /mcp HTTP/1.1\nMCP-Protocol-Version: 2026-07-28\nMcp-Method: tools/call\nMcp-Name: search\nContent-Type: application/json\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\n \"params\":{\"name\":\"search\",\"arguments\":{\"q\":\"otters\"},\n           \"_meta\":{\"io.modelcontextprotocol/clientInfo\":{\"name\":\"my-app\",\"version\":\"1.0\"}}}}\n```\n\nThe second one is cold. That client has never spoken to this server, nothing was set up first, and nothing is kept afterward. The three new headers are there so a gateway can route and meter it without opening the body.\n\nVersion negotiation stopped being a phase and became a field. The server accepts or rejects each request on its own, and if it can't speak the version it answers `UnsupportedProtocolVersionError` listing the versions it can, so the client retries. There's a `server/discover` method that returns supported versions and capabilities in one call, which servers **MUST** implement and clients don't have to call. Discovery is a convenience now, not a precondition.\n\n**Server-to-client calls stopped needing an open pipe.** That was the load-bearing dependency on sessions. A tool that needs to ask the user something used to do it over a stream held open for the length of the call, which pins the call to one process.\n\nUnder SEP-2322 the server returns an `InputRequiredResult` and hangs up. The client answers later with `inputResponses` and the `requestState` it was handed, and any instance can take it from there. SEP-2260 tightens things further: a server may only start a request while it's actively processing one from the client. Between them they replace `elicitation/create` and `sampling/createMessage`.\n\nState moved into the open. The spec is blunt about where it went:\n\nServers that need to carry state across calls can do what HTTP APIs have always done: mint an explicit handle (a `basket_id`, a `browser_id`) from a tool and have the model pass it back as an ordinary argument on later calls.\n\nThe spec's own phrasing is that this \"makes the state visible to the model rather than hidden away.\" A session was state the model never saw and could not recover once it was gone.\n\n**Infrastructure can read the traffic without parsing it.**\n\n| SEP | What it adds | What it buys | \n|---|---|---|\n| 2243 | `Mcp-Method` and`Mcp-Name` request headers | Routing, metering and WAF rules with no body inspection | \n| 2549 | `ttlMs` and`cacheScope` on list and read | Clients know how long a tool list is good for, and who may share it | \n| 414 | `traceparent` /`tracestate` /`baggage` in`_meta` | One trace across client, gateway and server, into any OTel backend | \n\nA server rejects any request where the headers and the body disagree, which is what makes the headers safe to trust for anything that never looks inside.\n\nThe protocol changes are the part you notice. The governance changes are the part that decides whether any of it survives the next revision, and there are three.\n\nExtensions became first class. They get reverse-DNS identifiers, negotiate through an `extensions` map, version independently of the spec, and live in their own `ext-*` repositories with their own maintainers. Tasks graduated out of the core and into `io.modelcontextprotocol/tasks`. MCP Apps, which is server-rendered HTML in a sandboxed iframe talking back over the same JSON-RPC, ships as one too. The core stays small and the interesting parts move at their own speed.\n\nFeatures now have a lifecycle: Active, Deprecated, Removed, with a documented migration path and twelve months minimum before anything deprecated is eligible for removal. Three features entered it immediately.\n\n| Deprecated | Use instead | \n|---|---|\n| Roots | Tool parameters, resource URIs, or server configuration | \n| Sampling | Your LLM provider's API, directly | \n| Logging | `stderr` on stdio, OpenTelemetry for anything structured | \n\nAll three still work. That's the point of writing the policy down.\n\nConformance became a gate. A Standards Track SEP can't reach Final until matching scenarios land in the conformance suite, and official SDKs are scored against that suite under a tier system. All four Tier 1 SDKs (TypeScript, Python, Go, C#) shipped `2026-07-28` support the day the spec did, with Rust in beta.\n\nThe remaining breaking changes are small but worth knowing. Tool schemas moved to full JSON Schema 2020-12, so `oneOf`, `$ref` and conditionals are legal now, and the spec says out loud that you must not auto-dereference external `$ref` URIs. `structuredContent` takes any JSON value instead of only objects. The missing-resource error went from MCP's custom `-32002` to the standard `-32602`, which breaks anyone matching on that literal. Every `2025-11-25` Tasks implementation needs migrating.\n\nI spent sixteen months building [Appwrite's MCP server](https://github.com/appwrite/mcp), and [most of that time went to authorization](https://www.chiragaggarwal.tech/blog/how-i-built-the-appwrite-mcp-server). When I got to the transport, the hosted server went out with one flag set:\n\n```\nStreamableHTTPSessionManager(app=server, json_response=False, stateless=True)\n```\n\nNo sessions. Every request carries a bearer token; verify it, build a client from it, serve the call, forget everything. I'd love to claim foresight here. The real reason is duller: OAuth had already made session state pointless, because the token identifies the user on every request, and I didn't want to run a Redis to hold something I had no use for.\n\nThe spec shipped on July 28. We merged `2026-07-28` support on July 29, in [PR #89](https://github.com/appwrite/mcp/pull/89), by moving from MCP Python SDK v1.28 to v2.0.0. The HTTP and OAuth surface didn't move at all. What made it a day rather than a quarter is that v2's session manager routes on the protocol version header, so one deployment serves both eras:\n\n``` php\ndef _is_modern_request(scope: Scope) -> bool:\n    \"\"\"Match the SDK's era-routing predicate: a present ``MCP-Protocol-Version``\n    that is not a handshake-era revision goes to the modern path.\"\"\"\n    version = _header(scope, b\"mcp-protocol-version\")\n    return version is not None and version not in HANDSHAKE_PROTOCOL_VERSIONS\n```\n\nHandshake versions, `2024-11-05` through `2025-11-25`, take the legacy path. Anything else takes the per-request one. Nobody's client broke.\n\nThree things did cost real work:\n\n`CallToolResult(isError=true)`. v2 turns an uncaught exception into a sanitized `-32603`, so the model never learns why the call failed. Our `confirm_write` refusals are only worth anything if the model can read them, so every handler now returns `is_error=True` explicitly.`Mcp-Method` and `Mcp-Name` ride on every modern request. If your allow-list doesn't name them, browser-based clients fail before they ever reach your auth code.`initialize`, so our handshake counter would have quietly flatlined exactly as clients migrated, which is the worst possible time to lose a graph. It's keyed off activity windows now, so the timeseries stays comparable through the transition.\nNone of that is protocol work. It's the ordinary cost of a dependency changing shape under you, and it only took a day because a decision made eighteen months earlier, for completely unrelated reasons, happened to line up.\n\nA `2026-07-28` MCP server takes an HTTP request, reads some headers, and returns a response. That's the whole shape of it. You can put it behind a CDN, cache its list endpoints, and restart it in the middle of someone's conversation without telling anyone.\n\nIt's a less interesting protocol than the one that launched in 2024. I think that's the point. The bidirectional stateful version was more fun to read about and considerably worse to operate, and operating it is what most of us actually do.\n\n| Term | What it means here | \n|---|---|\n| handshake era | Every MCP revision through `2025-11-25` , where a connection began with`initialize` and carried a session from then on. | \n| `Mcp-Session-Id` | The header that identified that session. Removed in `2026-07-28` . | \n| `_meta` | A metadata object on every request. Now carries protocol version, client info and capabilities, one copy per request. | \n| `server/discover` | A single call returning supported versions, capabilities and identity. Mandatory for servers, optional for clients. | \n| sticky routing | Making a load balancer send every request from one client to the same backend. The price of server-side session state. | \n| `requestState` | The opaque blob a server hands back with an `InputRequiredResult` so the follow-up can be processed by any instance. | \n| SEP | Specification Enhancement Proposal, MCP's change process. Standards Track SEPs now need conformance scenarios before reaching Final. | \n| Streamable HTTP | The HTTP transport introduced in `2025-03-26` , replacing the original HTTP+SSE one. | \n| CIMD | Client ID Metadata Documents, the replacement for Dynamic Client Registration in OAuth flows. | \n\n`2026-07-28`` server/discover``2025-11-25` changelog`https://mcp.appwrite.io/`, or `claude mcp add --transport http appwrite https://mcp.appwrite.io/`\n*Originally published on [chiragaggarwal.tech](https://www.chiragaggarwal.tech/blog/mcp-2-0-the-release-that-deleted-the-handshake).*", "url": "https://wpnews.pro/news/mcp-2-0-the-release-that-deleted-the-handshake", "canonical_source": "https://dev.to/chiragagg5k/mcp-20-the-release-that-deleted-the-handshake-18o6", "published_at": "2026-10-26 12:35:27+00:00", "updated_at": "2026-10-26 12:43:35.119997+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "developer-tools", "ai-tools"], "entities": ["Model Context Protocol", "MCP working group"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/mcp-2-0-the-release-that-deleted-the-handshake", "markdown": "https://wpnews.pro/news/mcp-2-0-the-release-that-deleted-the-handshake.md", "text": "https://wpnews.pro/news/mcp-2-0-the-release-that-deleted-the-handshake.txt", "jsonld": "https://wpnews.pro/news/mcp-2-0-the-release-that-deleted-the-handshake.jsonld"}}