01
The story: one request, everything included
Old MCP opened a conversation: handshake first, then a session id on every call, with server and client keeping shared state alive between them. New MCP is a series of self-contained envelopes: each request names its protocol version and capabilities inside _meta, and SHOULD say who is calling (clientInfo). If versions clash, the server answers UnsupportedProtocolVersionError and that is the whole negotiation.
Before: statefulsession era
initialize handshake opens the session
- Server issues
Mcp-Session-Id
- Every call must present that session id
- Server holds per-session state in memory
- A dropped stream could resume via
Last-Event-ID
Scaling means sticky sessions or shared session storage.
After: statelessthis revision
- No handshake; optionally probe with
server/discover
- Each request carries version + capabilities in
_meta
- Client SHOULD identify itself per request (
clientInfo)
- Cross-call state travels as explicit handles in tool arguments
- A broken stream means re-issue as a new request, no redelivery
Any plain round-robin load balancer works, no shared storage.
One new obligation came with the freedom: servers MUST implement server/discover, which advertises supported versions, capabilities, and identity. Clients MAY call it up front, and on STDIO it doubles as the backward-compatibility probe.
02
The change ledger, read as six diff hunks
@@ stateless core @@QA impact: high
- initialize / notifications/initialized handshake- Mcp-Session-Id header on Streamable HTTP+ _meta: io.modelcontextprotocol/protocolVersion + clientCapabilities per request+ server/discover (servers MUST implement)+ explicit server-minted handles as ordinary tool arguments
The headline change. List endpoints no longer vary per connection, so a fleet of identical server replicas behaves identically. Anything in your harness that mocked the handshake or asserted on session headers is now testing a protocol that new servers no longer speak.
Test it: restart the server mid-suite. Under the new shape, in-flight requests fail and re-issue cleanly; nothing should depend on a session surviving.
@@ MRTR: multi round-trip requests @@QA impact: medium
- server-initiated roots/list, sampling/createMessage, elicitation/create+ resultType: "input_required" + inputRequests in the result+ client retries the ORIGINAL request with inputResponses every result now carries resultType ("complete" | "input_required")
Servers stop calling the client back over an open stream. When a server needs something (a directory, a model completion, a human answer), it returns an interim result saying "input required", and the client retries the same request carrying the answers. Correlation across retries rides in requestState.
- Back-compat rule worth memorizing. Results from older servers that omit
resultType MUST be treated as "complete".
Test it: a client under test must survive an input_required interim result and retry correctly, that is a brand-new happy path plus a brand-new timeout path.
@@ transport: headers, caching, no redelivery @@QA impact: high
- SSE resumability: Last-Event-ID, event ids, message redelivery- HTTP GET endpoint, resources/subscribe, resources/unsubscribe+ Mcp-Method and Mcp-Name headers required on Streamable HTTP POSTs+ ttlMs + cacheScope on tools/prompts/resources list + resources/read results+ subscriptions/listen: one opt-in stream for change notifications
Gateways and rate limiters can now route and police MCP traffic on plain HTTP headers without parsing JSON bodies. List and read results declare how long they may be cached (ttlMs) and whether shared intermediaries may cache them (cacheScope "public" or "private"). And when a response stream breaks, the in-flight request is simply lost: clients MUST re-issue it as a new request with a new id.
Test it: two new bug classes arrive, stale-cache bugs (a client trusting ttlMs past a real change) and retry bugs (double-effects when a lost request is re-issued against a non-idempotent tool).
@@ tasks move to an extension @@QA impact: low
- experimental tasks in core; blocking tasks/result; tasks/list+ io.modelcontextprotocol/tasks extension+ poll with tasks/get; send input with tasks/update+ servers may return task handles unsolicited
Long-running work formalizes as an extension: poll-based instead of blocking, with a new client-to-server input channel. Capabilities gained an extensions field on both sides to declare support.
Test it: polling loops need budget assertions (poll interval, give-up time), the classic async-testing discipline.
@@ authorization hardening @@QA impact: medium
+ clients MUST validate a present iss (RFC 9207) against the recorded issuer+ application_type required during Dynamic Client Registration+ credentials keyed by issuer, never reused across auth servers DCR itself is deprecated in favor of CIMD (next section)
Three tightenings aimed at confused-deputy and mixed-up-issuer attacks. If you test an MCP client that does OAuth, the issuer-binding rules are new required behavior, not suggestions.
Test it: negative cases: a mismatched iss must abort redemption, and credentials from one authorization server must never be replayed against another.
@@ error codes renumbered @@QA impact: high
- resource not found: -32002+ resource not found: -32602 (Invalid Params, JSON-RPC alignment)- HeaderMismatch -32001, MissingRequiredClientCapability -32003, UnsupportedProtocolVersion -32004+ renumbered: -32020, -32021, -32022 (spec range -32020..-32099 reserved)
The quietest change on the page and the one most likely to fail a pipeline on upgrade day. The error-code space is now partitioned: -32000..-32019 stays implementation-defined (existing SDK usage grandfathered), -32020..-32099 belongs to the spec.
Test it: grep your suites for hardcoded -32002 and the renumbered trio right now; assert on error semantics or symbolic names where the SDK offers them, not raw integers.
03
The deprecation registry, and the clock on it
The revision also formalized a feature lifecycle: Active, Deprecated, Removed, with a public registry of everything deprecated. Deprecated features keep working during the window; new implementations should not adopt them.
⏰ Minimum twelve months between Deprecated and Removed, so nothing here breaks today.
| Feature | Status | Migrate to |
| Roots | Deprecated | Pass directories and files via tool parameters, resource URIs, or server configuration. |
| Sampling | Deprecated | Integrate directly with LLM provider APIs. |
| Logging | Deprecated | Log to stderr on stdio, or use OpenTelemetry (trace context now has documented _meta conventions). |
| HTTP+SSE transport | Deprecated | Streamable HTTP. |
| includeContext "thisServer" / "allServers" | Deprecated | Omit the field or use "none"; removed no later than Sampling itself. |
| Dynamic Client Registration (RFC 7591) | Deprecated | Client ID Metadata Documents (CIMD); DCR stays for authorization servers without CIMD support. |
Also gone outright in this revision (not deprecated, removed): ping, logging/setLevel, notifications/roots/list_changed, and SSE resumability. Log level now travels per request as io.modelcontextprotocol/logLevel in _meta.
04
What it breaks in your tests, and what holds
- Hardcoded error-code assertions:
-32002 is now -32602, and the header/capability/version trio moved to -32020..-32022.
- Handshake mocks and
Mcp-Session-Id fixtures: dead code against new servers.
- Stream-resume assumptions: no
Last-Event-ID redelivery; lost request means re-issue, so non-idempotent tools need double-effect tests.
- Suites that treat
tools/list as per-connection: lists no longer vary by connection and SHOULD come back in deterministic order.
- Clients that ignore
resultType: an input_required interim result is now a normal response you must handle.
- Tools, prompts, and resources as concepts: unchanged. Calling a tool looks the same to the person prompting.
- Usage-level tutorials (wiring Playwright MCP into an agent, using tools from Copilot or Claude Code) stay correct; it is the plumbing chapters that age.
- Deprecated features keep working for the whole window, so existing setups run while you migrate deliberately.
- Deterministic list ordering is a gift: list snapshots become stable, and prompt caching gets better hit rates.
- STDIO local workflows:
server/discover doubles as the compatibility probe, old and new can coexist during the transition.
MRTRmulti round-trip requests, the retry-with-answers pattern
CIMDClient ID Metadata Documents, the DCR successor
ttlMshow long a list result may be cached
cacheScope"public" or "private": may intermediaries cache it
server/discoverthe mandatory who-are-you RPC
resultType"complete" or "input_required" on every result