MCP 2026-07-28 Goes Stateless: What Breaks and How to Migrate to V2
In this guide20 sectionsHide
MCP 2026-07-28 is the protocol's biggest revision since launch. It strips out the initialize handshake, the Mcp-Session-Id, and the assumption that protocol state persists across requests.
Session IDs were never well-defined across implementations in the first place. One client might open a session per chat per user. Another might share one session across every chat for a user. A third might drop the session entirely the moment a process crashes. Building application logic on that foundation was always fragile.
2026-07-28 removes it instead. If your server stores application state behind a session ID, that state has nowhere to attach once a client stops sending one. One side has to support both eras, or the conversation fails before the first useful call.
Developers have started calling this "MCP v2." MCP actually versions the protocol by date, not by number, so that's not the official name, but it's a fair shorthand for how big the change is.
Legacy clients and servers can keep speaking legacy MCP to each other indefinitely, and nothing forces an upgrade. The break only shows up when one side moves to the modern protocol, the other doesn't, and neither is built to handle both.
This guide walks through what changed, what breaks, what survives, where developers disagree, and how to migrate.
MCP 2026-07-28 in 60 seconds
Here's the release boiled down to eight points:
- The core protocol is stateless. The
initializehandshake andMcp-Session-Idare removed for modern implementations. - Protocol context travels with every request. Protocol version and client capabilities move into
_meta, withclientInforecommended rather than required. Servers also exposeserver/discoverfor on-demand discovery. - Cross-call application state becomes explicit. Long-lived workflows can use handles passed through tool arguments instead of hidden protocol sessions.
- Streamable HTTP becomes easier to route and observe. New headers, caching hints, and W3C Trace Context support make infrastructure less MCP-specific.
- Interactive calls become multi-request operations. Multi Round-Trip Requests replace connection-dependent mid-call interactions.
- Extensions become first-class. MCP Apps, already official since January 2026, is integrated into the new formal extension framework, while Tasks moves out of the core with a redesigned lifecycle.
- Roots, Sampling, and protocol Logging are deprecated. None of them are removed yet.
- Schemas, authorization, and errors are tightened. Tools gain full JSON Schema 2020-12, OAuth/OIDC behavior is hardened, and resource-not-found moves from
-32002to-32602.
Who should act first?
Use this table to triage:
Swipe to see all columns →
| You build… | What to do |
|---|---|
| A local, stateless stdio server | Upgrade deliberately; urgency is lower |
| A remote HTTP server | Audit sessions, state, headers, auth, and deployment now |
| An MCP client or host | Prioritize era detection, discovery, MRTR, auth, and caching |
| A gateway or platform | Test header validation, routing, tracing, and mixed versions |
| An experimental Tasks implementation | Treat this as a real port |
| A server using Sampling | Begin product, security, and billing planning |
| An observability product | Audit any logic that equates one JSON-RPC ID with one operation |
For a small local server, this might mean nothing more than an SDK upgrade. For a remote server built around session affinity, it's a genuine architectural migration.
Why MCP had to change
MCP began in an environment where a desktop application launched a local process and talked to it over standard input and output. In that world, a startup handshake and a connection-scoped session were cheap.
Remote MCP changed the economics.
A sessionful remote server could issue an Mcp-Session-Id, and the client returned that ID with later requests. If the server stored anything against the session, follow-up requests needed to reach the same instance or a shared session store. Horizontal scaling then meant session affinity, sticky routing, shared state, or a gateway that understood enough MCP to preserve the relationship.
Teams that leaned on protocol sessions were effectively paying a distributed-systems tax just to keep them alive. The new revision removes that tax from the core:
Swipe to see all columns →
| Area | Legacy MCP (2025-11-25 and earlier) | Modern MCP (2026-07-28 and later) |
|---|---|---|
| Connection setup | initialize handshake | No negotiation handshake |
| Protocol state | Mcp-Session-Id | Stateless requests |
| Client context | Negotiated once | Sent per request in _meta |
| Capability discovery | Initialization result | server/discover on demand |
| HTTP routing | No standard method/name routing headers | Mcp-Method and Mcp-Name |
| Mid-call interaction | Connection-dependent server requests | Multi Round-Trip Requests |
| Long-running work | Experimental core Tasks | Tasks extension |
| Feature evolution | Core specification releases | Independently versioned extensions |
The design principle is "pay-as-you-go complexity": keep the core request model lean, then add state, UI, long-running work, or other capabilities only where the application needs them.
Swipe to inspect the full diagram →
Will existing MCP servers break?
Not because the calendar reached July 28. A legacy client and legacy server can keep using their existing revision.
The break happens across eras. A modern-only client cannot talk to a legacy-only server, and a legacy-only client cannot talk to a modern-only server. Version negotiation only detects the mismatch. Bridging it is a separate job that the negotiation step doesn't do for you.
The official versioning documentation defines three categories:
- Legacy: Uses the
initializehandshake. This means2025-11-25and earlier. - Modern: Uses per-request protocol metadata. This means
2026-07-28and later. - Dual-era: Deliberately supports both styles.
Era compatibility is necessary, but it is not sufficient by itself. Two modern implementations still need at least one mutually supported modern protocol revision, and a request that depends on an extension may fail when the required capabilities do not overlap. A modern-only side and a legacy-only side cannot communicate. A dual-era implementation can bridge that era boundary, but only when it also supports a compatible revision and the capabilities required by the request.
Modern version negotiation
Every modern request declares its version. HTTP requests also carry it in MCP-Protocol-Version. If the server does not support that revision, it returns UnsupportedProtocolVersionError with the protocol revisions it does support. The client can retry a mutually supported modern revision in place. If the only compatible choice is a legacy revision, the client must switch through the transport-specific legacy flow rather than simply resending the request with modern per-request metadata.
Legacy detection is a different problem entirely
A legacy server does not understand modern per-request metadata or server/discover. It may return an implementation-defined error, stay silent, or interpret an era-ambiguous request according to the old protocol.
A client that wants to support both eras must actively detect this:
- Over stdio, probe with
server/discover. ADiscoverResultor recognized modern error means the server is modern. Any other error, or no response within a reasonable timeout, means the client should try legacyinitialize. Do not key this decision to-32601alone: legacy implementations use different errors. - Over Streamable HTTP, attempt a modern request and inspect both its HTTP status and JSON-RPC body. For modern-versus-legacy Streamable HTTP detection, inspect a
400 Bad Requestresponse before falling back. A recognized modern error, such as unsupported protocol version, missing required client capability, or header mismatch, proves the endpoint is modern and must not trigger legacy fallback. A400with an empty or unrecognized modern error body is the signal to try legacyinitialize. When additionally probing the deprecated HTTP+SSE transport,400,404, and405can trigger its separate GET fallback only when the body is not a recognized modern JSON-RPC error. Do not treat arbitrary statuses such as401,403, or429as evidence of a legacy server.
The compatibility specification recommends that the client cache the detected era for the lifetime of the stdio process or HTTP origin. If your origin fronts independently upgraded MCP services, route or version them so that this origin-level assumption remains valid.
A dual-era server performs the reverse job. A modern request selects stateless behavior. An initialize request selects legacy behavior.
If you cannot update both sides, a compatibility bridge is another transition option. Projects such as ToolFunnel emerged for this exact gap. Treat a bridge as another component to test, not as proof that every client-server pairing will work.
The practical lesson is simple:
The version field tells you what era you're in. Whether you can actually talk to the other side is something you have to build and test.
Is MCP just HTTP now?
This is the fairest criticism of the release. If remote MCP now looks like a conventional HTTP service, why not expose a normal API?
The honest answer: sometimes you should. If one application calls one known service and does not need dynamic agent integration, a traditional API is probably simpler.
MCP earns its keep when many agent hosts need to discover, understand, authorize, and operate the same capability surface. Its value sits above HTTP:
- Agent-oriented tool discovery.
- Tool, resource, and prompt schemas.
- Common host, client, and server roles.
- Authorization discovery.
- Capability and extension negotiation.
- MCP Apps.
- Tasks.
- A common integration contract that hosts can wrap in their own approval and policy controls.
MCP still solves a coordination problem raw HTTP leaves to every team to solve on its own. Making the transport layer boring is the point: it lets the coordination work happen at the protocol level instead of being reinvented per integration.
The handshake is gone. Versioning now travels in every request
In legacy MCP, the connection began like this:
Scroll code horizontally →
client → initialize
server → protocol version + capabilities + server info
client → initialized
client → tools/call + Mcp-Session-IdIn modern MCP, a request is self-contained:
Scroll code horizontally →
client → server/discover (optional for the client)
server → supported versions + capabilities
client → tools/call
protocol version + client context travel with the requestOn Streamable HTTP, a tool call looks roughly like this:
Scroll code horizontally →
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: search
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search",
"arguments": {
"query": "stateless MCP"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "example-client",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {}
}
}
}
}Use your SDK's generated types rather than copying this shape blindly. Two final-wire details matter if you built against a prerelease: clientInfo is a SHOULD rather than a MUST, and server identity moved to response _meta['io.modelcontextprotocol/serverInfo'] instead of the earlier DiscoverResult.serverInfo field. Both identities are self-reported. Do not use them for authorization or security decisions.
What replaces initialize?
Three things:
- Per-request metadata carries protocol version and client capabilities, plus recommended client identity.
server/discoverlets a client fetch the server's supported versions and capabilities when needed.- Modern version errors let a server reject an unsupported revision while naming supported alternatives.
Servers must implement server/discover. Modern clients are allowed to call another RPC first and handle a version error inline, although clients that also support legacy stdio servers should probe first to avoid ambiguous behavior.
What happens to logic wired to initialize?
Many servers used initialize for more than negotiation. Common examples include:
- Warming a cache.
- Creating a rate-limit bucket.
- Loading tenant configuration.
- Recording client information.
- Creating an application object.
- Starting an upstream connection.
Each of those needs a new home. Deployment-wide caches can initialize at process startup or lazily on first use. User-specific context should come from authenticated request state. Durable application state needs an explicit identifier. Expensive shared resources should use normal process- or application-lifecycle management rather than a protocol handshake.
The session is gone. Your application state still needs somewhere to live.
The new specification removes the Mcp-Session-Id header and the protocol-level session behind it.
That means any request can land on any compatible instance. A remote MCP server can sit behind a round-robin load balancer without session affinity, provided your application doesn't reintroduce instance-local state elsewhere.
Search your codebase for:
Scroll code horizontally →
rg 'Mcp-Session-Id|sessionIdGenerator|initialize|initialized' .Review every result. Common examples include authenticated user context, rate limits, tool-list variants, open documents, browser processes, database transactions, shopping carts, OAuth tokens, and pending interactive requests.
Some of this disappears because the protocol now carries the necessary context. Some belongs in ordinary request middleware. Some must move into durable application storage. Credential and refresh-token storage, for example, does not become stateless just because the MCP transport does.
Explicit handles make state visible
The replacement for session state is deliberately boring: return an identifier, then accept it on future calls.
Instead of hiding a cart inside the session:
sessions[mcpSessionId].basket = basket;Expose the relationship to the model:
// create_basket result
return {
content: [
{
type: "text",
text: "Created basket bsk_a1b2c3"
}
],
structuredContent: {
basket_id: "bsk_a1b2c3"
}
};Later calls pass the ID explicitly:
{
"name": "add_item",
"arguments": {
"basket_id": "bsk_a1b2c3",
"sku": "shoes"
}
}This is a tool-design pattern rather than a new protocol primitive. There is no handles/create method or universal handle type. From MCP's perspective, basket_id is an ordinary string.
Explicit state can be more expressive
A session gave each connection one implicit container, and that scope was often wrong.
Imagine an orchestrator with three research subagents. They should add purchases to the same basket, but each needs an isolated browser context. With sessions, you usually had to choose between sharing everything and isolating everything. With explicit handles, the orchestrator can share one basket_id and issue three different browser_id values.
While the handle remains valid and the backing state remains available, the model can:
- Carry a handle across tool calls.
- Hand it to a subagent.
- Resume work in a later conversation.
- Keep two documents or browser contexts active at once.
- Reason explicitly about which state a tool will modify.
Hidden transport state never let the model carry those relationships to another client or subagent.
Secure handles like capabilities, not secrets
Handles will appear in transcripts, prompts, logs, copy-paste buffers, and subagent context.
The MCP specification does not prescribe handle entropy, lifetime, or authorization semantics. Those are application-security decisions. A defensible implementation should:
- Make handles opaque and unguessable.
- Bind each handle to the authenticated principal and tenant.
- Re-authorize the handle on every call.
- Document its lifetime in the tool description.
- Return a recoverable error when it expires.
- Use at least 128 bits of secure entropy if an unauthenticated handle necessarily acts as a bearer token.
- Avoid embedding user IDs, environment names, or internal topology in the value.
A handle should never be treated as proof that the caller is allowed to use it. Re-authorize on every call.
Retry safety is still your job
Going stateless removes the session, but a mutation still needs its own protection against being applied twice.
This was one of the clearest production warnings in the r/mcp discussion. Suppose a client calls publish_post. The server commits the write, but the HTTP response is lost. The client retries. Without deduplication, the post is published twice.
Sessions never guaranteed idempotency, but removing implicit connection context makes explicit retry semantics impossible to ignore. Idempotency keys are an application-level reliability pattern, not an MCP field mandated by the specification.
For irreversible tools:
- Accept or derive an idempotency key.
- Scope it to the authenticated principal and operation.
- Store the result when the operation commits.
- Return the stored result when the same key is repeated.
- Retain the key for a duration appropriate to the action.
- Test timeout-after-commit behavior.
- Apply replay protection to MRTR continuations and Task updates.
An example tool input might be:
Scroll code horizontally →
{
"name": "publish_post",
"arguments": {
"draft_id": "draft_72f9",
"idempotency_key": "publish_draft_72f9_v3"
}
}The model does not need to invent random keys. Your host, client, or server can generate them according to the workflow. What matters is that duplicate delivery and duplicate execution are treated as different events.
One tool call can now span multiple requests
A stateless protocol still needs a way for a server to ask for input while fulfilling a tool call.
The generic server-to-client JSON-RPC request channel is gone, along with the old long-lived GET SSE pattern. Streamable HTTP can still return a request-scoped SSE response, and extensions can define subscription mechanisms. If you relied on the old channel for elicitation, Sampling, Roots, or free-floating server requests, this is an application migration, not a transport rename.
In the new model, server-initiated requests must be associated with an active client request. Multi Round-Trip Requests (MRTR) carry the interaction across independent requests.
The flow is:
Swipe to inspect the full diagram →
One logical operation spans two independent JSON-RPC requests. Observability should correlate the operation, not only the request IDs.
One operation can span several request IDs, and that quietly breaks observability
That breaks a common observability assumption.
If a tool call returns input_required after 10 milliseconds, the user takes five seconds to approve it, and the retry completes in one second, the logical operation took roughly six seconds.
An observability tool that groups only by JSON-RPC ID may report:
- Two tool calls instead of one.
- Roughly one second of measured work.
- No record of the five-second human decision.
- Inflated invocation and billing counts.
Not every MCP observability tool is broken by this. The failure is narrower than that:
MRTR breaks tools that equate one JSON-RPC ID with one logical operation.
The robust approach is to mint a logical-operation identifier in the client or host's telemetry and carry the same trace across retries. Exact requestState equality can be a secondary correlation signal when present, but the value is opaque: do not parse it, make assumptions about its contents, or record the raw value in logs. If multiple in-flight operations could match, leave them unlinked rather than inventing a false relationship.
On the server, bind continuation state to the principal, operation, and expiry. Protect it against tampering and replay.
Track at least three durations:
- Server-processing time.
- Time waiting for external or human input.
- End-to-end logical-operation time.
Tasks introduce the same deeper shift. A tools/call can return a task handle, while the actual result arrives through later requests. The operation and the request are no longer synonymous.
Streaming and notifications change too
The standalone HTTP GET stream is removed. Clients that want long-lived change notifications now send subscriptions/listen, whose POST response remains open as an SSE stream. The client selects the notification categories it wants, and the server acknowledges the subscription and tags delivered notifications with io.modelcontextprotocol/subscriptionId.
This replaces resources/subscribe and resources/unsubscribe as well as the old GET stream. It does not absorb request-scoped traffic: notifications/progress and notifications/message still travel only on the response stream of the request they describe.
SSE resumability is also gone. Last-Event-ID and SSE event IDs no longer provide MCP message redelivery. If a request's response stream breaks, its in-flight result is lost, and a client must issue a new request with a new JSON-RPC ID. That is another reason mutation tools need application-level idempotency.
Cancellation is transport-specific. A modern Streamable HTTP client cancels an in-flight request by closing its response stream, while a stdio client sends notifications/cancelled.
Infrastructure gets simpler
HTTP routing no longer needs to parse JSON
For JSON-RPC requests, Streamable HTTP requires:
Mcp-Methodon every request, mirroring the JSON-RPC method.Mcp-Nameontools/call,resources/read, andprompts/get, mirroringparams.nameorparams.uri. Extensions may define additional uses.
The core revision defines no client-to-server notifications over Streamable HTTP, and it does not define metadata-header requirements for extension notification POSTs. Do not generalize the request-header rules to notifications without reading the relevant extension.
This allows infrastructure to:
- Route tool calls independently of discovery calls.
- Apply per-tool rate limits.
- Record operation-level metrics.
- Enforce policies without parsing arbitrary JSON bodies.
It also creates a new validation boundary. Servers must reject a request when the routing headers disagree with the body. They must also reject a request when MCP-Protocol-Version disagrees with io.modelcontextprotocol/protocolVersion in _meta. These validation failures use HTTP 400 and the MCP HeaderMismatch error (-32020). A gateway that authorizes tools/list from the header while the backend executes tools/call from the body is vulnerable to a protocol-confusion bypass.
Tools can also mark eligible string, boolean, or safe-integer input properties with x-mcp-header, subject to the schema's declaration, placement, naming, and uniqueness constraints. Conforming HTTP clients then mirror those values into Mcp-Param-* headers. The headers mirror the body, and every component that processes the body must reject disagreement rather than silently preferring either representation. Servers that recognize these headers must decode them and reject missing or mismatched values. This feature is optional for servers but mandatory for clients to support when a tool declares it.
Audit the entire request path: CDN, WAF, API gateway, reverse proxy, load balancer, service mesh, and application server. Confirm that custom Mcp-* headers survive every hop. Header names are case-insensitive, but their MCP values are case-sensitive, so a proxy that normalizes casing can silently corrupt them. Let the SDK handle the specified encoding for non-ASCII or otherwise unsafe Mcp-Param-* values.
Also review where headers are logged. They routinely end up in infrastructure logs, so do not mark secrets or sensitive personal data for header mirroring. Streamable HTTP servers must validate Origin, and local servers should bind to loopback rather than 0.0.0.0. If you serve request-scoped SSE through a buffering proxy, follow the transport guidance (for example, X-Accel-Buffering: no with Nginx-compatible proxies) to prevent delayed event delivery.
Tool-list caching becomes explicit
Completed results from tools/list, prompts/list, resources/list, resources/templates/list, and resources/read carry ttlMs and cacheScope. server/discover also supports these fields. ttlMs is a freshness hint, and cacheScope is either public or private. Interim input_required results and MRTR retries are not cacheable.
This matters more than it sounds. Under the session model, a client could not safely assume that a tools/list result from one session applied to another. Modern list results no longer vary by connection, so an orchestrator spawning multiple subagents can avoid fetching the same catalog for every subagent when request parameters, authorization context, and cache scope allow reuse.
For server authors:
- Return tools in deterministic order.
- Use
privateunless the response is safe to reuse across authorization contexts. - Do not mark a user-specific catalog as publicly shareable.
- Treat TTL as a freshness hint, not proof that downstream permissions have not changed.
- Preserve explicit invalidation signals where supported.
For client authors:
- Key private caches by endpoint, protocol era, authorization context, request parameters, and pagination cursor.
- Do not reuse private catalogs across users.
- Treat each paginated page as an independently cacheable result.
- Revalidate when the server or authentication context changes.
- Measure token and latency savings rather than assuming more caching is always better.
Tool catalogs occupy model context. Caching avoids wire traffic, while smaller, better-designed tool surfaces reduce catalog tokens and may improve tool selection. Measure the effect with the clients and models your users actually run.
Tracing gets a shared convention
MCP now documents W3C Trace Context propagation in _meta, standardizing:
traceparenttracestatebaggage
That gives SDKs, gateways, servers, and downstream services a common way to place one agent action into a single OpenTelemetry-compatible trace. Don't use baggage as a dumping ground: bound its size, allowlist keys, and avoid copying credentials or sensitive user data into it.
Extensions keep the core small
MCP now has a formal extension framework. Extensions use namespaced identifiers, are negotiated through capabilities, version independently, and can define their own fallback behavior. That lets MCP add interactive UI, long-running work, and future capabilities without expanding the core every time.
Two official extensions are central to this release: MCP Apps and Tasks.
MCP Apps joins the formal extension framework
MCP Apps was announced as the first official MCP extension on January 26, 2026. The July revision integrates it into MCP's new formal extension framework. MCP Apps lets a server provide an interactive HTML interface that the host renders in a sandboxed iframe. A tool can declare its UI template in advance, allowing the host to prefetch and review it before execution. The App communicates with the host through an Apps-specific JSON-RPC dialect over postMessage. When the App requests a tool call, the host can mediate that request through the normal MCP path and apply its audit, consent, and approval controls.
For developers, the extension model clarifies how support is advertised and negotiated. It does not make browser security disappear.
Treat App content as untrusted:
- Sanitize tool-generated HTML and user-controlled content.
- Preserve iframe sandbox restrictions.
- Restrict outbound network access.
- Avoid leaking host secrets into the App.
- Defend against stored cross-site scripting.
- Consider phishing inside an otherwise trusted agent interface.
A sandbox limits the blast radius, but you still need output encoding and content security on top of it.
Tasks moves out of the core, and changes shape
Tasks shipped experimentally in 2025-11-25. Production experience led to a redesigned lifecycle that now lives in an extension.
In the new model:
- The client advertises support for the Tasks extension.
- A server can decide that a
tools/callshould continue as a Task. - The call returns a task handle.
- The client drives the lifecycle using
tasks/get,tasks/update, andtasks/cancel. tasks/listis removed.
The extension identifier is io.modelcontextprotocol/tasks. A server must not return a task unless the client declared support, and it must durably create the task before returning its handle. Clients can poll, or listen for optional task notifications through subscriptions/listen. Cancellation is cooperative: acknowledging tasks/cancel does not guarantee that the task will end in cancelled.
Clients should respect pollIntervalMs and persist task IDs if work must survive a client restart. Over Streamable HTTP, tasks/get, tasks/update, and tasks/cancel must set Mcp-Name to params.taskId. That makes explicit task state routable without reviving an implicit MCP session.
If you implemented the experimental API, this is a real port rather than an import rename.
Search for:
Scroll code horizontally →
rg 'tasks/result|tasks/list|notifications/tasks/status|CreateTaskResult|related-task' .Long-running work also needs application controls:
- Per-user and global concurrency limits.
- Cancellation.
- Expiry and cleanup.
- Result-retention policy.
- Idempotent creation.
- Replay protection.
- Authorization on every status or update call.
- Resource quotas after the initiating client disconnects.
Task creation may be cheap for the caller and expensive for the server. Without quotas, asynchronous work becomes a denial-of-service primitive.
Roots, Sampling, and Logging are deprecated
The new lifecycle policy formally deprecates three core features:
Swipe to see all columns →
| Deprecated feature | Previous role | Recommended direction |
|---|---|---|
| Roots | Client supplies session-scoped operating boundaries | Tool arguments, resource URIs, or server configuration |
| Sampling | Server asks the client's model to generate | Direct provider integration or explicit input workflows |
| Protocol Logging | Server streams structured logs to the client | stderr for stdio; OpenTelemetry for operations |
The older HTTP+SSE transport is also formally Deprecated. New remote implementations should use Streamable HTTP. DCR and two Sampling includeContext values have narrower deprecations discussed below.
Deprecated features stay functional for a defined window rather than disappearing on release day. Roots, Sampling, Logging, and Dynamic Client Registration have earliest removal dates in the first specification revision released on or after July 28, 2027, and actual removal would require a later specification change. The already-deprecated HTTP+SSE transport is a transitional exception: its registry entry gives an earliest removal of three months after SEP-2596 reaches Final rather than a new twelve-month window. Use the Deprecated Features registry as the authoritative source for each feature's earliest removal.
That timeline covers the features themselves. It says nothing about how long any given vendor keeps supporting a historical protocol revision, which is a separate, per-vendor decision.
The wire flow also changes even while a feature stays available. In the modern era, Roots and Sampling requests travel inside InputRequiredResult and return through MRTR. Logging verbosity is requested per call with io.modelcontextprotocol/logLevel, and log notifications are confined to that request's response stream.
Do not confuse those deprecated features with messages that this revision actually removes. ping, logging/setLevel, notifications/roots/list_changed, resources/subscribe, resources/unsubscribe, and notifications/elicitation/complete are gone from the modern protocol. URL-mode elicitation also loses elicitationId. Correlate continuation state through requestState instead.
Replacing Roots
Pass the required scope explicitly:
- A working directory as a tool argument.
- A resource URI identifying the target.
- A server-side deployment configuration.
Do not treat a client-supplied path as authorization. Validate it against server-side policy.
Replacing Sampling
Sampling is the most architecturally expensive deprecation, and the most debated one.
The disagreement in r/mcp is understandable. Direct provider calls give server authors control over model choice and evaluation. But they also move credentials, inference cost, data handling, and enterprise policy onto the server operator.
A server that previously borrowed the client's model may now need to:
- Hold provider credentials.
- Select and evaluate a model.
- Pay for inference.
- Become a processor of additional user data.
- Honor enterprise model and residency policy.
Treat this as a product, security, and billing decision rather than a mechanical migration. Where the server merely needs user or client input, an explicit MRTR flow may be the better replacement.
If you keep Sampling during the deprecation window, omit includeContext or use "none". The old "thisServer" and "allServers" values are themselves deprecated.
Replacing protocol Logging
Use:
stderrfor local stdio server diagnostics.- OpenTelemetry for structured production traces and metrics.
- Your normal centralized logging system for operator logs.
Protocol logs and operator logs serve different audiences. A remote client that previously displayed server-originated structured messages may lose that exact experience, even if the operations team gains a better observability pipeline.
Other wire-level changes
Results gain a discriminator
Every modern result requires resultType to distinguish a completed response from one that needs more input. Core values include complete and input_required. Extensions can add values such as task. When a client is intentionally speaking to an earlier-protocol server, a missing discriminator is treated as complete.
Do not assume your SDK exposes this field. The TypeScript v2 SDK consumes wire-only bookkeeping before returning its public result types. Inspecting result.resultType in application code is therefore not portable.
Tool schemas move to full JSON Schema 2020-12
Tool inputSchema and outputSchema now support full JSON Schema 2020-12. Input schemas still require an object at the root, but can now use oneOf, anyOf, allOf, if/then/else, $ref, and $defs. Output schemas are not restricted to an object, and structuredContent can be any JSON value.
This improves validation and expressiveness, but it creates new implementation duties:
- Bound schema depth and validation time.
- Do not automatically dereference arbitrary external
$refURIs. - Test complex schemas across the actual hosts and models your users run.
- Prefer a simple tool interface when it expresses the job clearly.
A schema can be technically valid and still be difficult for a model to use.
The resource-not-found error code changes
The missing-resource error changes from MCP's custom -32002 to the standard JSON-RPC -32602 Invalid Params code.
Search clients, servers, fixtures, and test assertions:
rg -- '-32002' .Prefer semantic error handling over magic-number branching where your SDK makes that possible.
Protocol errors get allocated ranges
The specification now reserves -32020 through -32099 for MCP-defined errors. -32000 through -32019 remains implementation-defined. The new assignments include:
-32020:HeaderMismatch-32021:MissingRequiredClientCapability-32022:UnsupportedProtocolVersion
Earlier 2026 prereleases used different numbers. Update hard-coded alpha literals and prefer SDK error constants.
Authorization gets stricter
Authorization remains optional for deployments that do not need it. The specification's OAuth framework targets HTTP. Stdio servers should normally obtain credentials through their process environment. For protected remote servers, the basic model is unchanged: the MCP server is a protected resource, an authorization server issues tokens, and the client discovers that relationship through metadata before sending the user through a browser-based OAuth flow.
The new revision tightens the details:
- Clients record the selected authorization server's validated issuer and compare any authorization-response
isswith it using exact string comparison. Absence is currently fatal only when the server advertisedauthorization_response_iss_parameter_supported: true. A future revision is expected to requireiss. - Pre-registered credentials and credentials obtained through Dynamic Client Registration are bound to the authorization server's issuer. They cannot be silently reused when a resource changes authorization servers. URL-based CIMD client IDs are portable and do not require re-registration.
- OpenID Connect
application_typeis declared during Dynamic Client Registration. - Refresh-token guidance is clarified.
- Scope accumulation during step-up authorization is clarified.
- Well-known discovery behavior is clarified.
For server authors, the practical checklist is:
- Publish accurate RFC 9728 Protected Resource Metadata.
- Return a correct
WWW-Authenticatechallenge. - Keep authorization-server issuer identifiers stable.
- Validate the access token's audience.
- Bind tool authorization to the authenticated principal.
- Do not trust identity, tenant, or role claims supplied in arbitrary
_meta. - Apply size limits and namespace rules to custom
_meta. - Test issuer and redirect mix-up defenses.
- Test the client-registration paths you advertise.
CIMD, preregistration, and DCR
MCP deployments can obtain a client identity through preregistered credentials, Client ID Metadata Documents (CIMD), or Dynamic Client Registration (DCR), depending on what the client and authorization server support.
CIMD is particularly useful for clients and servers with no prior relationship: the client ID is a URL pointing to a metadata document the authorization server can read, which avoids a separate registration mutation at every authorization server.
DCR remains available for backward compatibility, but it is deprecated. New implementations should prefer preregistration when the parties already have a relationship and CIMD when they do not. Use DCR only as a supported fallback or for a specific deployment requirement.
Authorization remains one of the least interoperable parts of remote MCP. Test it against real clients instead of stopping after a successful token request in one development harness.
New security boundaries to review
The stateless design makes more context explicit and routable. That improves operability, but it also creates a useful five-point threat-model review:
Swipe to see all columns →
| Boundary | Failure mode | Minimum control |
|---|---|---|
| Handles and continuation state | Guessing or replay crosses users, tenants, or operations | Bind state to principal, tenant, operation, and expiry; re-authorize on use; integrity-protect requestState where tampering matters |
Client-supplied _meta | Claimed identity or tenant data is mistaken for authenticated context | Treat metadata as untrusted input and derive trusted identity from the authorization layer |
| Mirrored HTTP headers | A gateway authorizes one operation while the backend executes another | Validate required headers against the body at the component that executes the request; reject mismatches |
| MCP Apps content | Stored XSS, phishing, or data exfiltration occurs inside an apparently trusted host | Preserve sandboxing, sanitize rendered content, and restrict network and secret access |
| Tasks | Cheap requests create expensive work that outlives the caller | Enforce authorization, quotas, idempotent creation, expiry, cancellation, and cleanup |
As a concrete application-security floor, use at least 128 bits of secure entropy if an unauthenticated handle necessarily acts as a bearer token.
The specification itself didn't introduce five new vulnerabilities, and most of the controls above are application-security decisions rather than protocol mandates. These are simply the boundaries most likely to become bugs when teams translate "stateless" into "nothing needs ownership, integrity, or lifecycle checks."
Migrating the SDKs: do not assume the defaults
The TypeScript, Python, Go, and C# SDKs ship on different schedules and expose different APIs. A new major version also does not guarantee that 2026-07-28 is on the wire. In TypeScript v2, for example, modern behavior is opt-in. A default Client.connect() can still perform the legacy handshake.
Swipe to see all columns →
| SDK | Review before upgrading |
|---|---|
| TypeScript | Package split, ESM-first packaging with a CommonJS build, era configuration, transport renames, request context, MRTR, Tasks, and initialization hooks |
| Python | Version-specific imports, transport configuration, per-request context, session helpers, deprecated features, Tasks, and protocol-revision settings |
| Go and C# | Release or preview status and explicit protocol-version/stateless-mode options; do not assume another SDK's defaults apply |
Pin exact SDK versions during the migration. If you shipped against a pre-final 2026 alpha, audit hard-coded protocol error numbers and wire-only result fields. Prefer SDK constants and public result types.
Choose a rollout track
- New server: Build on the modern request model. Add legacy support only when your clients need it.
- Existing server: Upgrade in a branch, add dual-era tests, and validate the complete HTTP path in staging.
- Critical production server: Preserve legacy behavior, canary modern traffic, and measure failures by client and protocol era before removing old routing or storage.
The copy-and-paste migration checklist
Drop this into your issue tracker and remove the items that do not apply.
Protocol and application state
- Search for
Mcp-Session-Id. - Find everything keyed by session ID.
- Separate protocol context from application state.
- Replace cross-call state with explicit handles.
- Bind handles to principal and tenant.
- Add expiration and recoverable errors.
- Implement or verify
server/discover. - Include the required protocol version and per-request metadata.
- Move initialization hooks to the correct application lifecycle.
Compatibility
- Decide whether the client is modern-only or dual-era.
- Decide whether the server is modern-only or dual-era.
- Test modern client → modern server.
- Test dual-era client → legacy server.
- Test legacy client → dual-era server.
- Produce an actionable error for unsupported eras.
- Cache era detection correctly.
HTTP infrastructure
- Preserve
Mcp-MethodandMcp-Name. - Support declared
x-mcp-headerparameters and preserveMcp-Param-*. - Reject header/body mismatches.
- Reject disagreement between the protocol-version header and
_meta. - Confirm
MCP-Protocol-Versionreaches the application. - Send both
application/jsonandtext/event-streamin the HTTPAcceptheader. - Validate
Originand review local bind addresses. - Replace GET streams and resource subscriptions with
subscriptions/listen. - Remove assumptions about
Last-Event-IDor SSE message redelivery. - Implement cancellation correctly for each transport.
- Test CDN, WAF, proxy, load balancer, and service mesh behavior.
- Review logs for sensitive header capture.
- Remove sticky routing only after application-state review.
Reliability and observability
- Add idempotency for irreversible tools.
- Test timeout-after-commit retries.
- Correlate MRTR at the operation level.
- Separate processing, waiting, and end-to-end latency.
- Propagate W3C Trace Context safely.
- Update metrics that count JSON-RPC IDs as tool calls.
Protocol features
- Port experimental Tasks usage.
- Verify task durability, polling intervals, task-ID persistence, cooperative cancellation, and optional subscriptions.
- Set
Mcp-Nameto the task ID on Streamable HTTP Task methods. - Replace or plan replacement of Roots.
- Replace or plan replacement of Sampling.
- Move protocol Logging to appropriate observability channels.
- Remove calls to methods and notifications deleted from the modern protocol.
- Add and honor
ttlMs/cacheScope. - Handle
resultTypeat the wire layer without assuming it appears in public SDK results. - Test JSON Schema 2020-12 behavior.
- Update
-32002assertions to-32602.
Authorization and security
- Publish RFC 9728 Protected Resource Metadata.
- Validate token audience.
- Bind stored credentials to the correct issuer and apply the metadata-dependent authorization-response
issrules. - Test supported client-registration methods.
- Treat
_metaas untrusted input. - Prevent cross-tenant handle use.
- Protect continuation state against tampering and replay.
- Add Tasks quotas, cancellation, and expiry.
- Security-test MCP Apps.
SDK and deployment
- Read the migration guide for your language.
- Opt in to
2026-07-28explicitly where required. - Pin exact SDK versions during rollout.
- Run conformance scenarios.
- Test against the real clients your users run.
- Canary modern traffic before removing legacy support.
Test the migration at four levels
- Unit-test application behavior. Cover handle ownership and expiry, idempotency, cache isolation, replay protection, Task quotas, header/body mismatches, and authorization.
- Run official conformance tests. The MCP conformance framework catches wire-level deviations before they become interoperability bugs.
- Probe the deployed endpoint. Tools such as
mcp-spec-checkcheck externally visible behavior including discovery, routing headers, and session independence. - Test real clients and models. Protocol correctness does not tell you whether a client chooses the right tool, a model understands your schema, an extension is supported, or OAuth works across hosts.
That fourth layer is where MCPJam fits. Use it to inspect JSON-RPC, exercise tools and auth, test Apps and Tasks, and run model-in-the-loop evals against real workflows.
Start the inspector with:
npx -y @mcpjam/inspector@latestIn the raw protocol view, verify the selected era, server/discover versus initialize, per-request _meta, HTTP headers, MRTR continuations, Task messages, OAuth flows, and cache metadata. Then run evals: a transport migration should not quietly reduce tool-selection accuracy or change how models use your results.
MCP is becoming boring infrastructure
MCP 2026-07-28 is important because it removes machinery.
Remote servers become easier to route and scale. Tool catalogs become cacheable. Traces can cross the agent boundary. Optional capabilities can evolve as extensions instead of expanding the core.
But the complexity does not vanish. It moves to places where developers can see and control it:
- Application state becomes explicit handles.
- Retry safety becomes idempotency.
- Compatibility becomes a deliberate dual-era decision.
- Interactive calls become multi-request operations.
- Authorization and state integrity become application responsibilities.
Audit where your implementation depends on the old connection model, test both eras, and migrate while compatibility remains a choice rather than an emergency.