Migrating to the Stateless Model Context Protocol - What Breaks in Session-Based Servers and What Replaces It

First Published:
Last Updated:

For anyone running an MCP server of their own in production, 2026-07-28 is more than another version number. The specification itself calls this revision backward-incompatible.

The places that break are concrete. The session identifier header that the server used to mint has been removed from the transport. The handshake performed when a connection opens is gone as well. If you configured session affinity on a load balancer, what that configuration protected no longer exists. If you held per-session state in memory, there is nothing left to hang it on.

This article treats the revision not as a summary of the specification but as the migration of an existing implementation. The reader it has in mind runs a server on 2025-11-25 or earlier and wants to know which parts of the code to change, and in what order.

First, the thing this article most wants to say: the protocol-layer session going away and an application losing the ability to hold state are two entirely different things. The former is a documented change in the specification; the latter has not occurred. Confusing these two will lead to an incorrect migration process. Shopping carts, browser instances, and database transactions can still be carried across calls as before. What changed is what you attach them to.

Second, changes to the transport layer and the deprecation of certain features are being rolled out on separate schedules. The transport layer changes take effect the moment you speak this revision. The latter has a grace period, generally at least twelve months. However, there is one exception to this minimum timeframe (see Section 6.4). Combining these two into a single migration plan risks prioritizing non-urgent tasks while delaying those that require immediate attention.

Furthermore, many readers may be operating under a false assumption. Version 2026-07-28 was officially released on July 28, 2026. Material written earlier describes it as a release candidate. This article begins with a confirmation of that fact in its first chapter.

Every specification claim in this article rests on the specification itself at modelcontextprotocol.io, the official blog, and the release notes of each SDK. The AWS side rests on AWS's official documentation. The verification date is August 19, 2026. That process turned up seven places where the official sources disagree with one another. One such instance involved a request example published on the official blog, which omitted two fields that the specification designates as mandatory. Chapter 10 lists all of them with their sources.

This article covers only the procedure for moving to the latest revision, and does not cover the version history. Details regarding changes for each version, along with key milestones for adoption, are available in the Model Context Protocol Specification Version Timeline. MCP Server Implementation Reference holds the vendor-by-vendor implementations, MCP Server Testing and Debugging Guide holds the post-migration verification methods, and MCP Tool Poisoning Defense Guide holds the client-side defenses. This article cites each of them only where the migration touches its boundary.

Table of Contents

  1. 1. When did the current version change?
  2. 2. Dividing Changes into Two Categories
  3. 3. Where a Session-Based Implementation Breaks
  4. 4. Replacement Options
  5. 5. The Three Layers of the Word "Session"
  6. 6. Deprecated Features
  7. 7. The Extension Framework
  8. 8. What happens on the SDK side?
  9. 9. What to Fix in AWS Implementations
  10. 10. Where the Primary Sources Contradict Each Other
  11. 11. Migration Sequence
  12. 12. Determining Parallel Operation
  13. 13. Items to Verify After Migration
  14. 14. Failure Modes and Anti-Patterns
  15. 15. Migration Checklist
  16. 16. Frequently Asked Questions
  17. 17. Summary
  18. 18. References

1. When did the current version change?

1.1 A Revision Carries One of Three States

A revision can be marked with one of three states.

Before the states themselves, the identifier is worth pinning down. A revision is named by a string identifier in the format YYYY-MM-DD. That date is the last day a backward-incompatible change was made. The versioning page states explicitly that the protocol version is not incremented as long as the changes stay backward compatible. The identifier is therefore neither a release date nor a date of last update.

* **Draft**: in-progress specifications, not yet ready for consumption.
* **Current**: the current protocol version, which is ready for use and may continue to
  receive backwards compatible changes.
* **Final**: past, complete specifications that will not be changed.

And the same page explicitly identifies the current version.

The **current** protocol version is [**2026-07-28**](/specification/2026-07-28/).

As of the verification date, visiting modelcontextprotocol.io/docs/learn/versioning redirects to modelcontextprotocol.io/docs/2026-07-28/learn/versioning. The document path itself indicates the current version.

1.2 Why Start with This Verification?

This version was initially released as a release candidate on May 21, 2026, and became the official version on July 28, 2026. The official blog post, published on that date, announced the official release and the SDK update.

The reason for this verification is that, in addition to the secondary articles, some primary source materials related to this field were also written during the release candidate (RC) period. Documents written with the RC in mind were, of course, correct at that time. However, as statements describing the current state, they are no longer valid. If a reader has concluded that the migration can wait, the premise behind that conclusion is itself wrong.

Furthermore, the official release date does not mark the end-of-life for the previous version. A blog post announcing the SDK beta clearly states that July 28th was the date when the definitive specifications were published, and not the date when existing implementations should switch over to the current version. Not needing to rush and not needing to know the current state are different things.

There are also instances where specifications changed between the RC and the official release. The TypeScript SDK release notes, for example, document that the serverInfo within DiscoverResult was moved from the main body to the result's _meta, and that the clientInfo required for each request was downgraded from a requirement to a recommendation. Clients implemented using the RC version may not be able to correctly interpret responses from servers adhering to the official version, potentially causing them to incorrectly identify the server as an older version and attempt a handshake. Anyone who implemented features during the RC period must absolutely re-evaluate and adjust their implementations to account for the differences with the official version.

1.3 Scope of This Article

This article covers the procedure for moving servers and clients running 2025-11-25 or earlier to 2026-07-28. A list of changes for each version is already documented in previously published timeline articles, and will not be repeated here. Those timeline articles document which versions included which changes. This article focuses on outlining the specific code modifications required.

2. Dividing Changes into Two Categories

2.1 Two Systems Operate on Separate Schedules

What happened in 2026-07-28 falls into two categories. Do not mix them.

The first involves changes to the transport layer and the core protocol. This includes the removal of sessions, handshakes, server-initiated requests, and several methods. These changes take effect immediately. From the moment you decide to speak this revision, the old shape no longer holds.

The second involves deprecating features. Under the feature lifecycle policy that this revision adopts, the specification registers the affected features in a registry and records the earliest point at which each becomes eligible for removal. Deprecation does not mean removal. The affected features go on working fully in this revision.

2.2 Each Has a Different Authoritative Source

Changes to the transport layer are considered definitive when documented in the changelog and on individual feature pages. For deprecated features, the deprecated features registry page becomes the definitive source. The specification positions this registry as the official record of what will be deprecated and when.

The feature lifecycle policy describes the registry as a derived view.

This registry is a derived view kept consistent with the per-feature
deprecation notices and changelog entries, which are the normative records.

In other words, the normative records are the deprecation notices on each feature page and the changelog entries, and the registry is the list that collects them. When discrepancies arise between the two, the individual feature pages and changelog take precedence. In practice the registry is the easier one to work from, so start there and then read the original wording behind any row that applies.

2.3 What Risks Arise from Combining Them?

When creating a migration plan, combining these two systems into a single list can lead to two types of errors.

One is the risk of prematurely removing features that have been deprecated, potentially delaying high-priority work on the transport side. The other is the risk of postponing transport-side work until later, simply because there's a grace period available. The former results in wasted effort, while the latter can lead to non-functional servers.

The basis for making a decision is straightforward. Ask whether a change takes hold the moment you speak this revision, or whether it carries a grace period at all. The first belongs to the transport side, the second to the deprecation side. Chapter 6 provides a list detailing the available grace periods for each.

Session-Based and Stateless Request Flows Compared
Session-Based and Stateless Request Flows Compared

3. Where a Session-Based Implementation Breaks

What follows walks through the places in an existing implementation that break, one at a time. Breaking is not limited to errors. Some things you send get ignored, and some things you return go unused.

3.1 No Handshake

In 2025-11-25 and earlier, a Streamable HTTP exchange began with initialize and notifications/initialized. This exchange allowed the client and server to synchronize versions and features.

2026-07-28 removed that handshake. The changelog reads:

Make MCP stateless: remove the `initialize`/`notifications/initialized` handshake.

On the server side, the control that waited for the initialize message before accepting other methods is no longer necessary. On the client side, the code that sent a handshake immediately after opening the connection is now unnecessary.

3.2 Missing Session Identifier Header

After the handshake, the server was issuing a session identifier using the Mcp-Session-Id header. The client had to carry it on every later request. This header does not exist in Streamable HTTP as of 2026-07-28.

The specification states the recommended behavior for a server that speaks only this revision when traffic arrives from an older client.

* An `Mcp-Session-Id` header on a request: ignore it, and do not mint or echo
  session IDs.

This is easy to misread. The specification does not tell a server to reject a request carrying this header. It tells the server to ignore it, and it tells the server not to mint one of its own. The distinction matters, and Section 9.3 shows where it bites on AWS.

3.3 There Is No Standalone GET Stream

Previously, clients would make HTTP GET requests to MCP endpoints to establish a stream for receiving messages from the server. This GET endpoint has been removed.

A server that speaks only this revision is recommended to answer GET and DELETE with 405 Method Not Allowed. DELETE was a method previously used to terminate sessions.

3.4 Stream Resumption Not Available

When an SSE stream disconnects, a mechanism previously existed to resume it using the Last-Event-ID header. This functionality has been removed. The changelog states that a disconnected response stream is lost along with the processing request.

A broken response stream loses the in-flight request; clients **MUST** re-issue it
as a new request with a new request ID

When resending, a new request ID must be used. Do not resend using the same ID. Clients implementing retry functionality need to verify this point.

3.5 There Are No Server-Initiated Requests

Previously, the server could send JSON-RPC requests to the client during processing. This included actions such as confirming with the user, requesting generation from the model, and retrieving a list of files in the file system.

As of 2026-07-28, the server is no longer able to stream independent requests.

The server **MUST NOT** send independent JSON-RPC *requests* on this stream.

This applies to stdio as well. A server must not write JSON-RPC requests to stdout.

Instead, the server embeds the request for what it needs in the result. That mechanism is multi round-trip requests, covered in Section 4.6. The specification names this replacement a breaking change.

3.6 Removed Methods

Several methods were removed in this revision. Instead of silently failing, these methods now return an error. The specification requires that servers that do not implement a requested RPC return a 404 Not Found status and the JSON-RPC error code -32601. The release notes for the Go SDK also state that removed methods are rejected with a MethodNotFound error. The following methods have been removed:

ping has been removed. If you were using this to verify connection status, you will need to find an alternative solution.

logging/setLevel has been removed. Log levels are now specified for each request using the io.modelcontextprotocol/logLevel field in _meta. Furthermore, a server must not send notifications/message for a request that did not include this field. Without that field, no log arrives. That is the new default.

notifications/roots/list_changed has been removed.

subscriptions/listen, covered later, replaces resources/subscribe and resources/unsubscribe.

3.7 A List Must Not Vary by Connection

There is a change here that is easy to miss. tools/list, resources/list, and prompts/list must no longer return different results per connection. The tools page of the specification first says that the set may be empty and may change over time, and then continues:

but **MUST NOT** vary
per-connection or as a side effect of other requests on the connection.

This statement prohibits a previously existing design pattern. SEP-2567, for example, described a scenario where a database server had a tool called connect_database, and after it was called, the tools/list endpoint would display query and list_tables. This pattern is no longer valid.

However, the same paragraph explicitly permits differences based on authorization.

The set **MAY** vary by the authorization presented on the request — for example,
returning only the tools the caller's granted scopes permit — since credentials are
per-request input, not connection state.

It is permissible to vary the list based on authorization, but it is not permissible to change it based on the connection history. Servers that dynamically change the list of tools need to verify the source of that dynamic behavior.

4. Replacement Options

4.1 Metadata per Request

Everything the handshake used to carry now rides in the _meta of every request. There are four keys defined as protocol fields for each request, and whether each is required differs. The table lists them. In addition to these four keys, the _meta section also reserves keys for progress tokens and OpenTelemetry trace contexts.

KeyTypeRequiredDescription
io.modelcontextprotocol/protocolVersionstringRequiredThe version used by this request.
io.modelcontextprotocol/clientCapabilitiesClientCapabilitiesRequiredClient capabilities relevant to this request.
io.modelcontextprotocol/clientInfoImplementationOptionalThe client's name and version.
io.modelcontextprotocol/logLevelLoggingLevelOptionalThe minimum log level the server should output for this request.

A request that omits a required field is malformed. The server rejects it with JSON-RPC error code -32602, and over HTTP returns 400 Bad Request.

io.modelcontextprotocol/clientInfo is optional. The specification still asks a client to include it on every request, unless it is specifically configured not to. The specification asks servers, with the same strength, to include io.modelcontextprotocol/serverInfo in the _meta of every result.

Important notes apply to these two keys.

`io.modelcontextprotocol/clientInfo` and `io.modelcontextprotocol/serverInfo`
are self-reported by the sender and are not verified by the protocol.

These values are self-reported and not validated. The specification intends them for display, logging, and debugging. It also says explicitly that implementations should not branch on them and should not lean on them for security decisions. If a server is branching its behavior based on the client's name, that would be contrary to the specification's intent.

In Streamable HTTP, the version is also included in the MCP-Protocol-Version header. The header value must match the value in _meta. If they do not match, the server will reject the request with a 400 Bad Request and a HeaderMismatch error.

4.2 Explicit Handles Are Not Protocol Components

An explicit handle is where the application state that used to hang off a session goes. A creation tool returns an identifier, and subsequent tool calls receive it as an argument.

The changelog puts it this way:

Servers that need cross-call state use explicit, server-minted handles passed as
ordinary tool arguments

This is the point most easily misread. SEP-2567 states explicitly that it is not a change to the protocol.

There is no `handles/*` method, no handle type in the schema, no wire-level concept
of a handle at all. From the protocol's perspective a handle is a string in a tool
result and a string in a tool argument, indistinguishable from any other tool data.

In other words, from the implementer's side this is not a matter of learning a new API. It is a matter of changing how tools are designed. Taking the cart as the example, create_basket() returns a basket_id and add_item(basket_id, ...) accepts it. The server implementation decides the handle's lifetime, how it expires, and how it checks permission.

The change also brings a property sessions never had. SEP-2567 points to the freedom to set a different sharing scope per piece of state. State scoped to a session has a cardinality of exactly one per session. Picture an orchestrator running several subagents that must share one cart while each keeps its own browser: no session boundary satisfies both. With identifiers, you hand over only what should be shared.

4.3 Change Notifications Are Aggregated into a Single Stream

subscriptions/listen is the replacement for both the GET stream and resources/subscribe. When a client sends this request, the response itself becomes an open stream, delivering only notifications of the types the client has subscribed to.

The subscription filter has four fields:

FieldTypeNotification Received
toolsListChangedbooleannotifications/tools/list_changed
promptsListChangedbooleannotifications/prompts/list_changed
resourcesListChangedbooleannotifications/resources/list_changed
resourceSubscriptionsstring[]notifications/resources/updated for the specified URIs

All fields are optional; omitting a field indicates that the client is not subscribing to that type of notification. The server must not send any notification types that the client has not explicitly requested.

The server must send notifications/subscriptions/acknowledged as the first message in the stream, and must not send any notifications related to subscriptions before this. Because all subscriptions in stdio share a single channel, this order is defined per subscription identifier. Messages belonging to other subscriptions may be interleaved. The filters included in this response represent the subset of notification types that the server has agreed to provide. The server omits any type it does not support. The client must reconcile what it requested with what the server acknowledged.

All notifications on the stream include a _meta field with io.modelcontextprotocol/subscriptionId. The value is the JSON-RPC ID of the subscriptions/listen request, and on stdio it is what lets a client demultiplex the stream.

Progress and log notifications are not delivered on this stream. They flow on the response stream of the request they relate to. The changelog clarifies this distinction.

4.4 List Results Carry Cache Hints

Because sessions are gone, list results can now be reused across connections. Cache hints build on that.

These hints apply only to results with a resultType of complete. An interim result that returns input_required during a multi round-trip exchange is not cacheable, and therefore will not receive these hints.

ttlMs is an integer in milliseconds, giving how long a result may be treated as fresh. The semantics match the HTTP Cache-Control: max-age directive. cacheScope is either public or private, and it decides whether a shared intermediary cache may store the result.

If ttlMs is missing, clients should assume a value of 0 and treat the result as stale immediately. The specification notes that this situation is expected to occur only with older servers. Negative values are also treated as 0.

ttlMs is a hint, not a guarantee. The specification explicitly states that the server may modify the data before the TTL expires. The TTL represents the time during which retrieval can be avoided, not the duration for which the data remains unchanged.

And there is an important operational consideration.

Clients **SHOULD NOT** treat TTL as a polling interval that triggers automatic
background refetches.

Do not use the TTL as a polling interval. Instead, check whether the result is fresh when needed, and if it is stale, retrieve it again. Implementations that poll must always apply jitter and backoff mechanisms.

cacheScope has a security trap. A result marked public can travel into a different authorization context even when it came back from an authenticated endpoint. The specification says plainly that cacheScope alone must not be what stops unauthorized access. Marking an authorization-filtered list as public leaks past the filter.

4.5 Header Fields Become Mandatory for Routing

Streamable HTTP POST requests now require two header fields to determine the routing path. Combined with the MCP-Protocol-Version header (as described in Section 4.1), there are now three mandatory headers.

HeaderOriginating FieldRequired For
Mcp-MethodmethodAll requests
Mcp-Nameparams.name or params.uritools/call, resources/read, prompts/get

MCP-Protocol-Version is not in this table. The specification defines it in a separate section. On the server side, however, validation treats all three as required standard headers.

The goal is to enable intermediate devices to determine the routing path and traffic flow without parsing the request body. Load balancers, API gateways, and rate limiting devices can make decisions based solely on the HTTP layer.

Servers that process the request body must validate the consistency between the header fields and the body. On a mismatch the server rejects the request with 400 Bad Request and error code -32020. The specification states that this is to prevent vulnerabilities that arise when different components on the network rely on different sources of truth. The scenario where a load balancer routes based on the header and the server then executes based on the body is the issue.

The Mcp-Name header and Mcp-Param-{Name} headers may carry values that cannot be safely represented in ASCII. In such cases, these values should be encoded in a format that begins with =?base64? and ends with ?=, using lowercase characters for the prefix and suffix (case-sensitive).

There is also a mechanism to map tool parameters to header fields. Annotating a property in the inputSchema with x-mcp-header puts that value into a header named Mcp-Param-{Name}. Using x-mcp-header is optional for a server, but supporting it is mandatory for a client. Streamable HTTP clients that receive tool definitions with x-mcp-header properties that violate the constraints must exclude those tools from the results returned by tools/list. Clients using other transports may safely ignore this annotation entirely.

4.6 Multi Round-Trip Requests Replace Server-Initiated Requests

Multi round-trip requests are the mechanism by which a server puts a question to a client without holding any server-side state.

The process consists of four steps. First, the client sends an initial request. The server determines that it lacks sufficient information and returns a result with resultType set to input_required. The client then gathers the requested information and resends the original request with a new request ID. The server, having now received sufficient information, returns the final result.

The result contains two fields. inputRequests is a mapping from keys assigned by the server to request objects. The value associated with each entry can be one of three types: elicitation, sampling, or roots. requestState is an opaque string that only has meaning to the server; the client must not inspect, parse, or modify its contents.

There are specific regulations that implementers must carefully review regarding the handling of requestState.

If a client request contains a `requestState` field, servers **MUST** treat
`requestState` as an attacker-controlled input.

If this value influences authorization, resource access, or business logic, the server must protect its integrity and must reject state that fails verification. The specification provides examples such as HMAC and AEAD. Furthermore, to prevent replay attacks, it is recommended to include the authenticated principal, a short expiration time, and the original request identifier within the integrity-protected payload.

However, these measures do not guarantee single-use consumption. The specification scopes this: a server for which a given requestState must be consumed at most once, as in a one-time redemption, has to enforce that invariant server-side on its own.

Only three types of client requests can trigger a return of InputRequiredResult: prompts/get, resources/read, and tools/call. The server must not return this result for any other type of request.

There is also a rule that a server must not send a request for a capability the client has not declared. A server must not include elicitation/create for a client that has not declared support for elicitation. If processing requires undeclared capabilities, the server must return an error code -32021 and list the missing capabilities in the data.requiredCapabilities field.

4.7 server/discover Is Mandatory on the Server Side

A new RPC has been added: server/discover. This RPC returns version information, supported features, and identification details in a single request.

The implementation requirement differs between the two sides. The versioning page of the specification says a server must implement this RPC, and then continues about the client.

Clients
**MAY** call it before sending any other requests to learn the server's
supported versions up front, but are not required to

A server always implements it. A client may call it or not. Sending tools/call straight away, taking an UnsupportedProtocolVersionError when the revision does not match, and picking again from the advertised list is an allowed way to proceed.

The format of this error is as follows:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32022,
    "message": "Unsupported protocol version",
    "data": {
      "supported": ["2026-07-28", "2025-11-25"],
      "requested": "1900-01-01"
    }
  }
}

There are two scenarios where calling server/discover is clearly beneficial. One is when you want to display the server's identification information, features, and supported versions all at once. The other is legacy version detection via stdio. Because stdio has no HTTP status code, there is no other signal to drive the backward-compatibility decision. Chapter 12 covers it.

5. The Three Layers of the Word "Session"

This chapter is the one most likely to be misunderstood.

5.1 Dividing Layers

The term "session" refers to at least three distinct entities within this context. Confusion arises in discussions about migration because different parties may be referring to different layers.

LayerWhat it refers toStatus as of 2026-07-28
Protocol LayerThe Mcp-Session-Id and the associated handshake, version, and feature agreements.Removed
Application LayerBusiness state that spans calls, such as carts, browser instances, and transactions.Not gone. What you attach it to has changed.
Platform LayerAn identifier a hosting platform uses to pin the route.Outside the specification. Each platform decides.

Three Layers That the Word Session Can Refer To
Three Layers That the Word Session Can Refer To

5.2 What Happened at the Protocol Layer

The Statelessness section of the specification describes this layer.

The Model Context Protocol (MCP) is a **stateless protocol**: all the
information needed to process a request is contained in the request itself.
A server processes each request independently; no state should be inferred
from previous requests, even those on the same connection or stream.

There are five specific requirements. A server must not rely on prior requests over the same connection to establish context. The server should be capable of handling requests associated with multiple tasks, threads, or conversations. The server should not require clients to reuse the same connection or process for related operations. A client should not use an individual task, thread, or conversation as the lifetime boundary of the stdio process. Furthermore, any state spanning multiple requests must be referenced using explicit identifiers provided by the client with each request.

The note provides further detail.

This implies that an open connection, such as a STDIO process, is not a
conversation or session: clients may interleave unrelated requests on the same
transport, and a server must not treat connection or process identity as a
proxy for conversation or session continuity.

The phrasing that a stdio process is not a conversation or a session is strong, and it is strong for a reason: implementations exist that used the process lifetime as the lifetime of a conversation. SEP-2567 notes that many servers that appear to be effectively utilizing session state are, in reality, stdio servers that rely on the process's lifetime, which is a characteristic of the transport layer rather than the protocol itself.

5.3 The Application Layer Has Not Disappeared

Nothing in this layer is forbidden. Carts, browser instances, and database transactions still span calls, exactly as before. What changed is what they hang off. They used to hang off an implicit session. Now they hang off an explicit identifier.

SEP-2567 gives the reason for dropping sessions. The lifetime of a session is not defined by the specification and varies by host application. The behavior of deployed clients is inconsistent: some create a new session for every tool call, others create one at application startup and hold it for the life of the process. Almost no client resumes a previous session after a disconnect or a restart.

Server authors are the ones who decide what to attach to a session, and they do not know what a session corresponds to. That is why the session was not dependable as a container for application state.

5.4 The Platform Layer Is Outside the Specification

The third layer is the one the specification says nothing about. A hosting platform may use an identifier to deliver requests to the same computational resources. These identifiers, even if they happen to share the same header name, are distinct from the sessions managed by the protocol layer.

Section 9.3 gives the concrete example. What matters here is that removing Mcp-Session-Id from the specification does not make the header disappear from your traffic. If the hosting platform is using it on its own account, what was removed is only the protocol-level meaning.

5.5 What Happens When Layers Are Misinterpreted

Here are three common misinterpretations, all of which lead to an incorrect assessment of the work required for the transition.

First, there's the mistake of reading the deletion of a protocol layer as a prohibition at the application layer. This interpretation leads to a plan that requires rebuilding all stateful tools. In reality you only change what the state hangs off, and that is an identifier.

Second, there's the error of interpreting an identifier at the platform layer as a remnant of the protocol layer. Reading it that way, you look at the header the platform attaches and wrongly conclude that the server is still running an old version.

Third, there's the issue of applying application layer state to the platform layer. Route pinning in a hosting platform is a performance mechanism. It does not guarantee that state survives. If the state disappears when the route changes, the application layer was designed wrong.

6. Deprecated Features

6.1 The Registry Is the Starting Point

The deprecated features registry page lists the features currently in the Deprecated state. As of the verification date there are six rows. The Removed section is empty: no feature has been removed under this policy yet.

The feature lifecycle policy defines Deprecated as follows. The feature remains in the specification but is scheduled for removal. New implementations should not adopt it. Existing implementations should migrate before its earliest removal.

The earliest removal is the point at which a feature becomes eligible for removal, not the date it is removed.

The earliest removal marks when a feature becomes *eligible* for removal; the
actual removal is a Core Maintainer decision taken during release preparation
and may happen later.

The same policy also notes that, occasionally, a feature may stay Deprecated for much longer than the minimum window.

6.2 Three Features and Where They Go

Several features were newly marked as Deprecated in this revision. Of those, the ones in the protocol core are Roots, Sampling, and Logging. Each of these is a feature name in the specification, not an ordinary word. The authorization area carries one more, and Section 6.3 covers it. Throughout this article, these three appear in this form whenever the feature is meant.

FeatureWhere it goes
RootsPassing directories and files as parameters for the tool, resource URIs, and server configurations.
SamplingDirect integration with LLM provider APIs.
LoggingOn stdio, write to stderr. For structured observability, use OpenTelemetry.

Each of the three feature pages carries a note in the same form, stating that the feature stays in the specification for at least twelve months from the release of this revision.

Logging deserves a closer look, because deprecation and removal land on it at the same time. The feature is Deprecated, yet this revision removes the logging/setLevel method outright. The level now moves in the _meta of each request. Roots is the same shape: the feature is Deprecated, and notifications/roots/list_changed is gone. A feature can carry a grace period while an individual method inside it stops working today.

6.3 Two More Deprecations

There are two more deprecations, one in the authorization area and one in Sampling.

The first is OAuth 2.0 Dynamic Client Registration, deprecated as a client registration mechanism. The migration path is Client ID Metadata Documents. Dynamic Client Registration remains in place for backward compatibility with authorization servers that do not support Client ID Metadata Documents. The revision in which this feature became Deprecated is 2026-07-28, the same as the three in Section 6.2.

The second involves the values for includeContext, specifically thisServer and allServers. These were previously soft-deprecated as of 2025-11-25 and have now been formally reclassified as deprecated under this policy. The recommended alternative is to omit this parameter or use the value none.

The remaining row in the registry is the HTTP+SSE transport, reclassified in the same way. Its deadline differs from the rest, so Section 6.4 takes it.

6.4 The Time Before Removal Is Not Uniform

This is the most common point of misunderstanding in the migration plan. A minimum of twelve months is the figure usually quoted, but the Earliest removal column of the registry differs row by row.

FeatureRevision that deprecated itEarliest removal
Roots2026-07-28The first revision released on or after 2027-07-28
Sampling2026-07-28Same as above
Logging2026-07-28Same as above
Dynamic Client Registration2026-07-28Same as above
The two includeContext values2025-11-25Follows Sampling
HTTP+SSE transport2025-03-26Three months after SEP-2596 reaches Final

HTTP+SSE is the one row that does not sit inside the twelve-month frame. The reason is that the revision recorded as deprecating it is 2025-03-26, so the twelve-month window has already elapsed when counted from that revision's release. SEP-2596 provides a three-month timeframe for this feature as part of the migration guidelines.

The twelve-month figure has one more exception. The feature lifecycle policy carries an expedited-removal clause. Where a published security advisory exists, or exploitation in the wild is documented, and no in-place mitigation is available, the twelve-month floor may be shortened. Even when shortened, a minimum of ninety days is always maintained from the deprecation date.

Expedited removal requires approval from a Core Maintainer and is documented in either the deprecation SEP or a short expedited-removal SEP that references it. Therefore, twelve months is not an absolute guarantee. It is advisable to avoid relying solely on this number when planning your migration.

6.5 Tier 1 SDK Responsibilities

Deprecation carries obligations for Tier 1 SDKs. Once the revision that deprecates a feature ships as Current, a Tier 1 SDK must mark the matching API surface deprecated in its next release, using the language's native mechanism. The marking cites the deprecation SEP, and the earliest removal where the mechanism allows. The SDK should also emit a runtime warning when the deprecated feature is exercised.

This requirement can be helpful in migration efforts. It allows developers to identify instances within their code that utilize deprecated features through compiler warnings or static analysis. For example, the C# SDK exposes MCP9005 within the Roots, Sampling, and Logging APIs.

Core, Deprecated, and Extension Layers of MCP 2026-07-28
Core, Deprecated, and Extension Layers of MCP 2026-07-28

7. The Extension Framework

7.1 It Moves on a Cycle Separate from the Core

Of the three shown in Figure 3, extensions are the one not yet covered. This is a different grouping from the three layers in Chapter 5, and the two should not be mixed. Extensions exist outside the core specification, are managed in a separate repository, and follow an independent release cycle.

Identifiers use a reversed domain name prefix. Official extensions use io.modelcontextprotocol. Third-party extensions should use their own domain name, reversed.

The most important characteristic is that they are opt-in.

Extensions are always disabled by default and require explicit opt-in from the developer.

If one system supports an extension and the other does not, the system that supports it will revert to the core system's behavior. If the extension is required, it will reject the request with an appropriate error. Extensions should document the expected fallback behavior.

7.2 How Negotiation Works

Clients declare their support for extensions in the extensions field of io.modelcontextprotocol/clientCapabilities under the _meta for each request. Servers declare their support in the extensions field of the capabilities included in the server/discover response. Values consist of configuration objects for each extension; an empty object signifies support without any additional configuration.

7.3 What Is Official Today

Do not build your own list of extensions. These extensions grow independently, so it is best to refer to the official documentation. As of the verification date, the extensions listed as official are two related to authorization, one related to the user interface, and one for asynchronous execution. Specifically, these are OAuth Client Credentials and Enterprise-Managed Authorization (for authorization), MCP Apps (for the user interface), and MCP Tasks (for asynchronous execution).

There is also a slot for experimental extensions. Each one ties to a Working Group or an Interest Group and lives in a repository prefixed experimental-ext-. Promoting one to official status goes through the ordinary SEP process.

7.4 The Significance of Tasks Moving Out of the Core

In 2026-07-28, the experimental tasks feature moved out of the core and into an extension. The identifier is io.modelcontextprotocol/tasks. As part of the redesign, the blocking tasks/result has been replaced with polling via tasks/get, a tasks/update function has been added to send input from the client to the server, and tasks/list has been removed.

The key implication of this transition is that the core and the extension now operate independently. Simply updating the core to version 2026-07-28 does not automatically make the extension available. Both sides need to explicitly declare their compatibility, and the status of SDK support also needs to be verified separately from the core.

8. What happens on the SDK side?

8.1 Whether an Upgrade Alone Changes the Wire Depends on the SDK

What this chapter covers is what actually decides the migration order.

The four SDKs in Tier 1 all support 2026-07-28. However, whether upgrading the package alone changes what actually goes over the wire differs by SDK.

The release notes for the Go SDK require explicit configuration.

The streamable HTTP transport accepts requests at protocol version `2026-07-28` only when
`StreamableHTTPOptions.Stateless = true`.

If not configured, the client will negotiate down to 2025-11-25.

There is a condition here that is easy to overlook. The same release notes state that the new protocol is enabled by default for new clients. On the server side, it requires an explicit opt-in, while on the client side, it defaults to the new version. If both are implemented in Go, it can be easily misinterpreted, with only one side functioning.

The C# SDK is the opposite. The release notes state that the default behavior has been inverted.

`HttpServerTransportOptions.Stateless` now defaults to `true`.

If the previous behavior is required, explicitly set Stateless = false.

The Python SDK has eliminated the need for configuration. The release notes state that a single MCPServer can handle both older and newer clients on the same endpoint.

The TypeScript SDK consolidates the HTTP entry point into createMcpHandler. On whether upgrading the package alone changes the wire, the only statement that could be found is in the beta announcement. That article states that with TypeScript and Go, the opt-in extends to the communication layer, and specifying 2026-07-28 is an explicit choice when configuring the transport. No statement to the same effect was found in the stable release notes, so confirm it against the version you pin.

This difference directly impacts the migration order. For SDKs with inverted defaults, simply upgrading the package constitutes the actual behavior change. For SDKs that require an explicit opt-in, the package upgrade and version switching can be treated as separate changes.

8.2 The Major Version Update and Adopting the Revision Are Separate Jobs

The Python and TypeScript SDKs have been updated to major versions to reflect these changes. Do not combine the two.

Python has transitioned from FastMCP to MCPServer, and the decorator API has been carried over. pip install mcp now installs version 2.x. Libraries without version constraints may inadvertently install 2.x. Version 1.x has entered maintenance mode. What v1.x still receives, however, is described in two different ways inside a single paragraph of the release notes (Section 10.7).

TypeScript has been split into separate packages. The server and client are now in different packages, and adapters for Node.js, Express, Hono, and Fastify have been separated. It only supports ESM and runs on Node.js 20 and later. The tool schema now uses Standard Schema, allowing integration with tools like Zod, Valibot, and ArkType. A codemod is available to facilitate migration from version 1.

Go and C# have not undergone any package splitting or API redesigns.

Therefore, when using Python and TypeScript, the migration process will be in two stages. One is moving to the major version, the other is speaking the new revision. The TypeScript SDK repository provides separate guides for these two processes.

8.3 Incorporating Tier Differences into the Migration Plan

SDKs are classified into tiers. As of the verification date, TypeScript, Python, C#, and Go are Tier 1; Java, Rust, and Ruby are Tier 2; and Swift, PHP, and Kotlin are Tier 3.

Outside Tier 1, support does not arrive with the core release. On release day the official blog wrote that the four Tier 1 SDKs speak 2026-07-28 from that day, and that the Rust SDK supported it in beta. As of the verification date the Rust SDK has shipped a stable release, but that release came after the specification's release date.

Check the tier of the SDK you use, and build its support timing into the assumptions of your migration plan. Building your own support table freezes something that moves on an independent cycle, so read the official tier list instead.

9. What to Fix in AWS Implementations

The primary source for this article is not the AWS documentation. Everything up to this point was verified against the specification itself, the official blog, and SDK release notes. This chapter alone describes what happens when you run on AWS, verified against AWS's official documentation.

9.1 AgentCore Gateway Adopts It Through a Configuration Change

Amazon Bedrock AgentCore Gateway holds the MCP revisions it supports as configuration. The documentation lists four supported versions: 2026-07-28, 2025-11-25, 2025-06-18, and 2025-03-26.

The same section explains how 2026-07-28 is handled. Clients do not perform a handshake. Instead, each request includes the version number in the MCP-Protocol-Version header and in the _meta field as io.modelcontextprotocol/protocolVersion. The gateway publishes its capabilities through server/discover. 2025-11-25 and earlier keep the handshake.

One UpdateGateway call adds a revision. There is no need to recreate the gateway, and no need to touch the configuration of individual targets. The revision is a property of the gateway, not of its targets. Furthermore, an AWS blog post includes an important note:

`UpdateGateway` **replaces** `supportedVersions` with the set you send. It does not append.

The sent collection will replace the existing one; it will not be an addition. You must first read the current configuration and then send a complete list of the versions you want to advertise. Rolling back involves performing the same operation in reverse, sending a list that does not include the 2026-07-28 version.

9.2 Version Selection Happens per Request

Gateway behavior falls into three cases. A request naming a revision the gateway advertises is served in that revision. A request naming a revision the gateway does not advertise is rejected with HTTP 400 and code -32022, and the response body carries the list of supported revisions. Requests without a header default to the default version, which is 2025-03-26.

If this default is not included in the supportedVersions list, requests without a header will be rejected. The specification permits this behavior. Servers supporting versions prior to 2025-06-18 are permitted to treat headerless requests as if they were targeting 2025-03-26. Servers that do not support this behavior must reject such requests.

Because multiple versions can be advertised simultaneously, clients and gateways can migrate in either order, based on their individual needs. AWS recommends a three-stage rollout. This involves adding the new version alongside the existing version, migrating clients at their own pace, and trimming the list only after every client requests the new revision.

9.3 In AgentCore Runtime, the Session Identifier Persists

The concrete example of the platform layer from Section 5.4 sits here.

The documentation for Amazon Bedrock AgentCore Runtime states that it uses the Mcp-Session-Id header to pin requests to a microVM.

**MicroVM Stickiness** : Amazon Bedrock AgentCore uses the `Mcp-Session-Id` header to
route requests to the same microVM instance.

It states plainly that the header is always returned, in either mode.

In both modes, Amazon Bedrock AgentCore always returns an `Mcp-Session-Id` header to
clients. Always capture and reuse this header for optimal performance.

In a stateless configuration, the platform generates the identifier and includes it in requests to the server. The documentation requires that the server does not reject this identifier.

The relationship with the specification is worth stating precisely. The specification says that a server speaking only this revision should ignore the header and should not mint one itself. Ignoring the header and not rejecting it point in the same direction, so there is no contradiction on this point. However, the expectation that this header will disappear from your traffic is not met. What disappears is the protocol-level meaning associated with it, not the use of the header name itself.

Furthermore, a requirement outside the specification still lands on the client. To keep the route pinned, the client has to keep putting the returned identifier on subsequent requests. If it does not, requests may be routed to a different microVM each time, potentially leading to increased latency due to cold starts. A round trip the protocol no longer needs survives as a performance characteristic of the platform.

The Runtime documentation carries another statement. In 2025-11-25 and earlier, elicitation and sampling required stateful mode. From 2026-07-28 onward, the documentation states, elicitation and sampling use multi round-trip requests, so stateful mode is not required. Moving to this revision removes one of the reasons for choosing stateful mode.

9.4 Reviewing Error Code Matching

2026-07-28 partitions the JSON-RPC server-error range. -32000 to -32019 stays implementation-defined and legacy: new codes must not be allocated there, and new implementations should not use the sub-range at all. -32020 to -32099 belongs to the MCP specification.

Additionally, the documentation explicitly states that the current implementation should not emit two error codes previously defined in older versions. -32002 was resource not found, and -32602 now carries that meaning. -32042 was URL elicitation required and existed only in 2025-11-25.

The primary practical task for the client-side implementation is this: Locate any code that is checking for -32002 as a string or constant and update it to accept -32602 instead. However, if there is a possibility of communicating with older versions of the server, it will still be necessary to continue accepting -32002. The specification states that clients should accept -32002 from servers implementing older versions.

Section 10.5 takes the relationship with the AWS documentation.

9.5 Restrictions When Crossing a Gateway

When AgentCore Gateway fronts a new version of the server while clients remain on an older version, the gateway will perform a version conversion. However, this conversion has limitations. According to an AWS blog post (as of the date of verification), this conversion does not support calls related to elicitation or sampling. If an older client attempts to use a tool that requires elicitation or sampling on the new server version, an error will occur.

The order of migration is important. If a tool utilizes elicitation or sampling, it is not possible to upgrade only the server-side first.

9.6 Implementation on AWS Lambda

When running the MCP server on AWS Lambda, the SDK included determines the version used. As seen in Chapter 8, each SDK has different default settings.

For the Lambda adapter that AWS Labs publishes, the latest release as of the verification date is dated July 7, 2026. That predates July 28, 2026, the day the specification published its final revision. No official AWS statement was found, as of the verification date, on whether the adapter supports 2026-07-28. This article makes no judgment on that point. When migrating an implementation on Lambda, check the revision of the bundled SDK and the adapter's own support status, each against its own primary source.

The process of building the MCP server on Lambda itself is already detailed in the previously published MCP Server on AWS Lambda Complete Guide. That article describes the session identifier as current behavior, so mind the difference in revision when reading the two together.

10. Where the Primary Sources Contradict Each Other

Seven were found. The verification date for all of them is August 19, 2026. Three sit between the specification and the official blog, three call for a consistency check between the AWS documentation and the specification, and one sits inside the SDK release notes.

10.1 The Operations That Must Carry Cache Hints Are Listed Three Ways

There are three different sets of operations that require cache hints, as outlined in three separate documents.

DocumentListed OperationsCount
Caching Pageserver/discover, tools/list, prompts/list, resources/list, resources/templates/list, resources/read6
Changelog (Minor Changes)tools/list, prompts/list, resources/list, resources/read, resources/templates/list5
Official Blogtools/list, prompts/list, resources/list, resources/read4

Take the caching page as authoritative. An overview or an announcement shortens a list to explain something; the dedicated page exists to state the constraint. As an example, the response examples on the server/discover page include both ttlMs and cacheScope. Only the official blog omits resources/templates/list.

The practical implications are clear. If developers implementing the server only refer to the list on the official blog, they may forget to include cache hints for server/discover and resources/templates/list.

10.2 The Removal Deadline for HTTP+SSE Is Written Two Ways

The official blog states the following regarding deprecated features:

The legacy HTTP+SSE transport is also considered to be officially deprecated, with a
year-long offramp.

However, the entry for HTTP+SSE in the deprecated features registry indicates that the earliest removal date is three months after SEP-2596 reaches Final. The table outlining migration guidelines for SEP-2596 also specifies the same deadline.

The registry and SEP are consistent, but the blog post differs. The reason is the one given in Section 6.4: the revision recorded as deprecating HTTP+SSE is 2025-03-26.

This gap goes straight into the length of a migration plan. An implementation still on HTTP+SSE that reads the blog and assumes a year will badly overestimate the room it has.

10.3 Official Blog Request Examples Missing Required Fields

The tools/call examples on the official blog and on the AWS blog both put only io.modelcontextprotocol/clientInfo in _meta. They are missing io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities.

The specification requires these two fields. As shown in the table in Section 4.1, requests that omit these fields will be rejected as malformed with the error code -32602. Furthermore, in Streamable HTTP, the value of the MCP-Protocol-Version header must match the corresponding field in _meta. If that field is absent, there is nothing to match against.

The specification says so when it abbreviates an example. The page describing multi round-trip requests states:

For brevity, the request examples on this page omit the `_meta` request
metadata (`io.modelcontextprotocol/protocolVersion`,
`io.modelcontextprotocol/clientInfo`, and
`io.modelcontextprotocol/clientCapabilities`). Every request **MUST** include
the required `_meta` fields

These disclaimers are not present on either blog. A reader who copies either example verbatim gets rejected by any server that follows the specification. If you intend to use these as a reference, you should instead use the complete examples provided on the Streamable HTTP page.

10.4 Two Documents Read in Opposite Directions on the Session Identifier Header

The specification tells a server that speaks only this revision to ignore the Mcp-Session-Id header, and neither to mint nor to echo one. However, the AgentCore Runtime documentation states that the platform should always return this header in both modes, and that clients should continue including it in subsequent requests.

These documents describe different scopes, so the statements are not inherently contradictory. The specification describes the behavior of the MCP server, while the AWS documentation describes the behavior of the hosting platform that sits in front of the MCP server. It is entirely possible for the server to ignore the header while the platform uses it for routing.

Nevertheless, this discrepancy requires verification by the reader. Individuals who only read the specification may expect this header to disappear from their traffic. However, in the AgentCore Runtime environment, this is not the case. Furthermore, the continued presence of this header, when interpreted as a protocol session identifier, can lead to the second type of misinterpretation mentioned in Section 5.5.

10.5 Two Documents Overlap in the Reserved Error Code Range

The specification states that an implementation of 2026-07-28 must not emit -32002. Furthermore, it explicitly states that new implementations should not use the entire range from -32000 to -32019.

The AgentCore Runtime error list uses codes in the range -32001 to -32011. Within this range, -32002 is assigned to AccessDeniedException, which is a different meaning from the resource not found it carried in older revisions of the specification.

AWS's own blog advises readers to be cautious regarding this specific error code.

If your client matches on `-32002` anywhere, audit those code paths.

The scope of the requirement splits for the same reason as in Section 10.4. The list details exceptions returned by the Runtime service, rather than errors emitted by the MCP server. Both, however, reach the client as a JSON-RPC error body.

Therefore, it is risky for clients to treat -32002 as a standalone condition. A resource-not-found error from an older server and an authorization failure from AgentCore Runtime can arrive under the same code. It is necessary to use both the error message and the HTTP status code in the response to accurately determine the error. The specification also clearly states that the range from -32000 to -32019 is a range that the receiving end should not assume any specific meaning.

10.6 Two Explanations of Server-Initiated Requests Differ in Strength

The AWS blog describes how server-initiated requests are handled as follows:

With the latest protocol version, server-initiated requests are now only permitted while
the server is actively processing a client request (SEP-2260).

This phrasing can be interpreted as suggesting that these requests may still exist under certain conditions. However, the core specification is much stronger. The Streamable HTTP page states that a server must not send independent JSON-RPC requests on the stream, and the stdio page states that a server must not write requests to stdout. The page on multi round-trip requests explicitly states that previous patterns for server-initiated requests are no longer supported.

The specification is the one to follow. In 2026-07-28 there is no path by which a server sends a request, not even while it is processing a client request. The only way a server asks for what it needs is by embedding the request in a result.

The user guide for AgentCore Gateway also contains a similar ambiguity in its operational descriptions. Both elicitation/create and sampling/createMessage carry a server-initiated label, while the same page states that on 2026-07-28 these become multi round-trip requests. It is reasonable to interpret this as the labels retaining an outdated terminology.

10.7 The Python SDK's Maintenance Policy Is Inconsistent in the Release Notes

The v2.0.0 release notes for the Python SDK describe the handling of v1.x in two different ways inside one paragraph. The bolded statement reads:

**v1.x is in maintenance mode and will only receive security fixes from now on**

The same paragraph then says something else about that same 1.x line.

continues to receive critical bug fixes and security patches

Whether this means security fixes alone, or also critical bug fixes, does not resolve. A beta announcement published by the same organization on June 29, 2026, states the latter. Therefore, adopting the latter interpretation is more consistent, although the bolded statement is more prominently displayed.

This discrepancy can influence decision-making. Whether to postpone migration to v2 depends on whether v1.x will continue to receive bug fixes. If interpreted as only including security fixes, the priority for migration increases.

Regarding the TypeScript SDK, the same beta announcement states that it will continue to receive bug fixes and security updates for at least six months for v1.x. There is no equivalent timeframe specified for the Python SDK.

10.8 General Principles for Interpretation

Lay the seven side by side and they fall into three shapes.

First, there is a difference in precision based on the document's purpose. Documents intended for announcements or overviews may shorten lists, round durations, and omit examples. Sometimes the document does not say that it shortened anything. Documents that aim to define constraints are generally more precise. Sections 10.1, 10.2, 10.3, and 10.6 fall into this category. Treat the dedicated page, the registry, and the SEP itself as authoritative, and use blogs and overview pages for orientation.

Second, the two documents bind different parties. Sections 10.4 and 10.5, in isolation, do not appear to contradict each other. The specifications that define the standards imposed on the MCP server and the descriptions a hosting platform gives of its own behavior happen to use the same names and the same numbers. An implementer has to satisfy both, so neither drops off the checklist.

Third, there is a situation where a single document contains conflicting information. Section 10.7 exemplifies this, because it gives two different answers in the same paragraph. Comparing this one against other documents does not settle it. The only way through is to look at how other documents refer to it, and at the dates, and decide which of the two was written later.

11. Migration Sequence

11.1 Principles for Determining Order

Two principles set the order of migration.

First, ensure both versions can operate simultaneously before proceeding. The backward compatibility design of these versions allows for migration to occur incrementally. By reaching a point where both versions can function together, you gain the freedom to choose which version to upgrade first.

Second, clear the quiet failures first. Anything that fails with an error surfaces during the migration. Anything that is merely ignored, or that only changes a default, does not.

11.2 Stages

Stage 0. Current Inventory. List everything your implementation depends on. Chapter 15 carries the checklist for exactly this. Specifically, verify the session state, path control based on Mcp-Session-Id, the matching of -32002, the call to logging/setLevel, the use of GET streams, resumption using Last-Event-ID, the list that changes based on connection history, and the usage of Roots, Sampling, and Logging.

Stage 1. Major SDK Version Update. If you are using Python and TypeScript, perform this task independently. Treat this as a separate change from the version switch. For SDKs where a version update alone changes communication behavior, this stage and the next stage cannot be separated. In such cases, prepare for Stage 2 before performing the version update.

Stage 2. Move application state onto explicit handles. This is the stage with the most work in it. Replace the items currently associated with sessions with identifiers returned by a creation tool. You design the handle's lifetime, its expiry, and its permission check yourself. The protocol provides none of them. This stage can be performed even with the old version, so it's best to complete it before the version switch.

Stage 3. Decouple Transport Layer Dependencies. Migrate GET streams, resumption using Last-Event-ID, resources/subscribe, ping, and logging/setLevel to their respective replacements. If you are using server-initiated requests, migrate them to multi round-trip requests. Design the integrity protection for requestState at this stage.

Stage 4. Handle Request-Specific Metadata and Headers. The client should send the two required _meta fields, as well as MCP-Protocol-Version, Mcp-Method, and Mcp-Name. The server-side component that processes the body should verify the consistency between the header and the body.

Stage 5. Add Hints for List Caching. Add ttlMs and cacheScope to the results of the six operations described in Section 10.1, specifically the complete results. Whether or not you can use public for cacheScope depends on whether the resulting data is restricted by authorization.

Stage 6. Advertise both revisions. Ensure the server is capable of supporting both the old and new versions. For AgentCore Gateway, include both versions in the supportedVersions list. That setting is replaced by the set you send, so read the current configuration first and then send the complete list (Section 9.1). At this point, the client has not yet changed.

Stage 7. Move the clients. Switch the client to request the new version. Ensure that it can still revert to the old version.

Stage 8. Drop the old revision. After confirming that all clients are requesting the new version, remove the old version from the advertised list.

Stage 9. Strip the deprecated features. This step can be performed independently of the previous steps. Plan according to the timeline outlined in Section 6.4. Prioritize HTTP+SSE, as it has a shorter timeframe.

11.3 Areas Where the Order Can Be Adjusted, and Areas Where It Must Not Be

Stages 2 and 3 can be swapped. Both can be done while still on the old revision.

Stage 7 must not come before Stage 6. If a client requests a version that the server is not advertising, the request will be rejected.

Stage 7 must be complete before Stage 8. Dropping an advertised revision first stops the clients that have not migrated yet.

Stage 5 belongs before Stage 6. Cache hints are a requirement of the new revision, so adding them after you advertise it is the wrong way round.

12. Determining Parallel Operation

12.1 Defining Eras

The specification defines three terms for talking about backward compatibility.

TermDescription
ModernA revision that carries the version, the identity, and the capabilities as per-request metadata. 2026-07-28 and later.
LegacyA revision that establishes a session through the initialize handshake. 2025-11-25 and earlier.
Dual-eraAn implementation that supports both.

12.2 Outcomes by Combination

The specification carries a matrix of what happens for each combination of client and server era. There are clear distinctions between configurations that work and those that do not.

Modern clients and Legacy servers are incompatible. The server may reject the request with an implementation-defined error, stay silent, or process an era-ambiguous method under legacy semantics. The third outcome is the dangerous one, so on stdio the recommendation is to send server/discover first and fail deterministically.

Legacy clients are also incompatible with Modern servers. Legacy clients lack mechanisms for progressing to newer versions. For that reason the specification says a server that speaks only modern revisions should name the versions it supports inside the error it returns to initialize. That message is the only diagnostic a legacy client can put in front of a user.

Dual-era clients work with both Modern and Legacy servers. Legacy clients work with Dual-era servers.

Therefore, if you are planning a migration of either the client or server, it is safer to migrate the one to a dual-era configuration first.

12.3 The Era Probe Differs by Transport

On HTTP, send a modern request first. If a 400 Bad Request comes back, read the response body before deciding. A modern server also uses 400: for an unsupported version, for a missing required client capability, and for a header validation failure.

If the response body carries a recognized modern JSON-RPC error, the server speaks a modern revision. The client should either retry with the advertised version or modify the request. It must not revert to an older version. If the body is empty or does not conform to a known format, the client should fall back to the older version and send an initialize request.

stdio has no status code, so send server/discover first. The result can take three forms. A DiscoverResult means the server is modern. If a known, new format error is returned, the server is using a newer version but is not compatible with the requested version. Otherwise, if an unexpected error occurs, or if there is no response within a reasonable timeframe, the server is using an older version.

Do not base the version determination on specific error codes. The specification explicitly states this. Older server versions may return implementation-dependent errors, or return nothing at all, when encountering unknown requests before the handshake process.

12.4 How Long the Determination Holds

The era determination is a property of the server, not of an individual request. The specification says the result should be cached for the lifetime of the server process on stdio, and for the lifetime of the origin on HTTP. It may be kept across restarts of the same server configuration, and re-probed if the cached assumption later turns out to be wrong.

13. Items to Verify After Migration

The detail of verification methods belongs to an existing article, so only the items specific to the migration are listed here. See MCP Server Testing and Debugging Guide for information on using the Inspector, setting up integration tests, and performing regression testing in the CI environment. That article, however, contains verification steps that assume a session identifier, so parts of the procedure no longer apply under 2026-07-28.

There are six verification items specific to the migration.

Required fields. Confirm that a request omitting either required _meta field draws -32602. Confirm that a request whose header and body disagree draws -32020.

Version negotiation. Request an unsupported revision and confirm that -32022 comes back with the supported list in the body.

List independence from the connection. Call tools/list on separate connections and confirm the same result. Confirm that only a change of authorization changes it.

Handle lifetime. Verify that an explicit handle still works after the connection is closed and reopened. This is exactly where the session-scoped version broke. Not breaking here is the point of the migration.

Multi round-trip. Tamper with requestState and confirm the server rejects it. Present state minted for a different principal and confirm the server rejects that too.

Cache hints. Confirm that all six operations attach ttlMs and cacheScope to their complete results. Confirm that a result filtered by authorization does not carry public.

14. Failure Modes and Anti-Patterns

Eight failure modes are listed below. Each is the inverse of something described in an earlier chapter.

Reading the removal of sessions as a ban on state. Application layer states are not prohibited. See Section 5.3.

Reading a platform route-pinning header as a protocol session. A header the hosting platform attaches is outside the specification. See Sections 5.4 and 10.4.

Searching for an explicit handle as a protocol feature. There are no corresponding methods or schemas. You build it as part of the tool design. See Section 4.2.

Rushing to strip a feature because it is deprecated. The affected features work fully in this revision. See Section 6.1.

Planning as if twelve months were a uniform grace period. HTTP+SSE has a three-month timeframe, and there are exceptions for expedited removal. See Section 6.4.

Copying a request example straight from a blog. Two required fields are missing. See Section 10.3.

Matching on -32002 as a standalone condition. A resource-not-found error from an older server and an authorization failure from the hosting platform can arrive under the same code. See Section 10.5.

Using the TTL as a polling interval. The specification says explicitly not to. See Section 4.4.

15. Migration Checklist

The items below are what to check during the inventory stage. If one applies, go back to the section named beside it.

  • Application state is attached to a session (Section 4.2)
  • Sticky routing is configured based on Mcp-Session-Id (Section 5.4)
  • Control mechanisms exist to accept other methods only after initialize is received (Section 3.1)
  • An HTTP GET request is opening a stream for server-sent messages (Section 3.3)
  • Stream resumption using Last-Event-ID is implemented (Section 3.4)
  • The server sends JSON-RPC requests of its own (Section 3.5)
  • ping is used for connection liveness checks (Section 3.6)
  • logging/setLevel is being called (Section 3.6)
  • resources/subscribe is being used (Section 4.3)
  • The results of tools/list are affected by connection history (Section 3.7)
  • -32002 is used as an error-matching condition (Section 9.4)
  • Roots, Sampling, or Logging are being used (Section 6.2)
  • HTTP+SSE transport is being used (Section 6.4)
  • Client registration is performed using Dynamic Client Registration (Section 6.3)
  • The list response does not include ttlMs and cacheScope (Section 4.4)
  • MCP-Protocol-Version, Mcp-Method, and Mcp-Name are not being sent (Section 4.5)
  • Although the body is being processed, header and body consistency is not being verified (Section 4.5)
  • Behavior is being branched based on the value of clientInfo (Section 4.1)
  • Which revision the SDK speaks by default has not been checked (Section 8.1)
  • The tier of the SDK being used has not been verified (Section 8.3)

16. Frequently Asked Questions

16.1 Does the server have to move to 2026-07-28 right away?

No. Servers and clients running 2025-11-25 or earlier keep working as they are. The arrival of this revision does not shut the older ones off. The SDK beta announcement states plainly that July 28 is the date the normative specification text is published, and not a switch-off date for implementations that depend on the current revision.

16.2 Can an application still hold state?

Yes. Only the protocol-layer session is gone; state itself is not forbidden. Carts, browser instances, and database transactions can all be kept across calls. What you attach them to moved from an implicit session to an explicit identifier. Sections 4.2 and 5.3 have the detail.

16.3 How is an explicit handle implemented?

As tool design. A creation tool returns an identifier, and later tools accept it as an argument. There is no matching method and no schema on the protocol side. The handle's lifetime, how it expires, and how permission is checked are all decided on the server side. See Section 4.2.

16.4 Are Roots, Sampling, and Logging still usable?

Yes. All three are Deprecated, and all three work fully in this revision. New implementations should not adopt them, and existing implementations should migrate before the earliest removal. That point is the first revision released on or after 2027-07-28. See Sections 6.2 and 6.4.

16.5 Is the grace period always twelve months?

No. There are two exceptions. The earliest removal for the HTTP+SSE transport is three months after SEP-2596 reaches Final. The reason is that the revision recorded as deprecating it is 2025-03-26. The second exception applies to expedited removal, which may shorten the twelve-month timeframe if there is a published security advisory or active exploitation that cannot be mitigated immediately. Expedited removal still leaves at least ninety days. See Section 6.4.

16.6 Does server/discover have to be called?

No. On the client side it is optional. On the server side, implementing it is mandatory. A client may send the request it actually wants without calling it first, take a -32022 if the revision does not match, and pick again from the advertised list. If there is any chance of reaching a legacy server over stdio, call it first as the era probe. See Section 4.7.

16.7 Why does the Mcp-Session-Id header appear in the response?

Because the hosting platform may be using it on its own account. The specification removed this header from Streamable HTTP, and what it removed is the protocol-level meaning. Amazon Bedrock AgentCore Runtime explicitly states that this header is used for routing to microVMs and is always returned, regardless of the mode. See sections 5.4, 9.3, and 10.4.

16.8 Which should be migrated first, the client or the server?

Either. Version selection happens per request, so once a server speaks both revisions the clients can move at their own pace. Whichever side moves first is safer as a dual-era implementation. See sections 11.2 and 12.2.

16.9 Does upgrading the SDK alone start speaking the new revision?

It depends on the SDK. C# inverted its default, so an upgrade alone changes behavior. Go requires an explicit setting, so an upgrade alone does not. Python handles both revisions with no configuration. For TypeScript, no statement to that effect was found in the stable release notes, so confirm it against the version you pin. See Section 8.1.

16.10 Can a server still elicit input?

Yes. What changed is the delivery. Instead of sending an independent request, the server embeds the request in a result marked input_required, and the client re-sends the original request with the answer attached. See Section 4.6.

16.11 Is it safe to cache a list result?

Yes. In this revision a list no longer varies by connection, so reuse across connections holds. ttlMs gives how long the response stays fresh, and cacheScope decides who may share it. The TTL is a hint rather than a guarantee, and it is not a polling interval. See Section 4.4.

16.12 Where are the primary sources?

The specification itself, at modelcontextprotocol.io, for the revision in question. The versioning page carries the revision's state, the changelog carries the list of changes, and the deprecated features registry carries what is Deprecated. For details on SDK behavior, consult the release notes for each individual SDK. The official blog and overview pages can be helpful for general guidance, but they may omit specific details, such as lists, timelines, and examples. See Section 10.8.

17. Summary

What 2026-07-28 changed is where state is kept. It is not speed, and it is not the number of features. The revision took away the implicit container in the protocol layer and left an explicit identifier in its place.

The key consideration when reviewing this update is to consistently verify which layer a reference to "session" is referring to. The protocol-layer session has been removed. The application-layer state has not disappeared. The platform-layer identifier sits outside the specification, and each hosting platform decides it. Plan a migration with these three mixed together and the estimate comes out wrong by a multiple.

The changes fall into two categories. Modifications to the transport layer take effect immediately. The deprecation of certain features has a grace period. The length of this grace period varies by feature, and HTTP+SSE has the shortest: three months after SEP-2596 reaches Final. Twelve months is not an absolute guarantee and exceptions for expedited removal may apply.

The behavior of the SDKs is what actually sets the order of migration. Some SDKs will change communication simply by upgrading the package, while others require explicit configuration. For SDKs with inverted defaults, the update itself may result in a change to production behavior.

There were seven discrepancies found between the primary documentation sources. On the number of operations that must carry cache hints, the dedicated page says six, the changelog says five, and the official blog says four. The request examples provided in the official blog and the AWS blog are missing two fields that the specification designates as mandatory. Documentation intended for announcements often simplifies lists and rounds durations; for understanding the actual implementation details, refer to the dedicated pages, the registry, and the SEP itself.

One last thing about what this revision moved onto the implementer. The design of state. The protocol let go of the decisions a session used to carry: how long it lives and how widely it is shared. What was let go now has to be designed as the handle's lifetime, as the rule for expiry, and as the permission check. What the specification stopped providing is the container. It is not the state itself.

18. References



References:
Tech Blog with curated related content

Written by Hidekazu Konishi