Key takeaways
- MCP 2026-07-28 removes the initialize handshake and sessions. Every request carries its own version and capabilities.
- All four Tier 1 SDKs shipped stable support for the new revision on or around 28 July 2026.
- Client support is uneven, so remote servers should keep answering 2025-11-25 clients for now.
- Roots, Sampling, Logging and Dynamic Client Registration are deprecated. Removal can come no earlier than July 2027.
- Treat requestState as attacker-controlled input and protect it with an HMAC or AEAD.
The 2026-07-28 revision of the Model Context Protocol, stable since 28 July 2026, turns MCP into a request/response protocol. The initialize handshake and the Mcp-Session-Id header are gone. Every request carries its own protocol version and client capabilities, so any request can land on any server instance behind an ordinary load balancer.1 For most server authors the migration is an SDK upgrade and a handful of renamed APIs.
The part that needs care is the client side. As of 8 October 2026, widely used clients still disagree about which revision they speak, and a remote server that accepts only the new revision will lose some of its users. Two months after release, all four Tier 1 SDKs have shipped several stable versions, Claude Code has made the new revision its default, and Cursor’s staff have said there is no timeline for it.
The sections below cover what changed, where state lives now, which SDKs and clients are ready, how the version fallback fails in practice, and the order we would migrate a server in.
What the 2026-07-28 revision removes and adds
The official changelog lists nine major changes.2 These are the ones that touch most code:
initializeandnotifications/initializedare removed, so there is no handshake. Each request carriesio.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilitiesin_meta, and a version mismatch returnsUnsupportedProtocolVersionError.- The
Mcp-Session-Idheader is removed, and list endpoints no longer vary per connection. - Servers must implement a new
server/discovermethod that advertises their versions, capabilities and identity. Clients may call it first or skip it. - Server-to-client requests use Multi Round-Trip Requests (MRTR). A server that needs elicitation, sampling or roots no longer sends its own request down an open stream. It returns a result with
resultType: "input_required", and the client retries the original call with the answers. subscriptions/listenreplaces the HTTP GET stream andresources/subscribe. Clients opt in to the change notifications they want.Last-Event-IDredelivery is gone. If a response stream breaks, the in-flight request is lost and the client re-issues it with a new request ID.ping,logging/setLevelandnotifications/roots/list_changedare removed. Log level is now set per request in_meta.- Long-running tasks leave the core for the
io.modelcontextprotocol/tasksextension, which polls withtasks/getinstead of blocking ontasks/result.
| Concern | 2025-11-25 | 2026-07-28 |
|---|---|---|
| Connection setup | initialize handshake and a session ID |
Metadata on every request; optional server/discover |
| Server needs input from the user | Server-initiated elicitation/create on an open stream |
InputRequiredResult, then a client retry with inputResponses |
| Change notifications | HTTP GET stream and resources/subscribe |
subscriptions/listen stream, per notification type |
| Long-running work | Experimental tasks in core, blocking tasks/result |
Tasks extension, polling tasks/get |
| Routing in gateways | Parse the JSON-RPC body | Mcp-Method and Mcp-Name headers |
| Resource not found error | -32002 |
-32602 |
The small changes catch people out as well. Besides the resource-not-found code, the protocol errors introduced in this revision were renumbered into a reserved range before release, so HeaderMismatch is -32020 and UnsupportedProtocolVersion is -32022.2 Clients that match error codes literally need updating. URL-mode elicitation also lost its notifications/elicitation/complete message, because under MRTR the client learns the outcome by retrying.
Where state goes when sessions disappear
Before this revision, a server could keep per-session state against the session ID and rely on sticky routing to send the client back to the same instance. That option is gone, and the spec offers three replacements.
The first is an explicit handle. A tool that starts something returns an identifier minted by the server, such as a cart or job ID, and later tools take it as an ordinary argument (SEP-2567).2 The model carries the handle between calls. Any instance can serve the next call, provided the state behind the handle lives in a store every instance can read.
The second is requestState inside MRTR. When a tool needs a confirmation or a missing parameter in the middle of a call, the server returns an interim result and packs whatever it needs into an opaque string that the client echoes back.3 A confirmation step looks like this:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"resultType": "input_required",
"inputRequests": {
"confirm_delete": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Delete branch release-2.3?",
"requestedSchema": {
"type": "object",
"properties": { "confirm": { "type": "boolean" } },
"required": ["confirm"]
}
}
}
},
"requestState": "<signed, opaque to the client>"
}
}
The client asks the user, then sends tools/call again with a new JSON-RPC ID, the same arguments, an inputResponses map keyed by confirm_delete, and the exact requestState it received. The retry is a fresh request, so whichever instance receives it has everything it needs.
The spec is blunt about the risk. Because requestState passes through the client, servers must treat it as attacker-controlled. If it influences authorisation, resource access or business logic, the server must protect its integrity with an HMAC or AEAD and reject anything that fails verification.3 It should also bind the state to the authenticated principal, give it a short expiry, and tie it to the originating request. Those checks limit replay but do not make the state single-use, so a one-time action still needs a record on the server. A minimal signed version, written for this post:
import base64, hashlib, hmac, json, time
def _digest(args: dict) -> str:
return hashlib.sha256(json.dumps(args, sort_keys=True).encode()).hexdigest()
def seal(state: dict, principal: str, method: str, args: dict, key: bytes, ttl_s: int = 300) -> str:
body = {**state, "sub": principal, "m": method, "d": _digest(args), "exp": int(time.time()) + ttl_s}
raw = json.dumps(body, sort_keys=True).encode()
tag = hmac.new(key, raw, hashlib.sha256).digest()
return base64.urlsafe_b64encode(raw).decode() + "." + base64.urlsafe_b64encode(tag).decode()
def unseal(token: str, principal: str, method: str, args: dict, key: bytes) -> dict:
raw_b64, tag_b64 = token.split(".")
raw = base64.urlsafe_b64decode(raw_b64)
expected = hmac.new(key, raw, hashlib.sha256).digest()
if not hmac.compare_digest(expected, base64.urlsafe_b64decode(tag_b64)):
raise PermissionError("requestState failed verification")
body = json.loads(raw)
if (body["sub"], body["m"], body["d"]) != (principal, method, _digest(args)) or body["exp"] < time.time():
raise PermissionError("requestState belongs to another caller or request, or has expired")
return body
This version is signed but readable by the client. If the state itself is sensitive, encrypt it with an AEAD instead.
The third replacement is the Tasks extension for long-running work. A server can hand back a task handle without the client opting in per request, and the client polls with tasks/get and sends input with tasks/update.2 Google’s engineers recommend this for long-running tools on Cloud Run and Cloud Functions, where any container can answer any poll.4
SDK status two months in
The MCP project announced SDK betas on 29 June, a month before the spec went final, and all four Tier 1 SDKs (TypeScript, Python, Go and C#) supported the revision on release day.51 By early October each had shipped further stable releases. The table is as of 8 October 2026.
| SDK | First stable release with 2026-07-28 | Latest release | Notes |
|---|---|---|---|
Python, mcp |
2.0.0, 28 July 2026 | 2.3.0, 2 October 2026 | FastMCP is renamed MCPServer; one endpoint serves both revisions |
TypeScript, @modelcontextprotocol/server |
2.0.0, published 27 July 2026 (UTC) | 2.3.1, 5 October 2026 | Separate client and server packages; a codemod handles the renames |
| Go | v1.7.0, 28 July 2026 | v1.8.0, 14 September 2026 | Serves 2026-07-28 only with StreamableHTTPOptions.Stateless = true |
| C# | 2.0.0, 28 July 2026 | 2.2.0, 13 August 2026 | Stateless now defaults to true; roots, sampling and logging marked obsolete |
The dates come from PyPI, the GitHub release page of each SDK and the beta announcement.67895
Three notes from the beta announcement still apply.5 In TypeScript, upgrading to v2 and switching on the new revision are separate steps, so a team can take the API changes first; the beta notes give the codemod as npx @modelcontextprotocol/codemod@beta v1-to-v2 .. Library authors who depend on the Python SDK were told to set an upper bound such as mcp>=1.27,<2 until they had tested v2. And the 1.x lines are still maintained: TypeScript promised v1.x fixes for at least six months after v2, and Python 1.30.0 shipped on 7 September.6
Which clients speak the new revision
Server authors care less about SDK dates than about what connects to them. This is what we could verify as of 8 October 2026:
| Client | Revision it negotiates | Source |
|---|---|---|
| Claude Code | 2026-07-28 by default with direct HTTP servers (all install types since 2.1.274, 17 September) and with local stdio servers (since 2.1.292, 6 October) | Claude Code changelog |
| Other Claude products | Support “being rolled out”, no dates given | Anthropic, 28 July |
| Cursor | 2025-11-25 and earlier; “No timeline at the moment” for 2026-07-28 | Cursor forum, 21 and 25 September |
| Cloudflare Agents SDK | Both; its /mcp endpoint accepts the new protocol and stateless requests from 2025 Streamable HTTP clients |
Cloudflare, 6 August |
| ChatGPT | We found no OpenAI documentation naming a revision | Test it directly |
The Claude Code changelog shows what a careful client migration looks like.10 Version 2.1.265 (8 September) added the fallback to the legacy HTTP+SSE transport that the spec describes. Version 2.1.281 (23 September) added URL-mode elicitation on 2026-07-28 connections. Version 2.1.292 switched local stdio servers to the new negotiation, and it remembers servers that ignore the newer protocol check for seven days after one slow connect, then connects to them the older way. Both defaults can be switched off with documented settings. Anthropic’s own post from release day says only that support “is being rolled out across Claude products”.11
On 21 September, a user reported that Cursor 3.21.16 could not connect to servers that accept only 2026-07-28. A Cursor staff member replied the same day that Cursor “currently speaks the 2025-11-25 revision and earlier”, that its SSE fallback misreads the 400 response, and that there is “no client-side workaround right now”. The advice was to keep a legacy path enabled on the server.12
One source disagreement is worth flagging. Google’s 5 August post on the stateless update still calls the spec a release candidate, while the MCP project’s GitHub release marks 2026-07-28 stable from 28 July.413 We treat the project’s own release as the record.
How the version fallback works, and how it fails
The spec defines how a client that supports both eras finds out which one a server speaks.14 It sends a modern request first. If the server answers HTTP 400, the client reads the body before deciding. A recognised modern JSON-RPC error, such as UnsupportedProtocolVersionError with a list of supported versions, means the server is modern and the client should retry with one of those versions. An empty body, or one the client doesn’t recognise, means a legacy server, and the client falls back to initialize for the rest of the connection.
A server that supports only 2026-07-28 should answer GET and DELETE on the MCP endpoint with 405, ignore any Mcp-Session-Id header, and ignore Last-Event-ID.14
The Cursor report shows how this goes wrong. The server rejected Cursor’s legacy request with a modern -32020 error. Cursor misread the 400 and tried the old SSE transport, the server answered 405, and the connection ended with no tools.12 Cursor’s staff called the misread a client limitation. Until it changes, the fix in a server author’s hands is to keep answering initialize until your own logs show no legacy clients. The Python and TypeScript v2 SDKs serve both revisions from one endpoint, and Cloudflare’s endpoint does the same, so the legacy path costs little to keep.515
Gateways can route on headers now
Every Streamable HTTP POST must carry MCP-Protocol-Version and Mcp-Method. Calls to tools/call, resources/read and prompts/get must also carry Mcp-Name, set to the tool name or resource URI.14 A tool can mark parameters with x-mcp-header, and clients must then copy those values into Mcp-Param-{Name} headers. Values that are not plain ASCII travel Base64-encoded inside a =?base64?...?= wrapper.
The point is that a gateway, rate limiter or WAF can apply per-tool policy without parsing JSON-RPC. InfoQ’s write-up cites Cloudflare’s Matt Carey making this case, and notes that teams can reuse the controls they already apply to other APIs.16 The spec also closes the obvious gap. Any server that processes the body must check that the headers match it, and reject mismatches with HTTP 400 and error -32020. Intermediaries that enforce policy on these headers should reject requests from protocol versions that predate header validation instead of trusting the headers.14
List results changed in a way that helps cost. Results from tools/list, prompts/list, resources/list, resources/read and resources/templates/list must carry ttlMs and cacheScope, and servers should return tools in a deterministic order.2 The maintainers say a stable order keeps upstream prompt caches stable across reconnects.1 If your server builds its tool list from a hash map or an unordered database query, sort it.
Authorisation changes worth acting on
The revision tightens OAuth in four places.2
- Authorisation servers should return the
issparameter defined in RFC 9207, and clients must check a presentissagainst the issuer they recorded before redeeming the code. The maintainers describe this as closing an authorisation-server mix-up gap.1 - Clients must key stored credentials by issuer, must not reuse them with a different authorisation server, and must re-register when the server changes.
- Clients must set
application_typeduring Dynamic Client Registration, so servers stop rejectinglocalhostredirects from desktop and CLI apps. - Dynamic Client Registration is deprecated in favour of Client ID Metadata Documents. It keeps working for authorisation servers that don’t support the new documents.
The DNS-rebinding rules still apply: servers must validate the Origin header on every connection and should bind to 127.0.0.1 when running locally.14 The protocol does not stop a server from changing what its tools say after you approved it, so pin tool definitions in the client and ask for approval again when they change. For the OAuth side of serving several clients from one server, see making one MCP server work in Claude, ChatGPT, Cursor and VS Code.
The deprecation clock
The revision adds a formal lifecycle policy. A deprecated feature stays in the spec for at least twelve months, and a public registry tracks each one.17
| Feature | Deprecated in | Migration path | Earliest removal |
|---|---|---|---|
| Roots | 2026-07-28 | Pass paths via tool parameters, resource URIs or server configuration | First revision released on or after 28 July 2027 |
| Sampling | 2026-07-28 | Call the model provider’s API directly | Same |
| Logging | 2026-07-28 | Log to stderr on stdio; use OpenTelemetry | Same |
| Dynamic Client Registration | 2026-07-28 | Client ID Metadata Documents | Same |
| HTTP+SSE transport | 2025-03-26 | Streamable HTTP | Three months after SEP-2596 reaches Final |
Sampling is the one with design consequences. A server that used sampling to borrow the client’s model for summarising or classifying now needs its own model access, its own key management and its own line in the cloud bill. Roots and Logging are mostly mechanical. The revision also documents the OpenTelemetry trace context keys (traceparent, tracestate and baggage) for _meta, which gives Logging users a ready replacement.2
Where A2A fits, now it shares a foundation with MCP
On 27 August 2026 the Agent2Agent (A2A) protocol was accepted as a Growth Stage project at the Agentic AI Foundation, the Linux Foundation body that also hosts MCP.18 The A2A project says its v1.0 specification is stable, that more than 150 organisations back it, and that Google Cloud, AWS Bedrock AgentCore Runtime and Microsoft Azure AI Foundry support it natively. It positions A2A for collaboration between agents and leaves an agent’s access to tools and data to MCP. If your product exposes tools for other people’s agents to call, MCP is still the interface to build. A2A becomes relevant when another team’s agent needs to hand your agent a task and track it to completion.
A migration order that keeps old clients working
This is the order we would follow for a remote MCP server:
- Upgrade to the v2 SDK with the new revision switched off, and run your existing tests. In TypeScript, run the codemod first.
- Find everything that depends on a session: per-session caches, session-keyed authorisation, anything that reads
Mcp-Session-Id. Replace each with a handle passed as a tool argument, or withrequestState. - Move elicitation to MRTR, and seal
requestStatewith an HMAC or AEAD bound to the principal, the request and a short expiry. - Sort
tools/listdeterministically and setttlMsvalues that match how often your tools change. - Enable 2026-07-28 alongside 2025-11-25 on the same endpoint. Keep answering
initialize. - Test with each client your users run, including at least one that speaks only 2025-11-25.
- Move per-tool policy into your gateway using
Mcp-MethodandMcp-Name, and confirm the server rejects header and body mismatches. - Schedule the Sampling, Roots and Logging replacements before July 2027, starting with Sampling if you use it.
- Review legacy support each quarter against your own connection logs, and switch it off only when those logs say you can.
-
Model Context Protocol blog, “The 2026-07-28 Specification”, 28 July 2026, https://blog.modelcontextprotocol.io/posts/2026-07-28/ ↩↩↩↩
-
MCP specification 2026-07-28, “Key Changes”, https://modelcontextprotocol.io/specification/2026-07-28/changelog ↩↩↩↩↩↩↩
-
MCP specification 2026-07-28, “Multi Round-Trip Requests”, https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr ↩↩
-
Google Developers Blog, “Scaling AI Agent Infrastructure with the MCP Stateless updates”, 5 August 2026, https://developers.googleblog.com/scaling-ai-agent-infrastructure-with-the-mcp-stateless-updates/ ↩↩
-
Model Context Protocol blog, SDK beta announcement for 2026-07-28, 29 June 2026, https://blog.modelcontextprotocol.io/posts/sdk-betas-2026-07-28/ ↩↩↩↩
-
PyPI, release history of the
mcppackage, https://pypi.org/project/mcp/#history ↩↩ -
TypeScript SDK releases on GitHub, https://github.com/modelcontextprotocol/typescript-sdk/releases ↩
-
Go SDK releases on GitHub, https://github.com/modelcontextprotocol/go-sdk/releases ↩
-
C# SDK releases on GitHub, https://github.com/modelcontextprotocol/csharp-sdk/releases ↩
-
Claude Code changelog, entries for versions 2.1.265, 2.1.274, 2.1.281 and 2.1.292, https://code.claude.com/docs/en/changelog ↩
-
Anthropic, “MCP 2026-07-28 spec: stateless core, coming to Claude”, 28 July 2026, https://claude.com/blog/bringing-mcp-2026-07-28-to-claude ↩
-
Cursor community forum, “MCP client cannot connect to modern-only 2026-07-28 Streamable HTTP servers”, 21 to 25 September 2026, https://forum.cursor.com/t/mcp-client-cannot-connect-to-modern-only-2026-07-28-streamable-http-servers-legacy-initialize-rejected/172536 ↩↩
-
Model Context Protocol specification releases on GitHub, https://github.com/modelcontextprotocol/modelcontextprotocol/releases ↩
-
MCP specification 2026-07-28, “Streamable HTTP”, https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http ↩↩↩↩↩
-
Cloudflare blog, “The next generation of MCP”, 6 August 2026, https://blog.cloudflare.com/mcp-v2/ ↩
-
InfoQ, “MCP Goes Stateless, and Developers Ask Whether That Just Makes it an API Again”, 12 August 2026, https://www.infoq.com/news/2026/08/mcp-stateless-gateway/ ↩
-
MCP specification 2026-07-28, “Deprecated Features”, https://modelcontextprotocol.io/specification/2026-07-28/deprecated ↩
-
A2A Protocol blog, “A New Chapter for A2A: Joining the Agentic AI Foundation”, 27 August 2026, https://a2a-protocol.org/latest/blog/2026/08/27/a-new-chapter-for-a2a-joining-the-agentic-ai-foundation/ ↩
Frequently asked questions
What changed in the MCP 2026-07-28 specification?
It removes protocol-level sessions and the initialize handshake. Every request carries its protocol version and client capabilities in _meta, servers must implement server/discover, and server-to-client requests such as elicitation now use Multi Round-Trip Requests.
Is MCP 2026-07-28 backwards compatible with 2025-11-25?
The wire format changed, but the spec defines a fallback. A client sends a modern request first and falls back to initialize if the server returns HTTP 400 without a recognised modern error. The Python and TypeScript v2 SDKs can serve both revisions from one endpoint.
Which MCP clients support the 2026-07-28 revision?
As of 8 October 2026, Claude Code negotiates it by default for direct HTTP servers and, since version 2.1.292, for local stdio servers. Cursor staff said on 21 September 2026 that Cursor speaks 2025-11-25 and earlier, with no timeline for the new revision.
Are MCP sampling and roots removed?
They are deprecated and still work. Under the new lifecycle policy they can be removed no earlier than the first spec revision released on or after 28 July 2027.
How do I keep state across tool calls in stateless MCP?
Return an explicit handle, such as a job or cart ID, from the tool that creates the state and have the model pass it back as an argument. For a mid-call question to the user, put the state in requestState and protect its integrity.
Building something like this?
9io is a small team of senior engineers with a fractional CTO, and we work by the hour. Send us a note about your product. The reply comes from the person who'd do the work.