
What the MCP specification 2026-07-28 changes
MCP (Model Context Protocol) is the shared contract that lets an AI tool call an external server in a standard way. The Model Context Protocol specification names each revision by a date.
The changelog for 2026-07-28 lists what changed since 2025-11-25. To check the latest MCP version, look at the versioning page on the MCP site.
The theme of the revision is removing state. Under the 2025-11-25 design, a client connected, exchanged capabilities through initialize, and sent every later request on top of that shared understanding.
The 2026-07-28 revision drops that arrangement. The MCP protocol still defines how a model reaches tools and data, but each request now carries what the server needs to answer it. For a developer, the practical question becomes which of your assumptions about earlier requests no longer hold.
The removals and their replacements at a glance
The table below lists the main removals and what takes the place of each. Every row appears under the changelog's major changes, and the later sections take them one at a time.
Main removals in the 2026-07-28 revision (per the official changelog)
| Removed | Replacement |
|---|---|
Protocol-level sessions and the Mcp-Session-Id header |
Server-minted handles passed as ordinary tool arguments |
The initialize and notifications/initialized handshake |
Version and capabilities in _meta on every request |
The HTTP GET endpoint, resources/subscribe and resources/unsubscribe |
subscriptions/listen, one long-lived POST-response stream |
ping, logging/setLevel and notifications/roots/list_changed |
For logging/setLevel, a log level set per request in _meta |
Server-initiated requests such as sampling/createMessage |
MRTR: the server returns a result and receives the answer on a retry |
| SSE stream resumability and message redelivery | A new request with a new request ID |
In the session, handshake and log-level rows, what the protocol used to remember now travels in the request or in the application. The remaining rows swap one mechanism for another.
This document lists changes made to the Model Context Protocol (MCP) specification since the previous revision, 2025-11-25. / Remove protocol-level sessions and the Mcp-Session-Id header from the Streamable HTTP transport. List endpoints (tools/list, resources/list, prompts/list) no longer vary per-connection. Servers that need cross-call state use explicit, server-minted handles passed as ordinary tool arguments (SEP-2567). — From the statements on which revision this covers, and on the removal of sessionsView official source →
Stateless MCP: no session, no initialize handshake
Stateless MCP means the protocol itself keeps no state between requests, and anything a server needs to remember arrives with the request. Two removals make that true. The MCP session disappears, and so does the initialize handshake that used to open it.
Consider a server that keeps a user's draft order under the session ID. That lookup has nothing to key on anymore, so the draft has to be tracked some other way.
State now travels as a handle in tool arguments
When a server still needs state across calls, the specification gives it one route. The server creates an explicit handle, and the client passes that handle back as an ordinary tool argument. For the draft order, the server would create a handle for the draft and expect it in each later call that edits or submits the order.
The reference to the state is now visible in the call itself, not hidden in a connection. Because the server mints the handle, its format is the server's choice. How long a handle lives and what happens when one is missing are questions your tool design has to answer.
Servers that need cross-call state use explicit, server-minted handles passed as ordinary tool arguments (SEP-2567). — From the statement on passing cross-call state as handles in ordinary argumentsView official source →
Version and capabilities travel in every request
With the MCP initialize exchange removed, version negotiation moves into the requests. Every request carries its protocol version and client capabilities in _meta.
Clients SHOULD identify themselves on each request, and servers SHOULD identify themselves in each result's _meta. When the versions do not match, the server returns UnsupportedProtocolVersionError.
The revision also adds a call named server/discover, and servers MUST implement it. It reports the protocol versions the server supports, its capabilities and its identity.
A client MAY call it before any other request to choose a version up front. On STDIO (the transport that talks over standard input and output), a client can also use it as a backward-compatibility probe. The replacement for the MCP handshake is the per-request _meta fields, and server/discover is an optional way to learn a server's versions, capabilities and identity before the first request.
Make MCP stateless: remove the initialize/notifications/initialized handshake. Every request now carries its protocol version and client capabilities in _meta (io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities). Clients SHOULD identify themselves on each request (io.modelcontextprotocol/clientInfo), and servers SHOULD identify themselves in each result's _meta (io.modelcontextprotocol/serverInfo). Version mismatches return UnsupportedProtocolVersionError (SEP-2575). / Add server/discover: servers MUST implement this RPC to advertise their supported protocol versions, capabilities, and identity. Clients MAY call it before any other request for up-front version selection, or use it as a backward-compatibility probe on STDIO (SEP-2575). — From the statements on going stateless and on adding server/discoverView official source →
MCP transport changes: lists, caching and streams
The MCP transport is the layer that carries messages between client and server, and this revision changes three things in it. Lists stop varying per connection, one stream takes over change notifications, and a broken response stream can no longer be resumed. The changelog credits the list change to SEP-2567, the session removal, and the other two to SEP-2575, the proposal that removes the handshake.
Lists stay the same per connection and carry cache hints
The three list calls, tools/list, resources/list and prompts/list, no longer return different results depending on the connection. Two connections asking the same question now get the same answer, which lets a client store it. The changelog adds a recommendation that servers return tools from tools/list in a deterministic order, to enable client-side caching and improve LLM prompt cache hit rates.
Two fields support that caching. Results of the list calls, resources/read and resources/templates/list now require ttlMs and cacheScope, defined through a new CacheableResult interface. ttlMs is a freshness hint in milliseconds, and it lets a client cache a response and poll less often. cacheScope is either public or private, and it controls whether shared intermediaries may cache the response.
Servers SHOULD return tools from tools/list in a deterministic order to enable client-side caching and improve LLM prompt cache hit rates. / Require ttlMs and cacheScope fields on results returned by tools/list, prompts/list, resources/list, resources/read, and resources/templates/list via a new CacheableResult interface. ttlMs is a freshness hint (in milliseconds) allowing clients to cache responses and reduce polling; cacheScope ("public" or "private") controls whether shared intermediaries may cache the response. — From the statements on deterministic list ordering and the new caching fieldsView official source →
One listen stream replaces the GET endpoint
A client that wants to hear about changes now opens subscriptions/listen, a single long-lived POST-response stream. It replaces the HTTP GET endpoint along with resources/subscribe and resources/unsubscribe.
The client opts in to the kinds of change it wants: toolsListChanged, promptsListChanged, resourcesListChanged and resourceSubscriptions. The server acknowledges the request and tags each notification with a subscription ID.
One boundary matters when you update a client. Request-scoped notifications such as notifications/progress and notifications/message do not use the listen stream.
They keep flowing on the response stream of the request they belong to. A client therefore reads two kinds of stream: the listen stream for changes, and each request's own stream for progress and messages.
Replace the HTTP GET endpoint and resources/subscribe/resources/unsubscribe with subscriptions/listen: a single long-lived POST-response stream for opted-in server-to-client change notifications. Clients opt in to specific types (toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions); the server acknowledges and tags notifications with io.modelcontextprotocol/subscriptionId. Request-scoped notifications such as notifications/progress and notifications/message continue to flow on the response stream of the request they relate to, not the subscriptions/listen stream (SEP-2575). — From the statements on the new notification path and on request-scoped notificationsView official source →
A broken stream means a new request ID
SSE (server-sent events) stream resumability is gone from the Streamable HTTP transport, and so is message redelivery. A client can no longer pick up a stream where it stopped by sending the Last-Event-ID header. If a response stream breaks, the in-flight request is lost, and the client MUST send it again as a new request with a new request ID.
A client that used to resume a half-finished response now has to repeat the whole request. The server side should therefore be ready for the same work to arrive twice.
Remove SSE stream resumability and message redelivery (the Last-Event-ID header and SSE event IDs) from the Streamable HTTP transport. A broken response stream loses the in-flight request; clients MUST re-issue it as a new request with a new request ID (SEP-2575). — From the statement on dropping stream resumabilityView official source →
MCP sampling and other server asks now use MRTR
Sampling is the way an MCP server asks the client to run a model completion on its behalf. roots/list asks the client for the locations it exposes, and elicitation/create asks the user for more input. All three used to be requests that the server sent to the client in the middle of its work. The revision replaces that model with Multi Round-Trip Requests (MRTR).
The official release-candidate post has a section titled "Roots, Sampling, and Logging Are Deprecated". What replaces them, and how, is in the changelog's MRTR entry below.
How the input_required round trip works
Suppose a tool needs the user to confirm which of two accounts to use. Before, the server sent elicitation/create while the call was still running.
Now the server returns a result whose resultType is input_required, and its inputRequests field lists what it needs. The client collects the answer and sends the original request again with inputResponses attached.
Under MRTR the server does not interrupt a running call. It ends the call with an input_required result, and the client sends the request again with the missing pieces, so each request still maps to one response.
Asking for more information: the old way versus MRTR
| Stage | Previously (server-initiated) | MRTR |
|---|---|---|
| Trigger | The server pauses and sends a request | The server returns a result with resultType set to input_required |
| Where the ask lives | roots/list, sampling/createMessage, elicitation/create |
The result's inputRequests field |
| The client's answer | A reply to the server's request | The original request, sent again with inputResponses |
To support this, every result now requires a resultType field. It is complete for ordinary results and input_required for interim MRTR results. A client MUST treat a result from an earlier-protocol server that omits the field as complete.
Multi Round-Trip Requests (MRTR) pattern introduced which replaces the previous approach of sending server-initiated requests, such as roots/list, sampling/createMessage, or elicitation/create. Servers return an InputRequiredResult (resultType: "input_required") whose inputRequests field carries the requests for the additional information needed to process the request. Clients respond with inputResponses on a retry of the original request providing the requested information. (SEP-2322). / All results now carry a required resultType field: "complete" for ordinary results and "input_required" for multi round-trip request interim results. Clients MUST treat results from earlier-protocol servers that omit the field as "complete" (SEP-2322). — From the statements introducing MRTR and requiring the resultType fieldView official source →
MCP tasks, extensions and authorization in the revision
Three more topics sit around the core changes: tasks, extensions and authorization. The changelog spells out the change to tasks. Extensions and authorization appear as section titles in the official release-candidate post, and the pointers below lead there.
The official release-candidate post has a section titled "Extensions Become First-Class", and a sub-section on MCP Apps, described in its heading as server-rendered user interfaces. That post is the place to read the detail on both: The 2026-07-28 MCP Specification Release Candidate.
Tasks become an official extension
Tasks, the experimental mechanism for long-running work, moved out of the core protocol and into an official extension named io.modelcontextprotocol/tasks. The extension is also redesigned. The blocking tasks/result method is replaced by polling through tasks/get.
A new tasks/update carries input from client to server, and tasks/list is removed. Servers may also return task handles unsolicited, without per-request opt-in. For a client that waits on a long job, this means checking the task by polling instead of holding a call open until the result arrives.
Move experimental tasks out of the core protocol and into an official extension (io.modelcontextprotocol/tasks). The redesigned extension replaces the blocking tasks/result method with polling via tasks/get and a new tasks/update for client-to-server input, removes tasks/list, and allows servers to return task handles unsolicited without per-request opt-in (SEP-2663). — From the statement on moving tasks into an extension and redesigning itView official source →
Authorization is also covered in the revision
MCP authorization is part of this revision, and its details sit in the official post and in a third-party write-up. The official release-candidate post has a section titled "Authorization Hardening".
A third-party write-up from Scalekit has a section on MCP OAuth and EMA, which its FAQ expands as Enterprise-Managed Authorization. If your server handles sign-in, read those sections directly before planning the upgrade: MCP Spec Update July 28, 2026.
What breaks in an existing MCP server
The checks below run from the changes that reshape how a server is built to the ones a code search will find. Start with the first.
Session ID and dropped-connection recovery
First, check for anything that depends on the session ID. A server that reads the Mcp-Session-Id header to look up state will not work as written. Rewrite it so the server creates a handle and the tool takes that handle as an argument.
Second, check recovery after a dropped connection. Because stream resumability is gone, a client cannot continue with Last-Event-ID.
It has to send the request again with a new request ID. Review any retry path that assumed a half-finished call could continue.
Removed calls, error codes and looser schemas
Third, search for the removed calls: ping, logging/setLevel and notifications/roots/list_changed. Logging is now chosen per request, with the client putting io.modelcontextprotocol/logLevel in _meta. A server that gets a request without that field MUST NOT send notifications/message for it.
Fourth, update the error code for a missing resource. It changed from -32002 to -32602 (Invalid Params), which aligns it with the JSON-RPC specification, the message format MCP is built on. The changelog also splits the JSON-RPC server-error range. -32000 to -32019 stays implementation-defined, and -32020 to -32099 is reserved for the MCP specification.
Fifth, loosen any schema validation you wrote against the old limits. Both inputSchema and outputSchema now allow any JSON Schema 2020-12 keywords, and structuredContent allows any JSON value.
For a live server, check the session ID and the dropped-stream retry path first, because both change how the server behaves. The remaining MCP specification changes can be found by searching the code for the names listed above.
Related articles on the setup around an AI model: ARC AGI 3: Two Settings Tripled OpenAI's Score and Context Engineering: Claude Code's Prompt Cut 80%+. For an MCP server seen from the user's side, see X MCP Server: Give Claude and Grok the X API. When a rewritten tool starts returning handles or error payloads, a JSON formatter makes it easier to confirm that the structure matches what you meant to send.
Remove ping, logging/setLevel, and notifications/roots/list_changed. Log level is now set per-request via io.modelcontextprotocol/logLevel in _meta; servers MUST NOT emit notifications/message for requests that did not include this field (SEP-2575). / Change resource not found error code from -32002 to -32602 (Invalid Params) to align with JSON-RPC specification. / Define an error code allocation policy partitioning the JSON-RPC server-error range: -32000 to -32019 remains implementation-defined (existing SDK usage is grandfathered), -32020 to -32099 is reserved for the MCP specification. / Loosen inputSchema and outputSchema to allow any JSON Schema 2020-12 keywords, and structuredContent to allow any JSON value. — From the statements on removed calls, the error code change and allocation policy, and looser schema constraintsView official source →
FAQ
- What changed in the MCP 2026-07-28 revision?
- The biggest change is that protocol-level sessions are gone and MCP is now stateless. The Mcp-Session-Id header was removed from the Streamable HTTP transport, and list endpoints such as tools/list no longer return different results per connection. Servers that need state across calls now pass server-minted handles as ordinary tool arguments.
- Is the initialize handshake no longer required?
- It is gone. The initialize and notifications/initialized exchange was removed, and every request now carries its own protocol version and client capabilities in _meta. If the versions do not line up, the server returns UnsupportedProtocolVersionError.
- How do servers ask the client for something now?
- Server-initiated requests were replaced by a pattern called MRTR (Multi Round-Trip Requests). Instead of the server sending its own request, it returns a result whose resultType is input_required, with the information it needs listed in inputRequests. The client then re-sends the original request with inputResponses attached.
- Can a broken connection be resumed?
- No. SSE stream resumability and message redelivery were removed from the Streamable HTTP transport. When a response stream breaks, the in-flight request is lost, and the client has to issue it again as a new request with a new request ID.
- Do clients still work with servers built for an earlier revision?
- The changelog speaks to two points. A client must treat a result from an earlier-protocol server that omits resultType as complete, and server/discover can serve as a backward-compatibility probe on STDIO. For everything else, check each removal against your own code.



