New batches starting this week Β· Limited seats

MCP Goes Stateless: What the 2026-07-28 Specification Changes for Server and Client Developers

The 2026-07-28 MCP revision makes the protocol stateless. This guide explains every major change, how old and new clients coexist, and the migration checklist for server and host authors.

MCP before and after the 2026-07-28 spec: from initialize handshake and session IDs to per-request version and capabilities that any server instance can serve
Last updated Β· 13 min read Β· 2,828 words

The 2026-07-28 revision of the Model Context Protocol is the biggest change since MCP launched. MCP is now a stateless protocol: there is no initialize handshake and no Mcp-Session-Id, every request carries its own protocol version and client capabilities in _meta, and servers no longer send requests to clients mid-call. They return an input_required result and the client retries instead. Sampling, roots and logging are deprecated, Tasks have moved to an extension, and Dynamic Client Registration gives way to Client ID Metadata Documents. This guide explains each change, how old and new clients coexist, and what to migrate first.

New to the protocol? Start with What Is MCP?. Everything below was checked against the 2026-07-28 specification, its changelog and the official SDK migration guides.

Why statelessness matters

Under 2025-11-25 and earlier, a client opened a session with initialize. Over Streamable HTTP the server could hand back an Mcp-Session-Id, and every later request had to reach something that knew that session. Servers could also push requests back to the client (sampling, elicitation, roots) on an open stream.

Consider an insurer that runs a claims-lookup MCP server as several pods behind a load balancer. With sessions, the team had three options. Sticky routing breaks when a pod is rescheduled. A shared Redis session store adds latency and one more thing to secure. A single replica can't scale. Serverless was worse still, because no function instance outlives the request.

The new spec states the rule outright: everything needed to process a request is in the request, and servers must not rely on earlier requests on the same connection for version, capabilities or client identity. The practical results:

  • Load balancing. Any replica can serve any request, so you can drain pods freely and use plain round-robin.
  • Serverless. A function that answers one POST and exits is now a valid MCP server design.
  • Routing. Streamable HTTP now requires Mcp-Method and Mcp-Name headers (SEP-2243), so gateways can route or block by tool name without parsing the body.
  • Caching. List and read results now carry ttlMs and cacheScope (SEP-2549), and tools/list should return tools in a deterministic order.

Before and after: the request flow

BEFORE (2025-11-25): stateful session
client                          server pod A
  |-- initialize ---------------->|
  |<-- capabilities + session id -|
  |-- notifications/initialized ->|
  |-- tools/call (session id) --->|  must reach pod A
  |<-- elicitation/create (push) -|  or a shared store
  |-- elicitation response ------>|
  |<-- tool result ---------------|

AFTER (2026-07-28): stateless requests
client                          any pod
  |-- server/discover (optional)->|
  |-- tools/call + _meta -------->|  pod A answers
  |<-- input_required + state ----|
  |-- tools/call + _meta          |
  |   + inputResponses + state -->|  pod B is fine
  |<-- resultType: complete ------|

The major changes, one by one

1. The initialize handshake is gone; _meta travels on every request

SEP-2575 removes initialize and notifications/initialized. Every request now carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in params._meta. Clients should also send clientInfo, and servers should return serverInfo in each result's _meta. On HTTP the version is also sent in the MCP-Protocol-Version header, but the body is the source of truth. If the two disagree, the server returns a HeaderMismatch error.

Every result now carries a required resultType: "complete" or "input_required". Clients must treat a missing resultType from an older server as "complete". The spec's own error codes move to a reserved range. For example, UnsupportedProtocolVersion is now -32022, and "resource not found" becomes the standard -32602.

2. server/discover

Servers must implement server/discover. It returns supportedVersions, capabilities, optional instructions and the server's identity, and the result can be cached. Clients may call it first, but they don't have to. A client can send any request with its preferred version and handle UnsupportedProtocolVersionError. The spec warns that serverInfo is self-reported and should not drive security decisions.

3. Mcp-Session-Id removed; state lives in handles

SEP-2567 removes protocol-level sessions. A modern-only server ignores an Mcp-Session-Id header from an old client and answers HTTP GET or DELETE with 405. SSE resumability (Last-Event-ID) is gone too. If a stream breaks, the client re-sends the request with a new ID.

When a server really needs state across calls (a basket, a browser context, a database transaction), the tools page recommends explicit handles. A creation tool returns an opaque ID, and later calls pass it as an ordinary argument. The spec's design advice: check authorization against the handle on every call, keep handles opaque, state their lifetime in the tool description, and return a clear error when one has expired so the model can create a new one.

4. Multi Round-Trip Requests and input_required results

Servers no longer send elicitation/create, sampling/createMessage or roots/list as separate requests. Under the Multi Round-Trip Requests pattern (SEP-2322), a server that needs more input during tools/call, resources/read or prompts/get returns an InputRequiredResult. That result holds inputRequests and, optionally, an opaque requestState. The client gathers the answers and retries the original request with a new JSON-RPC ID, the inputResponses, and the unchanged requestState.

Because the retry carries everything the server needs, it can land on a different replica. The catch is that requestState passes through the client, so the spec says to treat it as attacker-controlled. If it affects authorization or business logic, protect its integrity with an HMAC or AEAD, and bind it to the user, a short expiry and the original request.

5. Subscriptions: one long-lived listen stream

subscriptions/listen replaces the HTTP GET stream and resources/subscribe. The client opts in to specific types: toolsListChanged, promptsListChanged, resourcesListChanged, or a list of resourceSubscriptions. The server first sends an acknowledgment listing what it will honour, then tags every notification with io.modelcontextprotocol/subscriptionId. Progress and log messages still travel on their own request's stream. Servers keep no subscription state across reconnects, so clients must re-subscribe.

6. Removals and deprecations

ping, logging/setLevel and notifications/roots/list_changed are removed. Log level is now set per request with io.modelcontextprotocol/logLevel in _meta, and servers must not emit logs for requests that didn't include it. SEP-2577 deprecates Roots, Sampling and Logging: they still work, but new implementations should not adopt them.

Deprecated featureSuggested migrationEarliest removal
RootsTool parameters, resource URIs or server configurationFirst revision on or after 2027-07-28
SamplingCall your LLM provider's API directlyFirst revision on or after 2027-07-28
Loggingstderr on stdio; OpenTelemetryFirst revision on or after 2027-07-28
Dynamic Client RegistrationClient ID Metadata DocumentsFirst revision on or after 2027-07-28

Elicitation is not deprecated. It now travels inside MRTR. The old HTTP+SSE transport, deprecated since 2025-03-26, is also formally listed as Deprecated. This revision adds a feature lifecycle policy (SEP-2596) with three states, Active, Deprecated and Removed. A deprecated feature normally stays at least twelve months, and a public registry tracks every deprecated feature. For enterprise teams, that predictability matters more than any single feature.

7. Tasks moved to an extension

Experimental Tasks have left the core protocol and become the official io.modelcontextprotocol/tasks extension (SEP-2663), which clients and servers negotiate through the new extensions field in capabilities. The redesign replaces the blocking tasks/result with polling through tasks/get, adds tasks/update, and removes tasks/list. Check SDK support before you depend on it. The Python SDK v2 migration guide says that SDK does not implement the extension yet.

8. Authorization updates

Authorization still covers HTTP transports only, built on OAuth 2.1, Protected Resource Metadata (RFC 9728) and Resource Indicators (RFC 8707). The verified changes:

  • Client ID Metadata Documents (CIMD) are preferred. The client's client_id is an HTTPS URL that serves a JSON document containing at least client_id, client_name and redirect_uris. Authorization servers advertise support with client_id_metadata_document_supported. The priority order is pre-registration, then CIMD, then DCR, then asking the user.
  • Dynamic Client Registration is deprecated (PR #2858). It remains as a fallback, and clients using it must send an appropriate application_type (SEP-837).
  • Issuer validation (SEP-2468). Authorization servers should include iss in authorization responses (RFC 9207), and clients must check it against the recorded issuer before redeeming the code.
  • Issuer-bound credentials (SEP-2352). Clients key stored credentials by issuer and must re-register when the authorization server changes.

For delegated access and token exchange with an enterprise identity provider, see AI agent identity and access.

9. Tool annotations and tool definitions

The annotation hints are unchanged: title, readOnlyHint, destructiveHint, idempotentHint and openWorldHint. Clients must still treat them as untrusted unless they come from a trusted server. What did change: schemas now accept any JSON Schema 2020-12 keyword (SEP-2106), and the new x-mcp-header property copies a primitive parameter into an Mcp-Param-{name} HTTP header for routing. Never mark sensitive values this way, because intermediaries can see headers. Clients must drop tools whose x-mcp-header values are invalid.

Version negotiation and coexistence with older clients

The spec defines three eras. Legacy uses initialize (2025-11-25 and earlier). Modern uses per-request metadata (2026-07-28 onward). Dual-era means an implementation that supports both. There is no handshake. A server that doesn't support the requested version returns UnsupportedProtocolVersionError with a supported list, and the client retries.

ClientServerOutcome
ModernModernWorks; server/discover optional
ModernLegacyFails; on stdio, send server/discover first so it fails clearly
Dual-eraLegacyWorks; falls back to initialize
LegacyModernFails; legacy clients cannot move up to the new protocol
LegacyDual-eraWorks under legacy rules

To detect a server's era over stdio, a dual-era client sends server/discover and falls back on any error that isn't a recognised modern one. Over HTTP it sends a modern request and, on a 400, reads the body first: a modern error means retry, anything else means fall back. It caches the result per server process or origin. A dual-era server decides per request from how the client opens, and can serve both eras on one endpoint.

The practical point: a modern-only server breaks every legacy client, so run dual-era servers during the transition. SDK defaults differ. The Python SDK v2, where FastMCP is now MCPServer, defaults Client to mode='auto', which probes and then falls back. The TypeScript SDK's guide to this revision says its v2 packages speak the 2025-era protocol unless you opt in, for example with versionNegotiation: { mode: 'auto' }.

Rolling out protocol changes like this inside a customer's environment, across their gateways, identity provider and ITSM, is routine work for Forward Deployed Engineers. The FDE PRO program practises it in its ServiceNow AI Agent via MCP project.

Migration checklist for server authors

  1. Upgrade the SDK and read its migration guide. In Python v2, imports move from mcp.server.fastmcp to mcp.server.mcpserver, and transport options move to run().
  2. Serve server/discover with a real serverInfo name and version.
  3. Find hidden session state: anything keyed by connection or session ID. Move it into handles or requestState.
  4. Port server-initiated calls to MRTR. In Python v2, ctx.elicit() raises NoBackChannelError on a 2026-07-28 connection. The guide recommends resolver parameters (Elicit, Sample, ListRoots), which work in both eras.
  5. Sign and expire requestState whenever it affects access.
  6. Publish change notifications on subscriptions/listen. On 2026-era connections, the old session helpers' notifications are dropped.
  7. Replace deprecated features. Roots become parameters, sampling becomes a direct LLM API call, and logging goes to stderr or OpenTelemetry (AI observability).
  8. Return the cache fields, then remove sticky sessions from the load balancer.

A server built from our MCP server in Python tutorial needs little change. The work is mostly dependency pins, imports and testing both eras.

Migration checklist for host and client authors

  1. Send the required _meta on every request. Over HTTP, also send MCP-Protocol-Version, Mcp-Method and Mcp-Name.
  2. Implement era detection, and cache the result.
  3. Handle input_required: collect the answers, retry with a new ID, and echo requestState unchanged.
  4. Treat a missing resultType as complete, and treat unknown values as invalid.
  5. Open subscriptions/listen with explicit filters, and re-subscribe after a disconnect.
  6. Re-issue broken requests, since streams can no longer be resumed.
  7. Move OAuth to CIMD, validate iss, and key credentials by issuer.

Testing with the MCP Inspector

The MCP Inspector now has three clients: web (npx @modelcontextprotocol/inspector), CLI (--cli) and terminal UI (--tui). Each server has a protocol era setting. legacy is the default and does a plain initialize. auto probes server/discover and falls back. modern pins 2026-07-28 with no fallback. For a dual-era server:

  • Connect in legacy mode and confirm existing hosts see no change.
  • Connect in modern mode and check that supportedVersions and serverInfo appear in Connection Info.
  • Call a tool that asks the user for input and confirm the input_required round trip completes.
  • Set the per-request log level to Off and confirm no logs arrive. That silence is correct.
  • Run the CLI client in CI so every pull request tests both eras.

Then repeat the same calls through several replicas behind your real load balancer.

Security implications

  • requestState is client-held state. Without integrity protection, a client can edit it to skip checks. Anything that must be used only once needs a server-side record.
  • Handles are names, not permissions. Check authorization against the handle on every call.
  • Headers are now policy inputs. Gateways should trust Mcp-Name or Mcp-Param-* headers only on versions that require the server to validate headers against the body. Never put personal data in them.
  • Self-reported identity. Don't build allow-lists on serverInfo or clientInfo.
  • Annotations are advisory. A tool marked readOnlyHint can still write, so approvals belong in the host (human-in-the-loop AI).
  • Keep the old defences. Origin validation and binding local servers to localhost still protect against DNS rebinding.

Consider a GCC IT team in Hyderabad that exposes a ServiceNow incident server to an internal assistant. After migrating, it can scale the server horizontally, block Mcp-Name: delete_incident at the gateway, and sign the requestState that carries a half-filled change request. That is easier to audit than a hidden session cache. For how this compares with plain REST, read MCP vs API. For MCP alongside A2A and other agent protocols, see AI agent protocols explained.

Frequently asked questions

Is the MCP 2026-07-28 spec backward compatible with older clients?

Not by default. A legacy client that sends initialize to a modern-only server fails. Servers that must support older hosts should run dual-era, answering initialize under legacy rules and per-request metadata under 2026-07-28 rules.

Do clients have to call server/discover first?

No. Servers must implement it, but clients may send any request with their preferred version and retry on UnsupportedProtocolVersionError. On stdio it is the recommended probe for detecting legacy servers.

What replaces Mcp-Session-Id?

Nothing at the protocol level. State that must span calls uses explicit handles passed as tool arguments, or an opaque requestState inside a multi round-trip request.

Is elicitation deprecated in MCP 2026-07-28?

No. Roots, sampling and logging are deprecated under SEP-2577. Elicitation remains, but servers now deliver it inside an input_required result instead of sending a separate request.

When will sampling, roots and logging be removed?

The deprecated features registry gives the earliest removal as the first specification revision released on or after 2027-07-28. Until then they keep working.

What happened to MCP Tasks?

Tasks became the official io.modelcontextprotocol/tasks extension under SEP-2663, using polling with tasks/get instead of a blocking tasks/result. SDK support varies, so check your SDK's documentation first.

Should a new MCP client use CIMD or Dynamic Client Registration?

Use Client ID Metadata Documents when the authorization server advertises support. Dynamic Client Registration is deprecated and remains only as a fallback, and pre-registered credentials still take priority.

How do I test both protocol eras with the MCP Inspector?

Set the server's protocol era to legacy, auto or modern. Test in legacy and modern modes, and run the CLI client in CI.

Ready to take MCP servers from a working demo to a governed, observable production service? Cloudsoft's AI Forward Deployed Engineer course builds a ServiceNow AI Agent via MCP alongside Kubernetes, Entra ID and observability. Classes run in Ameerpet, beside Ameerpet Metro, or live online. Call +91 96660 19191 for a free demo.

Share𝕏infβœ‰
EnrollWhatsAppCall us