Tech13 min read

MCP 2026-07-28 goes stateless, compared old vs new with two minimal servers

IkesanContents

On July 28, Anthropic announced that the 2026-07-28 revision of the MCP specification had been finalized.
The initialize handshake and the Mcp-Session-Id header are gone, and the protocol has been rebuilt so that every request completes on its own. Sampling, Roots, and Logging are deprecated.
David Soria Parra of Anthropic, who has led the spec work, calls it MCP’s most important release since remote MCP first launched.
The four Tier 1 SDKs (TypeScript, Python, Go, C#) shipped support the same day the spec was published.
Still, code that depends on session IDs needs migration work, and anything that implements the wire protocol by hand hits a lot of breaking changes.
The minimum 12-month window only covers how long deprecated features must survive before removal; it is not a grace period for the breaking changes like statelessness.

Protocol-level sessions are gone

MCP started in November 2024 as a protocol for wiring local developer tools to LLMs.
It assumed a 1:1 connection over stdio, so both sides greeted each other with an initialize request and a notifications/initialized notification, then kept shared state for the rest of the connection.
The MCP 2026 roadmap also describes this origin as a way to connect local tools.
The design carried over to the Streamable HTTP transport for remote servers, where a server could issue an Mcp-Session-Id header.

Scale a stateful server out to multiple instances and you need sticky routing — pinning each session’s requests to the same instance — or shared session state across instances.
Even when the exposed functionality itself was stateless, large production deployments were complicated to build and operate.
It also fit poorly on serverless and edge platforms, where a different instance may answer every request.

In 2026-07-28 the handshake itself is gone.
The protocol version and the client’s capabilities ride in the _meta field of every request (io.modelcontextprotocol/protocolVersion and friends).
On Streamable HTTP, every POST also needs an MCP-Protocol-Version header that must match the value in _meta.
Servers are recommended to include their identity in the result’s _meta['io.modelcontextprotocol/serverInfo'], and they return UnsupportedProtocolVersionError on a version mismatch.
The Mcp-Session-Id header is removed, and tools/list / resources/list results no longer vary per connection.
When state has to span calls, the server mints a handle and passes it around as an ordinary tool argument.

The new server/discover RPC is mandatory for servers and returns supported versions, capabilities, and identity.
A client can call it first to pick a version up front, or use it as a probe over stdio to detect an old-generation server.

Here is the 2025-11-25 Streamable HTTP flow when the server issues a session ID.

flowchart TD
    A1["initialize request"] --> A2["receive Mcp-Session-Id<br/>in the initialize result"]
    A2 --> A3["send notifications/initialized<br/>with the ID attached"]
    A3 --> A4["attach the ID to every later<br/>HTTP request (tools/call etc.)"]

In the 2026-07-28 spec, that whole preamble disappears.

flowchart LR
    B1["tools/call<br/>version and capabilities<br/>included in _meta"] --> B2["result<br/>serverInfo in _meta (recommended)"]

Comparing the old and new wire exchanges with minimal servers

Reading the spec alone didn’t make it click, so I wrote a minimal server for each revision and hit them with curl.

Test environment

ItemDetails
MachineM4 Mac mini (10-core / 16GB)
Node25.3
ImplementationNo official SDK. Minimal servers handling Streamable HTTP JSON-RPC with plain node:http, about 70 lines each
Toola single add tool that adds two numbers
Methodhit directly with curl; the responses shown are actual output

In the old-spec (2025-11-25) server, session ID management takes up most of the code.
It mints a UUID on initialize, remembers it in a Set, and rejects requests without the ID. With curl, it takes three round trips before you get to call add once.

# いきなりtools/callを呼ぶと拒否される
$ curl -s -X POST localhost:8931 -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",...}'
{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Bad Request: No valid session ID provided"}}

# initializeでセッションIDをもらう
$ curl -si -X POST localhost:8931 -d '{"jsonrpc":"2.0","id":1,"method":"initialize",...}'
HTTP/1.1 200 OK
Mcp-Session-Id: 85c6e611-f672-4106-b74b-fca724f0dafb
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25",...}}

# 以後のリクエストは全部このヘッダを付ける
$ curl -s -X POST localhost:8931 -H "Mcp-Session-Id: 85c6e611-..." \
    -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}'
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"5"}]}}

The new-spec (2026-07-28) server loses all of that session management, and per-request validation takes its place. The core of the handler looks like this.

// ルーティング用ヘッダとボディの一致を検証(-32020 HeaderMismatch)
if (req.headers['mcp-method'] !== msg.method) return fail(-32020, 'HeaderMismatch');

// server/discover は実装必須。対応バージョンとcapabilitiesを返す
if (msg.method === 'server/discover') {
  return reply({ resultType: 'complete', protocolVersions: ['2026-07-28'],
    capabilities: { tools: {} }, serverInfo: SERVER_INFO });
}

// セッションの代わりに、毎リクエストの_metaでバージョンを検証する
const version = msg.params?._meta?.['io.modelcontextprotocol/protocolVersion'];
if (version !== '2026-07-28') return fail(-32022, 'UnsupportedProtocolVersion');

if (msg.method === 'tools/call') {
  return reply({ resultType: 'complete', content: [{ type: 'text', text: String(a + b) }],
    _meta: { 'io.modelcontextprotocol/serverInfo': SERVER_INFO } });
}

The client side finishes in one round trip.

$ curl -s -X POST localhost:8932 -H 'Mcp-Method: tools/call' -H 'Mcp-Name: add' \
    -H 'MCP-Protocol-Version: 2026-07-28' \
    -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3},
         "_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'
{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","content":[{"type":"text","text":"5"}],
 "_meta":{"io.modelcontextprotocol/serverInfo":{"name":"demo-new","version":"0.1.0"}}}}

Dropping the _meta version got me a -32022, and mismatching the header and body methods got a -32020.

Restarting the servers is where the difference showed clearly.
Restart the old server and the Set is wiped, so the same tools/call with an already-issued session ID gets rejected. The client has to start over from initialize.
Restart the new server and resending the exact same request just goes through.

# 旧: サーバー再起動後、同じセッションIDでtools/call
{"jsonrpc":"2.0","id":4,"error":{"code":-32000,"message":"Bad Request: No valid session ID provided"}}

# 新: サーバー再起動後、同じリクエストをそのまま再送
{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","content":[{"type":"text","text":"5"}],...}}

Scaling to multiple instances runs into the same thing: under the old spec you either share that Set across all instances or pin clients to one instance with sticky routing.
The new-spec server has nothing to remember, so the result is the same whichever instance you hit and whenever it restarts.

Server-initiated requests are replaced by the MRTR pattern

MCP used to have requests that flew backwards from server to client: sampling/createMessage (the server asks the client-side LLM to generate), elicitation/create (asking the user for extra input), and roots/list (asking for working directories).
On the old Streamable HTTP, the server sent these reverse JSON-RPC requests over the still-open SSE response of an in-flight request, then continued the original work.

Their replacement is Multi Round-Trip Requests, MRTR for short.
A server that needs more information returns an InputRequiredResult with resultType set to input_required, carrying its requests in the inputRequests field. Only prompts/get, resources/read, and tools/call may return this result.
The client retries the original request with inputResponses attached, echoing back requestState if the server returned one, under a fresh JSON-RPC request ID.
It has turned into a round trip where the server answers “here is what I still need” as a response to the request.

flowchart TD
    C1["client: tools/call"] --> S1["server: resultType input_required<br/>inputRequests asks for more info"]
    S1 --> C2["client: resend the original request with a new ID<br/>attaching inputResponses and requestState"]
    C2 --> S2["server: resultType complete"]

Along with this, a resultType field is now required on every result.
Ordinary results are complete; MRTR interim results are input_required.
If an older-generation server omits the field, clients treat the result as complete.

Change notifications move to subscriptions/listen

Server-to-client change notifications were consolidated too.
The Streamable HTTP GET endpoint and resources/subscribe / resources/unsubscribe are removed in favor of a single RPC, subscriptions/listen. On Streamable HTTP, each listen request opens a long-lived POST response stream.
Clients explicitly opt in to the notification types they want, such as toolsListChanged or resourceSubscriptions.
Multiple listen requests can be open at the same time, and the server acknowledges the accepted types and a subscription ID.
Progress (notifications/progress) and log messages (notifications/message) flow on the response stream of the original request instead.

SSE stream resumability is gone as well.
SSE (Server-Sent Events) keeps an HTTP response open so the server can keep pushing events, and the old spec had redelivery: after a disconnect, a client could resume mid-stream with the Last-Event-ID header.
In 2026-07-28, when a response stream breaks, the in-flight request is considered lost and the client re-issues it under a new request ID.
Keeping the event history needed for resumption was itself a source of server-side state.

ping, logging/setLevel, and notifications/roots/list_changed are removed too.
Log level is set per request via _meta['io.modelcontextprotocol/logLevel'].
Servers must not emit log notifications for requests that did not include the field.

MCP Apps and Tasks became official extensions

An extensions field was added to capabilities. It is the formal framework for adding features outside the core spec.
The extensions highlighted in the 2026-07-28 announcement are MCP Apps, which renders interactive UI inside conversations, and Tasks, which handles long-running work. There are also official authorization-related extensions.
Extensions are versioned independently of the core spec and are not necessarily merged into it.

Tasks used to sit in the core spec as an experimental feature and has been redesigned as the io.modelcontextprotocol/tasks extension.
The blocking tasks/result is replaced with polling via tasks/get, and the new tasks/update sends client input to a running task.
tasks/list is removed. As long as the client declares Tasks support on each request, the server can return task handles without any per-request opt-in flag.

Roots, Sampling, and Logging deprecated with a 12-month minimum clock

This revision introduces a feature lifecycle policy: features are managed as Active, Deprecated, or Removed.
A newly deprecated feature becomes removable after a minimum of 12 months in principle, with the actual removal decided in a later spec revision. An exception allows as little as 90 days for unaddressed critical security risks, and deprecated features and their earliest removal dates are tracked in a registry.

The first batch: Roots, Sampling, and Logging are deprecated.
They saw little use or caused implementation confusion. The spec’s suggested migrations:

DeprecatedMigrate to
Rootspass target directories and files via tool arguments, resource URIs, or server config
Samplingcall the LLM provider’s API directly
Loggingwrite to stderr (stdio) or use OpenTelemetry
HTTP+SSE transportStreamable HTTP
Dynamic Client Registration (RFC 7591)Client ID Metadata Documents

The HTTP+SSE transport, informally deprecated since 2025-03-26, is now formally reclassified as Deprecated under the lifecycle policy.
It falls under transitional rules: its earliest removal in the registry is three months after SEP-2596 reaches Final.
Dynamic Client Registration for authorization (the RFC 7591 mechanism where a client auto-registers with an authorization server) is deprecated too; new implementations should use Client ID Metadata Documents, where a client publishes its own metadata at a URL. Dynamic Client Registration stays available as a fallback for authorization servers that do not support them.

Authorization now matches enterprise production setups

The authorization changes assume MCP plugs straight into corporate identity providers like Entra ID or Okta.

Authorization servers should now include the iss parameter (the token issuer’s identifier) in authorization responses. Clients must check the received iss against the recorded issuer and, on a mismatch, reject the response without sending the authorization code to the token endpoint. The same applies when a server declares iss support but the parameter is missing from the response.
This follows RFC 9207 and counters the OAuth mix-up attack, where a malicious authorization server tricks the client into sending an authorization code issued by a legitimate server to the attacker’s token endpoint.
Pre-registered credentials and credentials obtained and persisted via Dynamic Client Registration must be stored keyed to the issuing authorization server and never reused with a different one. Client IDs based on Client ID Metadata Documents URLs remain portable across authorization servers.

Smaller changes aimed at caching and routing

complete results of server/discover, tools/list, prompts/list, resources/list, resources/templates/list, and resources/read now require ttlMs and cacheScope fields.
ttlMs is a freshness hint in milliseconds, not a guarantee that the data won’t change and not an auto-polling interval. cacheScope: public allows shared caches across authorization contexts; private restricts reuse to the same context.
tools/list should return tools in a deterministic order. Tool lists end up in LLM prompts, so a stable order is meant to raise prompt-cache hit rates.

Streamable HTTP JSON-RPC requests require an Mcp-Method header, and tools/call, resources/read, and prompts/get also require Mcp-Name. The MCP-Protocol-Version header on every POST must match the value in the body’s _meta.
Load balancers and gateways can route and rate-limit per method from headers alone, without reading JSON-RPC bodies.
Conventions for carrying OpenTelemetry trace context such as traceparent in _meta are documented as well.

Migration impact and where Claude stands

These are breaking changes: an old-generation client and a new-generation server, or the reverse, cannot talk unless both sides support the same protocol generation.
Over stdio, server/discover works as a probe; over Streamable HTTP, check the error body for a new-style request and fall back to the old style. Implementations that support both generations switch between 2026-07-28 and the older spec depending on the peer.

According to Anthropic, MCP SDKs now top 400 million monthly downloads, a 4x increase in 2026.
2026-07-28 support is rolling out across Claude products, and Claude’s connectors directory lists more than 950 MCP servers.
For hand-rolled implementations, the spec text and each SDK’s migration guide are the starting points. The earliest that Roots, Sampling, and Logging can be removed from the registry is the first spec revision published after July 28, 2027.

References