AI

MCP went stateless: what breaks when versions mix

By · Mon Oct 19 2026 · 10 min read · 0 views

View as a Web Story

AISoftware#ai agents#mcp#model context protocol#python#migration

MCP logo mark between Python and TypeScript logos showing a stateless handshake

The MCP 2026-07-28 migration is less frightening than the headlines suggest. The Model Context Protocol removed sessions and its connection handshake, and the official MCP blog says the release contains breaking changes. We tested all four client and server pairings with Python SDK 1.30.0 and SDK 2.3.0. Every pairing connected and returned a tool result. A new client fell back to the old handshake after one rejected request, and a new server still served old clients. Two things did change: an error code, and anything that relied on server-side session state.

What did MCP change on July 28, 2026?

The 2026-07-28 revision removes the connection handshake and protocol-level sessions, and it makes every request carry its own context. The MCP release candidate post names the final release date as July 28, 2026, after a release candidate locked on May 21.

MCP, the Model Context Protocol, is an open standard that lets AI applications call tools and read data from servers. The post lists these changes, each tied to a numbered proposal. A SEP is a numbered proposal that changes the MCP specification. Streamable HTTP is the MCP transport that carries requests and streamed replies over ordinary HTTP. Mcp-Session-Id is the HTTP header that used to tie a client to one server session. The release changes all three:

Change SEP What it means for you
initialize and initialized removed 2575 Version, client info and capabilities travel in _meta on each request
Mcp-Session-Id and sessions removed 2567 No session header, no sticky routing needed
Mcp-Method and Mcp-Name headers required 2243 Streamable HTTP servers reject requests where headers and body disagree
Server-initiated requests restricted 2260 Allowed only while the server handles a client request
Multi round-trip prompts 2322 Server returns InputRequiredResult, client retries with inputResponses
Missing-resource error code 2164 -32002 becomes -32602
Full JSON Schema 2020-12 for tools 2106 Do not auto-dereference external $ref URIs
Authorization iss check 2468 Clients must validate iss per RFC 9207

server/discover is the new MCP method that replaces the capability exchange. A client calls it to learn what a server offers, with no prior handshake. In our logs it was always the first request of a new-flow connection, and it carried the Mcp-Method header like every other request.

The practical effect is on infrastructure. A load balancer can now route on the Mcp-Method header alone. A gateway can allow tools/list and block tools/call without parsing the body. Rate limits, audit logs and web application firewall rules all get simpler, because the method is visible before the body is read.

Is the 2026-07-28 MCP release really breaking?

At the protocol level yes, and at the SDK level mostly no. The official post says the release has breaking changes. A foundation post, the Agentic AI Foundation's migration guide, says existing deployments keep working, thanks to a legacy option on its TypeScript handler. Both are right, and our test shows where the line sits.

The protocol removes the old flow, so a hand-written client or server that speaks only the old flow cannot talk to a hand-written one that speaks only the new flow. The SDKs bridge the gap. They detect which flow the other side speaks and fall back. If you use an official SDK on both ends, you mostly do not notice. If you wrote your own transport, you do.

How did we test the compatibility?

Our test setup ran two MCP servers and two clients on one Mac. We used Python 3.15.0, with mcp 1.30.0 in one virtual environment and mcp 2.3.0 in another. Each server exposed one tool that increments a counter and one resource template. A small ASGI wrapper logged every HTTP request: the verb, the status, the JSON-RPC method, whether a session header was present, and the Mcp-Method and protocol version headers.

SDK 1.30.0 supports protocol versions 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25. SDK 2.3.0 reports 2026-07-28 as its latest version. The two clients were ClientSession from 1.30.0 and Client from 2.3.0, whose mode argument defaults to auto.

What happened in each of the four pairings?

All four connected and returned a tool result. They differed in how many HTTP requests the connection took. The chart and table show what the server logs recorded.

Advertisement

Brand-colored chart of HTTP requests per MCP connection for the four client and server version pairings

Client Server HTTP requests Session header Result
1.30.0 1.30.0 6 Yes Works
1.30.0 2.3.0 6 Yes Works, old flow served
2.3.0 1.30.0 7 Yes, after fallback Works, first request rejected
2.3.0 2.3.0 3 No Works, new flow

The old flow made six requests: initialize, notifications/initialized, tools/call, tools/list, a DELETE to end the session and a GET to open the event stream. The new flow made three: server/discover, tools/call and tools/list. Every new-flow request carried an Mcp-Method header and the protocol version 2026-07-28, and none carried a session header.

The 2.3.0 client against the 1.30.0 server is the interesting case. The client opened with server/discover, and the old server answered HTTP 400. The client then fell back to initialize and finished normally. That costs one extra round trip per connection, and nothing else. Your old server's logs will show a 400 for every new client, which is noise worth knowing about before it pages anyone.

What does the code change look like in Python?

On the server, the official SDK 2.x class is MCPServer, and the HTTP app comes from streamable_http_app(). This is the exact server we ran for the 2026 flow.

import uvicorn
from mcp.server import MCPServer

mcp = MCPServer("demo")

@mcp.tool()
def bump() -> int:
    """Increment a server-side counter."""
    ...

uvicorn.run(mcp.streamable_http_app(), port=8812)

The old server used FastMCP from mcp.server.fastmcp on 1.30.0. In 2.3.0, importing it raises a ModuleNotFoundError that says FastMCP was renamed to MCPServer and links to a migration guide. So the class rename is the first real migration step, and it fails loudly rather than silently.

On the client, SDK 2.x uses Client, which accepts a URL and negotiates automatically:

from mcp.client import Client

async with Client("http://127.0.0.1:8812/mcp") as c:
    result = await c.call_tool("bump", {})

Pin the SDK major version in your lockfile before you try this. The 2.x line moved fast: on the day we checked, the npm registry listed @modelcontextprotocol/sdk at 1.32.1 and the newer @modelcontextprotocol/server at 2.3.1.

Does the missing-resource error code change?

Yes, and our test found a second surprise. The spec moves "resource not found" from -32002 to -32602, as SEP 2164 says. On the 2.3.0 server, we got -32602. On the 1.30.0 server, the Python SDK returned code 0 with the message "Unknown resource", which is neither number.

Client Server Code for a missing resource
1.30.0 1.30.0 0
1.30.0 2.3.0 -32602
2.3.0 2.3.0 -32602

If your client code matches a literal -32002, it has probably never matched anything on the Python SDK. Match on the new -32602 code, and also on the message text while old servers are still in the wild. Do this before you migrate, because the old servers will keep returning their own codes for months.

How do you keep state without sessions?

Make the state explicit. Without a session, a server cannot remember who called last. The AAIF guide says to mint an opaque handle in one tool call and have the model pass it back as an ordinary argument on the next call, or to store the state in a database or cache.

For example, a shopping tool used to remember a basket per session. Under the new flow, create_basket returns a basket_id, and every later call takes that ID as an argument. The model carries the handle, and any server instance can answer.

Two warnings from that guide are worth repeating. Losing sessions does not make your application stateless, because state just moves into your own storage. And statelessness does not make calls idempotent. A retried charge_card call still charges twice, so guard side effects with your own idempotency keys.

This also removes a deployment burden. Sticky routing is no longer required, so a plain round-robin load balancer works. For teams running MCP servers behind several instances, that is the best part of the release.

What is the TypeScript path?

The TypeScript migration swaps one package and one handler. The AAIF guide lists these steps, which we summarize without testing, because our lab used Python.

  1. Replace @modelcontextprotocol/sdk with @modelcontextprotocol/server.
  2. Replace manual StreamableHTTPServerTransport setup with createMcpHandler() and toNodeHandler().
  3. Delete the transport registry, session lookup, UUID generation and cleanup callbacks.
  4. Create a fresh McpServer for each request.
  5. Choose a legacy mode. legacy: 'stateless' accepts both 2025-era and 2026-era clients, and legacy: 'reject' accepts only 2026 clients.

The guide adds a caution. Legacy clients served through stateless mode do not keep their original initialize and session flow. If a client truly depends on that flow, run two handlers side by side: your old stateful handler, and a strict reject handler for modern clients.

What is deprecated, and how long do you have?

Three features are deprecated: Roots, Sampling and Logging. The release post says the deprecations are annotation-only. They keep working in this release and in every spec version published within a year of it. The post gives no removal dates, and says removal needs a separate proposal and at least twelve months after deprecation.

Deprecated Replacement the post names
Roots Tool parameters, resource URIs or server configuration
Sampling Direct LLM provider API integration
Logging stderr for stdio servers, or OpenTelemetry

If your server asks the client to run a model through Sampling, plan a direct provider call instead. If you log through the protocol, move to OpenTelemetry. Our post on Claude's OpenTelemetry traces shows what a trace export sends out, and the same ideas apply to MCP servers.

The experimental Tasks API also moves into an extension with tasks/get, tasks/update and tasks/cancel, and tasks/list is removed. If you built on the old Tasks API, that is the one rewrite you cannot defer.

Who has to act, and when?

Teams with SDK-managed servers should upgrade and test. Teams with hand-written transports must rewrite them. Teams that only use stdio servers have nothing to do for the session change.

Your situation Action Urgency
Stdio servers only Nothing for sessions; plan off Roots, Sampling, Logging Within a year
HTTP servers on an official SDK Upgrade to 2.x, keep legacy support on, test with old clients Next release cycle
Hand-written HTTP transport Rewrite for server/discover, _meta and the new headers Before clients upgrade
State keyed to Mcp-Session-Id Move to explicit handles or a database Before upgrading
Code matching -32002 Match -32602 and the message text Before upgrading
Uses experimental Tasks Move to the Tasks extension Before upgrading

Authorization needs attention too. SEP 2468 says clients must validate the iss parameter per RFC 9207, and the post says future clients will reject responses that omit it. If you run your own authorization server, add iss now. Our guide, Does Your MCP Client Leak OAuth Secrets? Check the SDK Version, covers another OAuth hazard worth checking in the same pass.

The bottom line is that this migration is a test plan, not a rewrite. Run your own four-way matrix before you ship. Log the protocol version header, the session header and the method. Then watch for the 400s. If a Laravel server is part of your fleet, Your Laravel MCP Server Now Returns 400 After Upgrading covers a related failure.

Advertisement

FAQ

Is the MCP 2026-07-28 spec backward compatible?

Mostly, at the SDK level. In our test, Python SDK 1.30.0 and 2.3.0 connected in all four client and server pairings. A new client fell back to the old handshake after one HTTP 400, and a new server still served old clients. Hand-written transports and session-keyed state do break.

What was removed in MCP 2026-07-28?

The initialize handshake, protocol-level sessions and the Mcp-Session-Id header were removed. Version, client info and capabilities now travel in _meta on each request, and a server/discover method replaces the capability exchange. Streamable HTTP now requires Mcp-Method and Mcp-Name headers.

Do I need to change my MCP server to be stateless?

Only if it keeps state per session. Anything keyed to the session must move to explicit handles that the model passes back as arguments, or to a database or cache. Servers that kept no per-session state need little more than an SDK upgrade and a test run.

What error code does MCP use for a missing resource now?

-32602, the standard JSON-RPC invalid params code, replacing the custom -32002. In our test, the Python 2.3.0 server returned -32602, while the 1.30.0 server returned code 0 with the message Unknown resource. Match on both the code and the message while old servers remain.

Are Roots, Sampling and Logging removed from MCP?

Not yet. The release deprecates all three, but they keep working in this release and in every spec version published within a year. Removal needs a separate proposal and at least twelve months. Replacements are tool parameters for Roots, direct provider APIs for Sampling, and stderr or OpenTelemetry for Logging.

Comments

Loading…

Sign in to join the conversation.

Related posts

Logos of Fin, HubSpot, Zendesk and Salesforce Agentforce beside a price per resolved support ticket

What an AI-resolved support ticket really costs in 2026

AI customer support agent pricing per resolution runs from $0.50 to about $4.00 for the same solved ticket. HubSpot charges $0.50 per resolved conversation, Fin charges $0.99, and Zendesk charges

Mon Oct 19 2026 · 10 min read · 0 views

AI