Skip to main content

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.

ChangeContentSEP
Sessions removedRemoves 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 argumentsSEP-2567
StatelessnessRemoves 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 UnsupportedProtocolVersionErrorSEP-2575
server/discoverAn RPC that servers must implement. Returns supported versions, capabilities, and identity. Clients call it optionally. On stdio it serves as the backward-compatibility probeSEP-2575
subscriptions/listenReplaces the HTTP GET endpoint and resources/subscribe / resources/unsubscribe. A long-lived POST response stream delivers only the notification types the client opted in toSEP-2575
ping and others removedRemoves ping, logging/setLevel, and notifications/roots/list_changed. The log level is specified per request with logLevel in _metaSEP-2575
Tasks as an extensionMoves 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-inSEP-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 inputResponsesSEP-2322
resultTypeRequired in every result. "complete" or "input_required". A result from an older server that omits it is treated as "complete"SEP-2322
SSE resumption removedRemoves 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 IDSEP-2575

Minor Changes​

  • extensions field: added to ClientCapabilities and ServerCapabilities, allowing negotiation of optional extensions beyond the core
  • OpenTelemetry trace context: documents conventions for the _meta keys traceparent, tracestate, and baggage (SEP-414)
  • tools/list ordering: servers are recommended to return tools in a deterministic order, for client-side caching and a better LLM prompt cache hit rate
  • Mcp-Method / Mcp-Name headers: 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 of tools/list, prompts/list, resources/list, resources/read, and resources/templates/list. ttlMs is the number of milliseconds a result can be considered fresh, and cacheScope (public or private) specifies whether shared intermediaries may cache it (SEP-2549)
  • Resource not found code: from -32002 to -32602
  • iss parameter validation: authorization servers are recommended to include iss in authorization responses (RFC 9207), and clients must check a present iss against 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: inputSchema and outputSchema allow any JSON Schema 2020-12 keywords, and structuredContent can be any JSON value. Adds $ref resolution requirements and resource bounds for composition keywords (SEP-2106)
  • URL-mode Elicitation: removes notifications/elicitation/complete and elicitationId, because under MRTR the client learns the outcome by retrying the original request. If correlation is needed, the server puts its own identifier in requestState
  • Error code allocation policy: -32000 to -32019 remains implementation-defined, and -32020 to -32099 is reserved for the specification. The codes introduced in the draft were renumbered: HeaderMismatch to -32020, MissingRequiredClientCapability to -32021, and UnsupportedProtocolVersion to -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 featureMigrate toDeprecated inEarliest removal
RootsPass directories or files via tool parameters, resource URIs, or server configuration2026-07-28 (SEP-2577)First revision released on or after 2027-07-28
SamplingIntegrate directly with LLM provider APIs2026-07-28 (SEP-2577)Same as above
LoggingLog to stderr for stdio, or use OpenTelemetry2026-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 transportStreamable HTTP2025-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.

StateMeaningExpectation for implementers
ActiveA feature in the Current revisionImplement per the specification's requirements
DeprecatedRemains in the specification but is scheduled for removal. A migration path is documentedNew implementations do not adopt it. Existing implementations migrate before the earliest removal
RemovedRemoved from the next Current revision. It remains in the last Final revision it appeared inImplementations 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 @deprecated tag, 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 two includeContext values 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.

  1. 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
  2. Add a stateless route: leave the old route and add a new one. The new route processes each request from _meta and HTTP headers alone
  3. Dispatch old and new: to accept old and new clients at the same URL, dispatch by the shape of the request. A request with _meta is modern, and one starting with initialize is legacy. A dual-era server can serve both at the same time on the same endpoint
  4. Replace dependencies: rebuild session-dependent features with stateless designs, as in the table below
  5. Move clients: move clients to the new route in turn
  6. Drain and remove: monitor traffic to the old route, and remove it after existing sessions have expired
Session-dependent featureStateless design
Data tied to an MCP sessionStore it behind an explicit boundary (such as a database) and reference it with an authenticated, server-issued handle
State of a multi-step interactionPut an integrity-protected requestState in input_required. Bind it to the user, the original method and parameters, and an expiry
Server-pushed Elicitation, Sampling, RootsReturn 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 recoveryMake 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/discover and normal requests go through? When you send an unsupported version, does UnsupportedProtocolVersionError (HTTP 400) come back with a supported list?
  • Legacy client x your server: if you keep old clients, can the same endpoint answer initialize? For a modern-only server, does the error to initialize include the supported versions?
  • Modern client x old server: on stdio, does the server/discover probe end in failure or a timeout and switch to initialize? 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 with 405?
  • Do MCP-Protocol-Version, Mcp-Method, Mcp-Name, and Mcp-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, or roots/list directly from the server
  • Standalone streams: you deliver notifications on a GET endpoint
  • Resumption: you depend on redelivery via Last-Event-ID or an event store
  • Processing that depends on the initialize response: saving capabilities at initialization, recording the version, and so on
  • Sending or receiving ping, logging/setLevel, notifications/roots/list_changed
  • List caching: */list results that do not return ttlMs / cacheScope
  • Deprecated features: are you newly using Roots, Sampling, Logging, HTTP+SSE, or Dynamic Client Registration?
  • Authorization: iss validation, binding credentials to the issuer, application_type

2. Deprecated Features and Migration Paths​

Deprecated featureMigrate to
RootsPass directories or files via tool parameters, resource URIs, or server configuration
SamplingIntegrate directly with LLM provider APIs
LoggingLog to stderr for stdio, or use OpenTelemetry
HTTP+SSE transportStreamable 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

References​