If you learned the Model Context Protocol from a tutorial written before August 2026, most of what it taught about transport is now wrong. There is no initialize handshake. There is no Mcp-Session-Id. There is no HTTP GET stream to listen on, and streams do not resume. The 2026-07-28 revision, released on 28 July 2026, removed all of them and replaced them with something closer to how the rest of the web works: every request is self-describing, so any request can land on any server instance. Protocol version, client identity and capabilities now travel per request in _meta; application state that must survive across tool calls can be represented by explicit handles passed as tool arguments.
This article is a migration-oriented reading of that revision. For each change it states what you can delete, what you must add, and what you have to keep if you still serve clients from the previous era. It ends with an integration decision table and a production checklist. Everything here is drawn from the specification, its changelog and the deprecated-features registry as they stand on 21 September 2026; where the SDKs lag the spec, that is stated.
MCP in September 2026
Three dates anchor the current state:
-
2025-11-25 is the last "legacy" revision: session-based, with an
initializehandshake and server-initiated requests. - 2026-07-28 is the current revision (release candidate 29 May 2026, final 28 July 2026). The specification's own versioning page calls implementations of this and later revisions modern, implementations of 2025-11-25 and earlier legacy, and anything that supports both dual-era. Those three words are used throughout this article.
-
28 July 2026 is also when Tier 1 SDKs shipped for the new revision. The TypeScript SDK became a family of
@modelcontextprotocol/*2.0.0 packages (server,client,core,node,hono,express, plus aserver-legacypackage). The Python SDK's 2.x line is at 2.2.0 (7 September 2026), alongside a 1.30.0 maintenance release for the legacy line.
The protocol is governed under the Agentic AI Foundation, with a formal SEP (specification enhancement proposal) process and, new in this revision, a feature lifecycle policy with a minimum twelve-month deprecation window. That last item matters for planning: nothing deprecated on 28 July 2026 can be removed before a revision released on or after 28 July 2027.
What 2026-07-28 removed
Sessions and Mcp-Session-Id
Protocol-level sessions are gone from the Streamable HTTP transport (SEP-2567). A modern server does not mint session IDs, does not echo them, and ignores an Mcp-Session-Id header if a legacy client sends one; a GET or DELETE to the MCP endpoint from an older client gets 405 Method Not Allowed. The consequence the changelog calls out explicitly: tools/list, resources/list and prompts/list no longer vary per connection. If your server previously returned a different tool set depending on session state, that behaviour has no home in the modern protocol; the tool set is a property of the server (and of the authenticated principal), not of a conversation.
The initialize handshake
There is no initialize request and no notifications/initialized (SEP-2575). Instead every request carries its own protocol version and client capabilities in _meta, and the server accepts or rejects each request independently. A version the server does not support gets an UnsupportedProtocolVersionError (code -32022) listing the versions it does support, and the client retries with one of them.
The GET stream, resumability and a few utilities
The standalone SSE stream a client used to open with HTTP GET is gone, replaced by subscriptions/listen (below). SSE resumability and message redelivery (Last-Event-ID, event IDs) are removed from the transport: a broken response stream loses the in-flight request and the client must re-issue it as a new request with a new ID. ping, logging/setLevel and notifications/roots/list_changed are removed too; log level is now requested per call via io.modelcontextprotocol/logLevel in _meta, and a server must not emit notifications/message for a request that did not ask for it.
Server-initiated requests
Servers no longer send their own JSON-RPC requests to clients on any stream. roots/list, sampling/createMessage and elicitation/create still exist as request shapes, but they are now carried inside results (see MRTR below). The notifications/elicitation/complete notification and the elicitationId field, both added only in 2025-11-25, are removed with them.
What replaced it
Per-request _meta
Every request now describes itself. The reserved keys are io.modelcontextprotocol/protocolVersion (required), io.modelcontextprotocol/clientCapabilities (required), and io.modelcontextprotocol/clientInfo (a client SHOULD send it). Servers SHOULD identify themselves with io.modelcontextprotocol/serverInfo in each result's _meta. On Streamable HTTP the version is mirrored into the MCP-Protocol-Version header, and the two must match or the server rejects the request with HeaderMismatch (-32020).
A modern tools/call over HTTP looks like this:
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Seattle, WA" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
There is no prior message. That request is valid as the first thing a client ever sends.
server/discover
Servers MUST implement server/discover, which returns supported protocol versions, capabilities and identity. Clients MAY call it up front to choose a version, and on stdio it doubles as the backward-compatibility probe. It is optional for clients: a client is free to send any RPC inline and handle UnsupportedProtocolVersionError if it guessed wrong. The response is cacheable.
{
"jsonrpc": "2.0",
"id": "discover-1",
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": {}, "resources": {} },
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "ExampleServer", "version": "1.0.0" }
}
}
}
subscriptions/listen
A client that wants change notifications sends one subscriptions/listen request, opting in to specific types (toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions). The response is a long-lived SSE stream that carries only those notifications, each tagged with a subscription ID. Request-scoped notifications (notifications/progress, notifications/message) do not travel on it; they stay on the response stream of the request they belong to. Servers are encouraged to send SSE comment lines as keep-alives on this stream.
Multi Round-Trip Requests and InputRequiredResult
This is the change that most affects tool design. When a server needs something from the client mid-call (user input via elicitation, an LLM completion via sampling, or the roots list) it no longer sends a request. It returns an interim result with resultType: "input_required" whose inputRequests map carries the requests it needs fulfilled, plus an opaque requestState blob (SEP-2322). The client gathers the input and retries the original request with a new ID, adding inputResponses and echoing requestState. The server reconstitutes whatever it needs from that blob; the two requests are fully independent and may be served by different instances.
Client Server
| POST tools/call (id 1) |
|---------------------------------------->|
| | needs user input
| InputRequiredResult |
| resultType: "input_required" |
| inputRequests: { github_login: elicitation/create }
| requestState: "<opaque>" |
|<----------------------------------------|
| (client prompts the user) |
| POST tools/call (id 2) |
| original params + inputResponses |
| + requestState |
|---------------------------------------->|
| | reconstitutes state, completes
| result, resultType: "complete" |
|<----------------------------------------|
Every result now carries a required resultType. Modern core results use "complete" or "input_required"; negotiated extensions may define additional values, and the Tasks extension uses resultType: "task" for its CreateTaskResult. Clients MUST treat results from earlier-protocol servers that omit the field as "complete".
Hosting consequences
The transport now mirrors selected body fields into HTTP headers so that intermediaries can route and inspect without parsing JSON. Mcp-Method is required on every POST; Mcp-Name is required on tools/call, resources/read and prompts/get (SEP-2243) (carrying the tool name or resource URI, Base64-wrapped when it is not header-safe). A server MAY annotate a tool parameter with x-mcp-header in its input schema, and conforming clients MUST then mirror that argument into an Mcp-Param-{Name} header; the canonical example is a region parameter that a gateway uses to route the call. Servers that process the body MUST validate that headers and body agree and reject mismatches with 400 and -32020.
Two more changes are aimed squarely at caches and prompt reuse. tools/list, prompts/list, resources/list, resources/read and resources/templates/list results now carry ttlMs (a freshness hint) and cacheScope ("public" or "private", controlling whether a shared intermediary may cache them) (SEP-2549). And servers SHOULD return tools from tools/list in a deterministic order, because a stable tool list is what lets the client's prompt cache hit.
Put together: normal modern MCP requests are stateless HTTP calls. A server can sit behind a round-robin load balancer, be scaled horizontally, be fronted by a gateway that routes on Mcp-Name or Mcp-Param-Region, and have its list results cached with normal HTTP semantics. That is the design goal the maintainers state in the release post, and it is the reason to migrate even if nothing you had was broken.
One caveat so the picture is complete: subscriptions/listen is still a long-lived SSE stream. If your deployment runs several instances and any of them can change a tool list or a resource, delivering those notifications to a client whose listen stream is held open by a different instance requires shared pub/sub or equivalent infrastructure. Request handling is stateless; notification fan-out is a deployment concern you still have to design.
Stateful work without sessions
Dropping protocol sessions does not force your application to be stateless. It moves state from the transport, where the model could not see it, into tool arguments, where it can. The pattern the maintainers recommend: mint an explicit, server-issued handle from one tool and have the model pass it back to later tools as an ordinary argument.
A worked example. A start_export tool creates a job and returns a handle:
{
"resultType": "complete",
"content": [{ "type": "text", "text": "Export started." }],
"structuredContent": { "export_handle": "exp_9f31c2", "expires_at": "2026-09-21T13:00:00Z" }
}
A later get_export_status call takes export_handle as a parameter. For a horizontally scaled server, prefer keeping the state the handle refers to in shared, durable storage (a database, a job queue, an object store) so that any instance can serve any call. It is possible instead to mirror the handle into a header with x-mcp-header and have a gateway route every call for that export to the instance holding it in memory, but be clear about what that is: application-level affinity, reintroduced by your deployment, even though MCP itself remains sessionless. Handles should carry or imply an expiry, and a call with an expired or unknown handle should return an ordinary tool error the model can act on ("that export has expired, start a new one"), not a transport error.
For interactions that need user input mid-call, use MRTR rather than a handle: the requestState blob is the server's own scratch space and should be protected (encrypted or signed) since it round-trips through the client.
Compatibility matrix
The specification defines the expected outcome of every client and server pairing. Condensed:
| Client | Server | Outcome |
|---|---|---|
| Modern | Modern | Works. server/discover optional; version mismatch surfaces as UnsupportedProtocolVersionError and the client retries with a supported version. |
| Modern | Legacy | Fails. The legacy server may reject, stay silent, or misinterpret. On stdio, send server/discover first so the failure is deterministic. |
| Dual-era | Modern | Works. The first modern request succeeds or returns a modern error; the client stays modern. |
| Dual-era | Legacy | Works. The modern request gets a 4xx without a recognised modern error body; the client falls back to initialize. |
| Legacy | Modern | Fails. Missing headers and _meta are rejected with 400. Legacy clients have no fall-forward mechanism. |
| Legacy | Dual-era | Works. The server answers initialize and serves the legacy revision. |
The detection rule for a dual-era client on HTTP: attempt a modern request; on 400, inspect the body. A recognised modern JSON-RPC error (UnsupportedProtocolVersion, MissingRequiredClientCapability, HeaderMismatch) means the server is modern, so correct the request or retry with an advertised version. An empty or unrecognised body means legacy, so fall back to initialize. Era is a property of the server, not of a request; cache the result per origin.
A dual-era server chooses its behaviour from how the client opens: a request carrying modern _meta is served statelessly, an initialize request selects legacy semantics scoped to that session. The Python SDK 2.2.0 implements exactly this (MCPServer and mcp.Client with a server/discover probe and legacy fallback), and its release notes add a detail worth knowing: idle legacy Streamable HTTP sessions now expire, which does not affect stateless or modern connections.
Deprecations and the lifecycle
The revision introduced a formal feature lifecycle (Active, Deprecated, Removed) with a minimum twelve-month deprecation window and a registry of deprecated features (SEP-2596); Roots, Sampling and Logging were deprecated under it (SEP-2577). As of 21 September 2026 the registry lists:
| Feature | Deprecated in | Migration | Earliest removal |
|---|---|---|---|
| Roots | 2026-07-28 | Pass directories or files via tool parameters, resource URIs or server configuration | First revision on or after 2027-07-28 |
| Sampling | 2026-07-28 | Integrate directly with LLM provider APIs | First revision on or after 2027-07-28 |
| Logging | 2026-07-28 |
stderr on stdio; OpenTelemetry for observability |
First revision on or after 2027-07-28 |
| Dynamic Client Registration (RFC 7591) | 2026-07-28 | Client ID Metadata Documents | First revision on or after 2027-07-28 |
includeContext: "thisServer" / "allServers"
|
2025-11-25 | Omit or use "none"
|
Follows Sampling |
| HTTP+SSE transport (2024-11-05) | 2025-03-26 | Streamable HTTP | Registry wording: "Three months after SEP-2596 reaches Final"; check the deprecated-features registry before removal |
Nothing has been removed under the policy yet. Deprecated features keep working during the window, but new implementations should not adopt them. The "earliest removal" column marks when a feature becomes eligible for removal; the registry states that actual removal is a maintainer decision taken during release preparation and may happen later, so treat the registry, not this table, as the source of truth. Note that Roots, Sampling and Elicitation are still delivered through MRTR in the current revision; Roots and Sampling are simply on their way out, while Elicitation is not deprecated.
Authorization hardening
The authorization section is optional for MCP implementations, but where a server implements it the requirements were tightened: OAuth 2.1 (draft 13) with the MCP server acting as a resource server; OAuth 2.0 Protected Resource Metadata (RFC 9728) is now mandatory for authorization server discovery; Client ID Metadata Documents are the preferred registration mechanism, with Dynamic Client Registration deprecated; resource indicators (RFC 8707) bind tokens to the server; authorization servers SHOULD return iss and clients MUST validate it (RFC 9207); client credentials are bound to the issuer that granted them; and a step-up flow lets a server challenge for additional scopes on a per-call basis. Two 2026-07-28 changelog items are easy to miss: the resource-not-found error code moved from -32002 to -32602, and a new error-code allocation policy reserves -32020 to -32099 for the specification (the earlier draft codes -32001, -32003 and -32004 were renumbered to -32020, -32021 and -32022).
Extensions, and an integration decision table
Capabilities now carry an extensions map. Official MCP extensions currently include:
-
Tasks (
io.modelcontextprotocol/tasks), moved out of the core in this revision and redesigned (SEP-2663): polling viatasks/getreplaces the blockingtasks/result,tasks/updatecarries client-to-server input,tasks/listis gone, and a server may return a task handle unsolicited. As of 7 September 2026 the Python SDK 2.2.0 release notes state the Tasks extension is not yet implemented; check your SDK before designing around it. -
MCP Apps (
io.modelcontextprotocol/ui), for servers that return interactive HTML surfaces the host renders. -
OAuth Client Credentials (
io.modelcontextprotocol/oauth-client-credentials), for machine-to-machine authentication where no interactive user authorization is present. - Enterprise-Managed Authorization, for organisations that need identity managed centrally rather than per server.
Which integration shape to choose:
| Situation | Recommended shape | Why |
|---|---|---|
| Local developer tools, one user, one machine | stdio | No network, no auth, server/discover as the probe; stateless semantics still apply |
| Internal service used by several agents | Streamable HTTP, modern only, behind your normal load balancer | Stateless core; route on Mcp-Method / Mcp-Name; cache list results with ttlMs
|
| Public or partner-facing server | Streamable HTTP, modern only, with the authorization section implemented | RFC 9728 discovery, resource-bound tokens, scope step-up; validate Origin
|
| Many servers behind one entry point | Gateway that validates headers against bodies and routes on Mcp-Name and Mcp-Param-*
|
This is what the mirrored headers exist for; the gateway must reject header and body mismatches |
| Long-running work (minutes to hours) | Tasks extension where the SDK supports it; otherwise an explicit handle plus a polling tool | Avoid holding a response stream open across minutes; streams are not resumable |
| Clients you do not control and cannot upgrade | Dual-era server for the twelve-month window, with a dated plan to drop legacy | Legacy clients cannot fall forward; only the server can bridge |
Failure modes
Legacy assumptions in new code. The most common migration bug is code that still sends initialize first, or expects an Mcp-Session-Id in the response, and then treats a modern server's 400 as a connection failure. A modern 400 carries a JSON-RPC error body that tells you exactly what to do; read it before retrying.
Buffering proxies. Streamed responses depend on the proxy passing SSE events through as they arrive. The specification recommends servers send X-Accel-Buffering: no; nginx-style proxies otherwise hold events and the stream looks dead. Keep-alive comment lines on subscriptions/listen streams prevent idle timeouts.
Header and body disagreement. Anything that rewrites bodies (a middleware that renames a tool, a proxy that normalises JSON) will produce HeaderMismatch rejections. The mirrored headers are validated against the body on purpose, so intermediaries must rewrite both or neither. Intermediaries that route on headers should also check the MCP-Protocol-Version indicates a revision that requires validation, and reject older traffic rather than trust unvalidated headers.
Stale cached lists. With ttlMs on list results, a client may legitimately serve a cached tool list for the hint's duration. If you add or remove tools, emit notifications/tools/list_changed to subscribers and keep ttlMs short during rollouts.
Lost in-flight requests. Streams do not resume. A load balancer that drains connections mid-response will lose the request, and the client must re-issue it with a new ID; make long tools idempotent or hand them a handle so the retry is safe.
Non-deterministic tools/list. A tool list that changes order between calls defeats both client-side caching and the model provider's prompt cache. Sort it.
Production checklist and tradeoffs
- Every request carries
_metawith protocol version and client capabilities; the HTTP header matches the body. -
server/discoveris implemented and its result is cacheable. - No code path depends on a session: cross-call state is an explicit handle in tool arguments, with expiry and a model-readable error when stale.
- Mid-call input uses MRTR;
requestStateis protected and the retry path is tested with a different server instance handling the second request. -
tools/listis deterministic and returnsttlMsandcacheScope; changes are announced onsubscriptions/listen. - Origin is validated; local servers bind to localhost; the authorization section, where used, follows the 2026-07-28 rules (RFC 9728 discovery, resource indicators,
issvalidation). - Proxies pass SSE through unbuffered and forward
Mcp-*headers untouched. - Roots, Sampling, Logging and Dynamic Client Registration are not adopted in new code; existing uses have a migration ticket dated before July 2027.
- If legacy clients exist, the server is dual-era for a bounded period with a written end date.
- The SDK's support for Tasks, DPoP and the
jwt-bearergrant is checked against its release notes before any design depends on them.
The tradeoffs are real. Stateless requests mean more bytes per call (_meta on every request, capabilities repeated) and no transport-level continuity for free; you pay for that with explicit handles and MRTR retries. Dual-era support doubles your test matrix for a year. And the SDKs are not uniformly ahead of the spec. In exchange, an MCP server becomes something a platform team can host, scale, cache, route and observe with the tools it already has, which is the difference between a demo integration and one you can put a production agent on.
If you are migrating, I would like to hear which 2025-era assumption broke first in your codebase. The next article in this series walks through building a modern MCP server for internal tools end to end: transport, authentication and error handling.
Drafted with AI assistance and reviewed, edited and approved by the author.
Top comments (0)