MCP 2026-07-28 Spec Changes and Migration
This document organizes the changes in the 2026-07-28 revision and collects the material for deciding how to migrate existing MCP servers and clients. For wire format details, see MCP Protocol Basics; for transport details, see Transports; for the big picture, see Introduction to MCP - What Is the Model Context Protocol.
1. What This Document Covers
- Major changes
- Minor changes
- Deprecated features
- The feature lifecycle
- How to migrate
- Checking compatibility
- Migration checklist
Major Changes
The changelog lists the following nine major changes.
| Change | Content | SEP |
|---|---|---|
| Sessions removed | Removes protocol-level sessions and the Mcp-Session-Id header from Streamable HTTP. tools/list, resources/list, and prompts/list no longer vary per connection. State that spans multiple calls uses handles minted by the server and passed as ordinary tool arguments | SEP-2567 |
| Statelessness | Removes initialize / notifications/initialized. Every request carries its version and capabilities in _meta. Clients are recommended to identify themselves with clientInfo and servers with serverInfo in results. A version mismatch returns UnsupportedProtocolVersionError | SEP-2575 |
server/discover | An RPC that servers must implement. Returns supported versions, capabilities, and identity. Clients call it optionally. On stdio it serves as the backward-compatibility probe | SEP-2575 |
subscriptions/listen | Replaces the HTTP GET endpoint and resources/subscribe / resources/unsubscribe. A long-lived POST response stream delivers only the notification types the client opted in to | SEP-2575 |
ping and others removed | Removes ping, logging/setLevel, and notifications/roots/list_changed. The log level is specified per request with logLevel in _meta | SEP-2575 |
| Tasks as an extension | Moves experimental Tasks out of the core into the official extension io.modelcontextprotocol/tasks. Replaces the blocking tasks/result with polling via tasks/get, adds tasks/update for client-to-server input, and removes tasks/list. Servers can return task handles without per-request opt-in | SEP-2663 |
| Multi Round-Trip Requests (MRTR) | Replaces server-initiated requests (roots/list, sampling/createMessage, elicitation/create). The server returns an InputRequiredResult with resultType: "input_required", and the client retries the original request with inputResponses | SEP-2322 |
resultType | Required in every result. "complete" or "input_required". A result from an older server that omits it is treated as "complete" | SEP-2322 |
| SSE resumption removed | Removes resumption and redelivery via Last-Event-ID and SSE event IDs from Streamable HTTP. When a stream breaks, the in-flight request is lost, and the client reissues it with a new request ID | SEP-2575 |
Minor Changes
extensionsfield: added toClientCapabilitiesandServerCapabilities, allowing negotiation of optional extensions beyond the core- OpenTelemetry trace context: documents conventions for the
_metakeystraceparent,tracestate, andbaggage(SEP-414) tools/listordering: servers are recommended to return tools in a deterministic order, for client-side caching and a better LLM prompt cache hit rateMcp-Method/Mcp-Nameheaders: required on Streamable HTTP POSTs.x-mcp-header, which turns tool arguments into headers, is also added (SEP-2243)ttlMs/cacheScope: required in the results oftools/list,prompts/list,resources/list,resources/read, andresources/templates/list.ttlMsis the number of milliseconds a result can be considered fresh, andcacheScope(publicorprivate) specifies whether shared intermediaries may cache it (SEP-2549)- Resource not found code: from
-32002to-32602 issparameter validation: authorization servers are recommended to includeissin authorization responses (RFC 9207), and clients must check a presentissagainst the recorded issuer before redeeming the authorization code (SEP-2468)application_type: specifying an appropriate value is now required in Dynamic Client Registration (SEP-837)- Binding credentials to the issuer: clients key persisted credentials by the issuer identifier, must not reuse them with a different authorization server, and re-register when the authorization server changes (SEP-2352)
- JSON Schema:
inputSchemaandoutputSchemaallow any JSON Schema 2020-12 keywords, andstructuredContentcan be any JSON value. Adds$refresolution requirements and resource bounds for composition keywords (SEP-2106) - URL-mode Elicitation: removes
notifications/elicitation/completeandelicitationId, because under MRTR the client learns the outcome by retrying the original request. If correlation is needed, the server puts its own identifier inrequestState - Error code allocation policy:
-32000to-32019remains implementation-defined, and-32020to-32099is reserved for the specification. The codes introduced in the draft were renumbered:HeaderMismatchto-32020,MissingRequiredClientCapabilityto-32021, andUnsupportedProtocolVersionto-32022
For details on the authorization changes, see MCP Authorization - OAuth 2.1 and Metadata Discovery and MCP Authorization Extensions and Enterprise Adoption.
Deprecated Features
Deprecated features remain in the specification but are scheduled for removal. New implementations should not adopt them.
| Deprecated feature | Migrate to | Deprecated in | Earliest removal |
|---|---|---|---|
| Roots | Pass directories or files via tool parameters, resource URIs, or server configuration | 2026-07-28 (SEP-2577) | First revision released on or after 2027-07-28 |
| Sampling | Integrate directly with LLM provider APIs | 2026-07-28 (SEP-2577) | Same as above |
| Logging | Log to stderr for stdio, or use OpenTelemetry | 2026-07-28 (SEP-2577) | Same as above |
| Dynamic Client Registration (RFC 7591) | Client ID Metadata Documents (DCR remains for compatibility with authorization servers that do not support them) | 2026-07-28 (PR #2858) | First revision released on or after 2027-07-28 |
includeContext values "thisServer" / "allServers" | Omit the field or use "none" | 2025-11-25 (reclassified by SEP-2596) | At the same time as Sampling removal, or earlier |
| HTTP+SSE transport | Streamable HTTP | 2025-03-26 (reclassified by SEP-2596) | In the registry: three months after SEP-2596 reaches Final |
Deprecated features work fully during the migration window. There is no reason to newly implement Roots / Sampling / Logging, though. The mechanism for receiving Roots / Sampling input through MRTR is covered in Elicitation and Multi Round-Trip Requests.
The Feature Lifecycle
SEP-2596 adopted a policy for managing the lifetime of individual specification features. A feature is in one of three states.
| State | Meaning | Expectation for implementers |
|---|---|---|
| Active | A feature in the Current revision | Implement per the specification's requirements |
| Deprecated | Remains in the specification but is scheduled for removal. A migration path is documented | New implementations do not adopt it. Existing implementations migrate before the earliest removal |
| Removed | Removed from the next Current revision. It remains in the last Final revision it appeared in | Implementations targeting the next revision must not depend on the feature |
The key points of the policy are as follows.
- Deprecating a feature requires a SEP, which states the rationale, the migration path, and a minimum deprecation window of at least 12 months. The window is counted from the release of the revision in which the deprecation first appears
- A deprecated feature is recorded in the schema's
@deprecatedtag, a notice in the specification prose, the "Deprecated" section of the changelog, and the registry of deprecated features - Removal is decided by the Core Maintainers during release preparation after the window has elapsed. The earliest removal is when the feature becomes eligible for removal, not a promise to remove it. A feature may stay deprecated for much longer
- When there is an active security risk, the 12 months can be shortened, but at least 90 days between deprecation and earliest removal is still guaranteed
- HTTP+SSE and
includeContext, which the specification already described as deprecated, were classified as Deprecated by the SEP-2596 transition rule. The 12 months are not applied again: the earliest removal of HTTP+SSE is three months after SEP-2596 reaches Final, and the twoincludeContextvalues follow the removal of Sampling and are removed no later than Sampling itself - A deprecation can be reverted to Active by a SEP
- Removal from the specification does not oblige an SDK to drop the feature immediately. It follows the SDK's own revision-support policy. Tier 1 SDKs mark the API for deprecated features as deprecated using the language's native mechanism
As of 2026-07-28, nothing has been removed yet.
How to Migrate
The skeleton of moving an existing server to 2026-07-28 is: "run a new, strictly stateless route alongside the existing session-based route, move features over, and remove the old route after the sessions are gone." Proceed as follows.
- Find session dependencies: look for places that depend on protocol sessions (see "Migration Checklist" below). If there are none, you can move directly to a stateless implementation
- Add a stateless route: leave the old route and add a new one. The new route processes each request from
_metaand HTTP headers alone - Dispatch old and new: to accept old and new clients at the same URL, dispatch by the shape of the request. A request with
_metais modern, and one starting withinitializeis legacy. A dual-era server can serve both at the same time on the same endpoint - Replace dependencies: rebuild session-dependent features with stateless designs, as in the table below
- Move clients: move clients to the new route in turn
- Drain and remove: monitor traffic to the old route, and remove it after existing sessions have expired
| Session-dependent feature | Stateless design |
|---|---|
| Data tied to an MCP session | Store it behind an explicit boundary (such as a database) and reference it with an authenticated, server-issued handle |
| State of a multi-step interaction | Put an integrity-protected requestState in input_required. Bind it to the user, the original method and parameters, and an expiry |
| Server-pushed Elicitation, Sampling, Roots | Return input_required, and the client retries with responses attached |
| A standalone list-change stream (GET) | Deliver through subscriptions/listen. The client reopens it when it ends |
| Session replay and transport recovery | Make each request independently recoverable, and keep progress in application storage |
This table and procedure are an organization based on the strategy in the Cloudflare Agents SDK v2 migration guide. The routing API differs by SDK, so check the migration guide of the SDK you use for the actual steps. requestState is a value that passes through the client, so treat it as untrusted input and verify it with a signature and a short expiry.
Checking Compatibility
Before and after migration, actually try the following combinations. For the overall outcome per era, see the compatibility table in MCP Protocol Basics.
- Modern client x your server: do
server/discoverand normal requests go through? When you send an unsupported version, doesUnsupportedProtocolVersionError(HTTP 400) come back with asupportedlist? - Legacy client x your server: if you keep old clients, can the same endpoint answer
initialize? For a modern-only server, does the error toinitializeinclude the supported versions? - Modern client x old server: on stdio, does the
server/discoverprobe end in failure or a timeout and switch toinitialize? On HTTP, does it look at the body of the 400 and switch? - Does a modern server respond to
Mcp-Session-Id,Last-Event-ID, GET, and DELETE sent by old clients by ignoring them or with405? - Do
MCP-Protocol-Version,Mcp-Method,Mcp-Name, andMcp-Param-*arrive intact through proxies, gateways, and WAFs? - Check the migration guide and release notes of the SDK you use. Pin the SDK version and track deprecated APIs and removal plans
Migration Checklist
Find dependencies on sessions and server-initiated features from these angles.
- Protocol sessions: you issue
Mcp-Session-Id, or keep data in memory or storage per session - Server-initiated requests: you send
elicitation/create,sampling/createMessage, orroots/listdirectly from the server - Standalone streams: you deliver notifications on a GET endpoint
- Resumption: you depend on redelivery via
Last-Event-IDor an event store - Processing that depends on the
initializeresponse: saving capabilities at initialization, recording the version, and so on - Sending or receiving
ping,logging/setLevel,notifications/roots/list_changed - List caching:
*/listresults that do not returnttlMs/cacheScope - Deprecated features: are you newly using Roots, Sampling, Logging, HTTP+SSE, or Dynamic Client Registration?
- Authorization:
issvalidation, binding credentials to the issuer,application_type
2. Deprecated Features and Migration Paths
| Deprecated feature | Migrate to |
|---|---|
| Roots | Pass directories or files via tool parameters, resource URIs, or server configuration |
| Sampling | Integrate directly with LLM provider APIs |
| Logging | Log to stderr for stdio, or use OpenTelemetry |
| HTTP+SSE transport | Streamable HTTP |
| Dynamic Client Registration (RFC 7591) | Client ID Metadata Documents (DCR remains for compatibility) |
3. Common Pitfalls for Developers
The Difference Between "Deprecated" and "Removed"
Roots, Sampling, and Logging are deprecated, not removed, in 2026-07-28. They work, and SDK APIs remain. On the other hand, ping, logging/setLevel, notifications/roots/list_changed, resources/subscribe, and initialize have already been removed. If you mix these up and keep using a removed method thinking it is only deprecated, a 2026-07-28 server will reject it.
Reading the Earliest Removal as a Deadline
"On or after 2027-07-28" is when removal becomes allowed. It is not the date removal was decided. Even so, the safe policy is not to migrate at the end of the window but to avoid them from the start in new implementations.
Forgetting to Change How You Log
logging/setLevel was removed, and the log level is specified per request in _meta. The server must not emit notifications/message for a request without logLevel. Logs from a stdio server go to stderr (stdout is reserved for the protocol; see Operating Local MCP Servers Safely).
Trying to Resume a Dropped Stream
Resumption via Last-Event-ID is gone. When a stream breaks, the in-flight request is lost. The client reissues the same content with a new request ID. So design tools to be safe to run again (idempotent). A subscription (subscriptions/listen) is also reopened by the client when it breaks, and changes that were missed are made up by refetching.
When Replacing Session-Based Features with MRTR
When you replace server-side Elicitation with input_required, the client retries the original request. No state remains in server memory between retries. Carry intermediate state in requestState and verify it against tampering and reuse.
Overlooking x-mcp-header and Mcp-Param-*
If a proxy or gateway is set to drop unknown headers, Mcp-Param-* goes missing and the request is rejected with HeaderMismatch (-32020). The headers are a copy of the body, and the body is the source of truth.
Mixing Old and New Material
Articles found by search, SDK samples, and AI code generation may be written with knowledge of 2025-11-25 or earlier. If Mcp-Session-Id, initialize, or a GET stream appears, treat it as material for an older version and check the specification version.
4. Code Examples
Accepting Old and New Clients at the Same Endpoint (Go)
A skeleton that looks at the request body and dispatches to modern handling if _meta has a version, or legacy handling for initialize. handleModern and handleLegacy are functions your application implements. This example shows the idea of dispatching and is not an SDK API.
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
)
type rpcRequest struct {
Method string `json:"method"`
Params struct {
Meta map[string]json.RawMessage `json:"_meta"`
} `json:"params"`
}
func isModern(r *rpcRequest) bool {
_, ok := r.Params.Meta["io.modelcontextprotocol/protocolVersion"]
return ok
}
func router(modern, legacy http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GET / DELETE get 405 on a modern server
if r.Method != http.MethodPost {
w.Header().Set("Allow", "POST")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
r.Body = io.NopCloser(bytes.NewReader(body)) // let later handlers read it again
var req rpcRequest
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
switch {
case isModern(&req):
modern.ServeHTTP(w, r) // process statelessly
case req.Method == "initialize":
legacy.ServeHTTP(w, r) // old session handling (keep until drained)
default:
// Neither modern nor initialize. For example, a follow-up request from an old client with Mcp-Session-Id
legacy.ServeHTTP(w, r)
}
})
}
Replacing the Deprecated Logging with stderr / OpenTelemetry
A stdio server writes logs to stderr so as not to pollute stdout.
import (
"log/slog"
"os"
)
// stdout is reserved for the protocol. Logs go to stderr
var logger = slog.New(slog.NewJSONHandler(os.Stderr, nil))
func main() {
logger.Info("server started", "transport", "stdio")
// ...
}
Per-Request Log Level and Trace Context
logLevel specifies the log volume per request, and traceparent carries the trace over.
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Tokyo" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/logLevel": "warning",
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"
}
}
}
Checking Behavior After Migration (curl)
Confirm that a modern request goes through and that an unsupported version comes back as UnsupportedProtocolVersionError. Replace the URL with your own server's.
curl -sS -X POST https://example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
# Unknown version. Expect 400 and UnsupportedProtocolVersionError (-32022)
curl -sS -i -X POST https://example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 1900-01-01' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":2,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"1900-01-01","io.modelcontextprotocol/clientCapabilities":{}}}}'
5. Summary
- The core of 2026-07-28 is statelessness. It removes sessions,
initialize, the GET stream, and SSE resumption, and each request is self-contained - Server-initiated requests moved to MRTR, experimental Tasks to an extension, and notifications to
subscriptions/listen - Roots, Sampling, Logging, HTTP+SSE, and Dynamic Client Registration are deprecated. The earliest removal for Roots / Sampling / Logging / DCR is on or after 2027-07-28
- A feature moves through Active, Deprecated, and Removed, with at least 12 months from deprecation to removal for newly deprecated features (HTTP+SSE is the transition-rule exception, at three months after SEP-2596 reaches Final). The Core Maintainers decide when removal happens
- Migration means running a stateless route alongside the existing session-based route, replacing dependencies, and removing the old route after the sessions are exhausted
- Things to check are session dependencies, server-initiated requests, standalone streams, dependence on resumption, and whether headers pass through intermediaries
Related Documents
- Introduction to MCP - What Is the Model Context Protocol
- MCP Protocol Basics - JSON-RPC and Stateless Design
- Transports - stdio and Streamable HTTP
- Elicitation and Multi Round-Trip Requests
- MCP Authorization - OAuth 2.1 and Metadata Discovery