back to blog

MCP stateless spec: how to upgrade your MCP server for AI agents

Read Time 14 mins | Written by: Cole

MCP stateless spec: how to upgrade your MCP server for AI agents

MCP is now a stateless protocol. The 2026-07-28 revision, which went final on July 28, removes protocol sessions and the initialize handshake, and replaces the requests servers used to send back to clients. If you publish an MCP server, you have to update it, and the changes break in both directions.

Apify was one of the first to make the move. Its MCP server, with more than 100,000 monthly users, put a stateless path into production on July 30, two days after the spec went final. That path dropped a Redis state store, a task store, a pub/sub coordination channel, and orphaned session recovery. Older clients kept working the whole time on a legacy path that still runs on all of it.

For MCP, statelessness is an operations upgrade. A remote MCP server becomes a normal HTTP workload you can run behind a plain round-robin load balancer, with no sticky sessions and no shared session store. The cost is a migration with breaking changes on both the server and client side, and a few that fail without telling you.

The short version

  • Sessions are gone. The initialize handshake and the Mcp-Session-Id header are removed, and every request now carries its own protocol version, identity, and capabilities.
  • Server-initiated requests are gone too. Sampling, elicitation, and roots now run through Multi Round-Trip Requests, the largest code change in the revision.
  • Three changes fail quietly. Error codes were renumbered, SSE streams are no longer resumable, and ping no longer exists.
  • Older clients keep working if you run dual-era. One endpoint can serve both eras, and every Tier 1 SDK ships support for it.
  • July 2027 is the deprecation date. Roots, Sampling, Logging, and Dynamic Client Registration become eligible for removal then, which is not the same as scheduled.

Here's what the job involves, how long you have, and the sequence that works to migrate to the new MCP spec.

Why migrate your MCP server ASAP

MCP is quickly becoming how businesses put their services in front of AI agents. The protocol passed 400 million monthly SDK downloads this year, and Claude's connectors directory alone lists more than 950 MCP servers. For Apify and GitHub, the MCP server is a product in its own right, with users who expect it to work.

If agents reach your business through MCP, three things make this migration urgent:

  • The work takes longer than the deadline suggests. July 2027 is the earliest the deprecated features can be removed, but the migration covers session removal, MRTR, error handling, and a dual-era period that lasts until your clients move. Three weeks in, Apify had one in eight clients on the new spec.
  • Stateless servers cost less to run. GitHub removed a database write from every session start and a database read from every call. A server on the old spec keeps paying for session infrastructure the new one doesn't need.
  • Clients are already moving. The major SDKs, GitHub, AWS, and Cloudflare already support the new spec. Every month on the old spec is another month of new clients reaching your server through a fallback.

Teams that started in July 2026 already serve both versions and track the split. The sooner you start, the sooner your server runs on the version agents are moving to.

What MCP migration looked like at Apify and GitHub

Apify and GitHub were two of the first teams to move their MCP servers to the new spec, and both published what the work involved.

Apify's write-up:

  • Removed session infrastructure. Tech lead Jiří Spilka: "Instead of the server holding a million clients and their state, it's now offloaded entirely to the client."
  • Retired a tool. add-actor changed an agent's tool list mid-session, which the new spec doesn't allow. Its function moved into call-actor.
  • Refactored first. Separating shared logic from protocol code, plus about 70 integration tests, left the stateless change itself small.
  • Adoption is gradual. Three weeks after launch, about one in eight clients used the stateless path.

GitHub's write-up:

  • Shipped early. Support went live July 23, built against the release candidate.
  • Got faster. "Database writes on initialize are gone, and database reads are gone from every call."
  • Got simpler. It no longer reads every request body for logging and secret scanning, because the values now arrive in required headers.

GitHub also told its users they don't need to change anything, because the Tier 1 SDKs kept backward compatibility. That holds if you use MCP servers through a client. If you publish a server, you have more work to do, and the rest of this article covers it.

What the MCP 2026-07-28 stateless spec removed

These are the features the 2026-07-28 spec removed, with what replaces each one, from the spec changelog.

If your server uses any of them, that code has to be deleted or rewritten.

Removed What replaces it
initialize / notifications/initialized handshake Per-request _meta: io.modelcontextprotocol/protocolVersion, clientInfo, clientCapabilities
Mcp-Session-Id header and protocol sessions Server-minted handles returned as ordinary tool results, passed back as tool arguments
HTTP GET stream endpoint subscriptions/listen – one long-lived POST-response stream you opt into by notification type
resources/subscribe and resources/unsubscribe subscriptions/listen
Server-initiated requests (roots/list, sampling/createMessage, elicitation/create) Multi Round-Trip Requests
SSE resumability (Last-Event-ID, SSE event IDs) Nothing. A broken stream loses the in-flight request
ping Nothing
logging/setLevel Per-request io.modelcontextprotocol/logLevel in _meta
notifications/roots/list_changed Nothing
notifications/elicitation/complete and elicitationId The MRTR retry
Tasks in the core protocol The io.modelcontextprotocol/tasks extension

The spec also added two new requirements that affect every server:

  • Routing headers. Every Streamable HTTP POST must carry an Mcp-Method header, plus Mcp-Name on tools/call, resources/read, and prompts/get. The header values must match the request body, and servers reject mismatches with a 400 and JSON-RPC error -32020.
  • Cache fields. Every list result and resource read must include ttlMs, which tells clients how long to cache it, and cacheScope, which says whether shared caches can store it.

One removal is a useful warning. notifications/elicitation/complete was added in the previous revision, 2025-11-25, and removed eight months later. MCP still changes quickly, so building on a brand-new feature carries real risk.

Multi Round-Trip Requests replace server-initiated requests

Multi Round-Trip Requests (MRTR) change how a server asks the client for something in the middle of a request. It's the largest code change in the revision.

Under the old spec, a server that needed something – a list of files, a model completion, an answer from the user – held a stream open and sent its own request back to the client. The new spec removes that.

Now the server returns a result that says what it needs, and the client calls again with the answers.

Here's what that result, an InputRequiredResult, looks like:

{

"jsonrpc": "2.0",

"id": 1,

"result": {

"resultType": "input_required",

"inputRequests": {

"github_login": {

"method": "elicitation/create",

"params": {

"mode": "form",

"message": "Please provide your GitHub username"

}

}

},

"requestState": "AEAD-protected blob"

}

}

The full exchange takes three steps:

  1. The server asks. It returns the InputRequiredResult above, listing what it needs, plus a requestState value holding whatever context it needs to pick up where it left off.
  2. The client gathers the input. It asks the user, calls a model, or lists files, depending on the request.
  3. The client retries. It sends the original request again with an inputResponses map and the exact requestState it received. The retry gets a new JSON-RPC id, because it's an independent request.

Because requestState travels through the client, the spec says servers MUST treat it as attacker-controlled input. If it affects authorization, resource access, or business logic:

  • Protect it. Sign or encrypt it with HMAC or AEAD, and reject anything that fails verification.
  • Bind it. The spec recommends including the authenticated user, a short expiry, and an ID for the original request, so nobody can replay it as a different user or against a different call.

Every result also now carries a required resultType field: "complete" for a normal result and "input_required" for an MRTR request. Clients MUST treat a result from an older server that omits the field as "complete".

How to keep older MCP clients working

Most MCP clients won't move to the new spec right away, so most production servers need to support both versions for a while. The spec calls a server that does this dual-era.

How a dual-era server works

The server checks how each client starts:

  • A request carrying the new per-request _meta fields gets the new stateless behavior.
  • An initialize request gets the old session-based behavior, scoped to that stdio process or HTTP session.

Both can run on the same endpoint.

How clients detect which spec a server uses

It depends on the transport:

  • stdio: The client calls server/discover, which every new server MUST implement. If the error it gets back isn't one the new spec defines, it falls back to the old version.
  • Streamable HTTP: The client sends a new-style request. A 400 alone doesn't answer the question, because new servers also return 400 for unsupported versions, missing capabilities, and header mismatches. The client reads the error body: a recognized new-spec error means retry with a supported version, and anything else means fall back.

Where dual-era support breaks

An old client talking to a server that only supports the new spec fails outright, because old clients have no way to move forward. The spec suggests listing your supported versions in the error you return to initialize, since that message may be the only clue a user gets.

Dual-era on AWS Bedrock AgentCore Gateway

Some platforms handle dual-era for you. AWS made it a configuration setting on Bedrock AgentCore Gateway, where one call sets the supported versions:

bash

aws bedrock-agentcore-control update-gateway \

--gateway-identifier <id> \

--protocol-configuration '{"mcp": {"supportedVersions": ["2025-11-25", "2026-07-28"]}}'

The call replaces the existing list, so include every version you want to support.

AgentCore's version translation has one gap: it doesn't cover elicitation or sampling. An older client calling a tool that needs either gets error -32021, MissingRequiredClientCapability. Dual-era support works until a tool needs to ask the user something.

The breaking changes that fail quietly

Most breaking changes show up right away as failed requests. These three don't. Your code keeps running, and the problem surfaces later in production unless your tests already cover it.

  • Error codes changed. Resource-not-found moved from -32002 to -32602 to match the JSON-RPC standard. Code that checks for -32002 still compiles but stops catching the error. If you built against the release candidate or the beta SDKs, check three more: HeaderMismatch moved from -32001 to -32020, MissingRequiredClientCapability from -32003 to -32021, and UnsupportedProtocolVersion from -32004 to -32022.
  • Dropped streams can't resume. The spec removed Last-Event-ID and SSE event IDs. If a connection drops, the in-flight request is lost, and the client has to send it again with a new request ID. For long-running tool calls on unreliable networks, that means more failed calls.
  • ping is gone. Any health check that relies on it needs a different mechanism.

When deprecated MCP features actually get removed

Besides the features it removed outright, the 2026-07-28 spec deprecated four more and moved the older HTTP+SSE transport under the same deprecation policy. Deprecated features still work today.

New servers shouldn't adopt them, and existing servers should move off them before they become eligible for removal. The deprecation registry lists each one with its replacement:

Feature Deprecated in Earliest removal Migration path
Roots 2026-07-28 First revision on or after 2027-07-28 Pass directories or files as tool parameters, resource URIs, or server config
Sampling 2026-07-28 First revision on or after 2027-07-28 Integrate directly with LLM provider APIs
Logging 2026-07-28 First revision on or after 2027-07-28 stderr on stdio, OpenTelemetry for observability
Dynamic Client Registration 2026-07-28 First revision on or after 2027-07-28 Client ID Metadata Documents
HTTP+SSE transport 2025-03-26 Three months after SEP-2596 reaches Final Streamable HTTP

"Earliest removal" is the first date maintainers can remove a feature. The actual decision happens during release preparation and may come later. Plan around July 2027 as the earliest these features could disappear. 

What the official MCP SDKs handle for you 

If your server is built on an official SDK, the SDK handles most of the protocol changes. The five SDKs that were Tier 1 at launch – TypeScript, Python, Go, C#, and Rust – all support 2026-07-28, and each includes some form of dual-era support. Two of them changed in ways to know before you upgrade:

  • TypeScript changed its package structure. There's no @modelcontextprotocol/sdk 2.x. Version 2 ships as separate packages (/core, /client, /server, plus framework adapters). The new spec is also opt-in: v2 doesn't send 2026-07-28 traffic unless you turn it on.
  • C# changed its default. Stateless is now the default for HTTP, where v1 defaulted to stateful. If your server still needs sessions, set Stateless = false.

What the official codemod can and can't do

The TypeScript SDK includes an official codemod that rewrites code for v2. The best public test of it is a migration report from the maintainer of fastmcp, a widely used TypeScript framework:

  • The codemod made 86 changes across 16 files.
  • 442 of 448 tests passed, and the six failures were test setup issues.
  • It left two decisions for a person: the tools/call handler signature changed, which breaks the build, and fastmcp's public Context needs a redesign for v2, which changes an API other people's code depends on.

Plan for that last part. The codemod handles the mechanical changes, and what's left is API design work someone on your team has to decide.

A migration sequence that works

  1. Inventory what depends on sessions. Session-scoped tool lists, anything keyed on Mcp-Session-Id, per-session caches, and any tool whose job is mutating session state. Apify found an entire tool in that last category.
  2. Grep for the old error codes. -32002 fails silently, and so do -32001, -32003, and -32004 if you built against the release candidate.
  3. Refactor before you migrate. Split protocol handling from business logic so each era gets an adapter and the change itself stays small.
  4. Stand up dual-era on one endpoint. Keep serving legacy clients from the same route while modern clients start arriving.
  5. Move server-initiated requests to MRTR. Sign requestState with a TTL and a principal binding before it ever round-trips through a client.
  6. Pin behavior with tests across both eras, then run the official conformance suite before you release.
  7. Watch the version split in your logs. Drain the legacy lane on real adoption data, not a calendar date.

Apify's ratio – one in eight clients on the new revision after three weeks – is a reasonable planning anchor. The legacy lane carries traffic for a while.

Don't Miss
Another Update

Subscribe to be notified when
new content is published
Cole

Cole is Codingscape's Content Marketing Strategist & Copywriter.