MCP is stateless: What does it mean for you?

Session-based MCP coordinated bidirectional capabilities. Stateless MCP moves remote traffic to self-contained requests and explicit application state.

An agent asks for a customer's order history. Before it can call anything, it needs to know which server has the tool, what that tool accepts, and whether the server is allowed to act.

That is the job MCP set out to solve. It gives AI hosts and external systems a shared way to discover capabilities and invoke them.

MCP defines more than tool definitions. The older protocol maintained a live, bidirectional client-server session for capabilities that needed it.

It also made remote deployment more complicated than a tool call should be.

Before I talk about the new changes in the MCP Spec 2026-07-28, let's first see how MCP is designed to work.

Session-based MCP: primitives, initialization, and capabilities

An MCP server could expose three kinds of capability:

  1. Tools are callable actions, such as looking up an order or creating a ticket.
  2. Resources are read-only content addressed by URI, such as a file or a database-backed document.
  3. Prompts are reusable templates that the host can expose in its UI.

The client could expose three capabilities of its own:

  1. Roots are the URIs the server may access. The client declares the boundary; the server respects it.
  2. Sampling lets a server ask the client's model to produce a completion, without the server owning the model API key.
  3. Elicitation lets a server ask the user for structured input, such as approval or a missing record ID.

MCP keeps these separate because they have different request paths. tools/call runs an action. resources/read returns content. prompts/get returns a template for the client to present or fill in.

The client-side primitives are different. They define a boundary or an interaction the server cannot safely choose on its own.

Session-based MCP had three stages.

  1. Initialize. The client sent initialize with its identity, protocol version, and capabilities. The server replied with its own details and capabilities. The client then sent notifications/initialized.
  2. Operate. The client discovered and called tools. Either side could send the notifications and requests that both had negotiated.
  3. Close. Either side closed the transport. MCP did not define a separate shutdown request.

Before a client called tools/list or tools/call, it completed the initialize stage.

The server response also established the remote session. Later HTTP requests included the session identifier so the server could associate them with this negotiated connection.

This was capability negotiation. If the client declared sampling, the server could use sampling/createMessage. If the server declared resources.subscribe, the client could subscribe to a resource.

Without the matching declaration, neither side used that feature.

The handshake was not ceremony for its own sake. It let clients and servers use the features they shared while ignoring features they did not support.

After initialization, the session carried the negotiated relationship. The client called tools and resources. The server could request sampling or elicitation, and it could send change notifications through the same live channel.

That is why a session represented more than a connection ID. It could hold negotiated capabilities, subscriptions, pending server-to-client requests, and reconnect state.

Session-based MCP: why horizontal scaling was harder

Remote MCP used Streamable HTTP. The server issued an Mcp-Session-Id, and the client sent it on later requests.

A horizontal deployment works best when every healthy server can handle the next request. A session changes that rule: its negotiated capabilities, subscriptions, and pending requests belong to a specific server or a shared store.

A common failure is an approval flow. Pod A asks the user to approve a payment and keeps that elicitation request in its session. If Kubernetes replaces Pod A before the user replies, Pod B needs that pending request to interpret the reply.

Sticky sessions route later requests back to Pod A, but they cannot help after Pod A has gone down. A shared session store fixes recovery, but adds a stateful dependency that every pod and the gateway must use correctly.

The same issue appears during reconnects, autoscaling, and rolling deployments. The infrastructure has to preserve session ownership or recover it before it can forward the request. This is the scaling problem that stateless MCP targets.

Can session-based MCP run on Lambda, Workers, or containers?

Yes. The old protocol could run on all of them. The constraint was that an ordinary request handler could not be the only place that held a remote MCP session.

Hosting optionWhat worked in session-based MCPLimitation introduced by the session
AWS LambdaA function could handle Streamable HTTP requests.Function memory cannot be the session store. Lambda invocations have a maximum timeout of 900 seconds, so a long-held session needs external state and a front door that supports its connection pattern.
Cloudflare WorkerA Worker could serve the MCP HTTP endpoint and stream while the client remained connected.An ordinary Worker invocation is not a durable session owner. Persistent session state needs external storage or a Durable Object, which becomes a per-session coordinator.
Containers or Kubernetes podsA pod could keep a live session and server-to-client stream in memory.Pod replacement, a crash, or autoscaling requires sticky routing, connection draining, or shared session recovery.

Cloudflare does not impose a wall-clock limit on an incoming HTTP request while the client remains connected. It does enforce CPU limits, and work normally stops after the client disconnects.

A permanent session therefore remains an operational choice, not free state.

Serverless was therefore possible before stateless MCP. It just required the same session store, ownership rules, reconnection behavior, and timeout design as a stateful container deployment.

Stateless MCP: self-describing requests

MCP 2026-07-28 removes the protocol-level initialization handshake and session ID from the core request path.

Each request carries the protocol details the server needs. It also has routing-friendly HTTP headers such as Mcp-Method and Mcp-Name.

JSON-RPC remains underneath. The body still contains the request ID, method parameters, and response. The headers make the operation visible to a gateway without asking it to parse every JSON body.

This is a stateless search tool call. The protocol version, MCP method, and tool name are visible before the gateway reads the body. Client identity travels in _meta with the JSON-RPC parameters.

POST /mcp HTTP/1.1 MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: search Content-Type: application/json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "search", "arguments": {"q": "otters"}, "_meta": { "io.modelcontextprotocol/clientInfo": { "name": "my-app", "version": "1.0" } } } }

An API gateway can route, authorize, rate-limit, or meter search using the headers. The MCP server reads the JSON-RPC body to execute the call and return the result.

The next tool call can go to a different healthy instance. The transport does not need a route back to the server that handled the last call.

The client can use server/discover when it needs server capabilities before doing work. Discovery is explicit and cacheable rather than a required initial exchange.

Stateless does not mean an application has no state. A new_browser tool can return a browser_id, and a later click call can carry that ID. The browser record belongs in Redis, a database, or a browser worker.

The difference is ownership. Mcp-Session-Id was protocol state that infrastructure had to preserve. browser_id is application state with its own owner, expiry, authorization checks, and cleanup policy.

What changes for each MCP feature

The stateless core keeps the useful operations while changing how session-dependent interactions are delivered. This table is the complete feature map; the sections below expand only the changes that require application redesign.

FeatureWhy it existsSession-based deliveryStateless handling and migration
Capability negotiationPrevent a client or server from using an unsupported feature.initialize exchanged capabilities once per session.Requests carry protocol and client details; server/discover is optional. Remove initialization and session-capability storage.
Tools, resources, and promptsExpose actions, data, and reusable templates.The client discovered them after initialization.List and call operations remain. They no longer require a prior session handshake.
SamplingLet a server use the client's model and credentials.The server sent sampling/createMessage over the live session.Deprecated in core. Move the model call to the host, or give the server its own model integration.
RootsLet the client declare which URIs a server may access.The server received client-controlled URI scope through the session.Deprecated in core. Put the URI in the tool or resource request and enforce project or tenant scope on the server.
ElicitationCollect missing input or user approval during an operation.The server sent elicitation/create and waited for the response.Return input_required; the client collects input and retries the original call with inputResponses.
Change notificationsTell a client that a tool list or resource changed.The server sent notifications through the existing session.The client explicitly opens subscriptions/listen for the notification types it needs.
Long-running tasksTrack work that outlives one request.Experimental task methods tracked progress alongside the core protocol.Use the io.modelcontextprotocol/tasks extension and store task state outside the request handler.

Sampling: decide which component calls the model

A session-based review_pull_request server could send the pull request prompt through sampling/createMessage. The client called its configured model and returned the review text.

Sampling is deprecated in the stateless core because the callback required a live server-to-client channel. The host can call its model before invoking the MCP tool, or the MCP server can call a model-provider API itself.

The server owns the provider credential and billing in the second design. The host owns them in the first. That boundary is now an application decision rather than a transport feature.

Elicitation: approval with a retry

A payments MCP server exposes pay_invoice(invoice_id). The client calls pay_invoice("INV-2048"). The server loads the invoice and must collect approval before it calls the payment provider.

The session-based server used elicitation/create to request that approval. The stateless server returns input_required with the required fields and a requestState value.

The client shows the payee, amount, source account, and approval control. It then retries the original pay_invoice call with inputResponses and requestState.

If approved is true, the server calls the payment provider. If it is false, the server returns a cancelled result. The server never needs to open a separate request back to the client.

The original tool call, approval fields, and resumed call form one traceable request sequence. Any server instance can process the retry if it can validate requestState.

Tasks: return a handle and persist work outside the request handler

The scan_repository tool cannot finish while its request is open if the scan takes 20 minutes. It returns a task handle: an opaque task identifier such as scan-8f3a. The server stores the scan state outside the request handler.

The Tasks extension lets the client use tasks/get to read the status and tasks/cancel to stop the work. A replacement server instance reads the same durable task record using scan-8f3a.

MCP Apps remain separate from this flow. They render interactive tool results in the host UI; they do not recreate a protocol session.

Deployment: requests can move between Lambda, Workers, and pods

Containers, Kubernetes services, serverless functions, and traditional application servers can all run a stateless MCP endpoint.

Lambda and Workers: each invocation handles one MCP request

For a short tool call, a serverless function is a natural fit. It receives one authenticated request, reads an explicit handle if it needs one, calls the tool backend, and returns a result.

Lambda no longer needs to find the server that created an MCP session. The tool still has to finish before its configured timeout, which cannot exceed 900 seconds. A job that may run longer belongs in a queue or durable task system.

An ordinary Cloudflare Worker can handle the next request without owning a prior MCP session. A Worker still has plan-specific CPU limits, and an HTTP request ends when the client disconnects.

Durable Objects remain useful when the application itself needs coordination or state.

For a Kubernetes deployment, instances can be added or removed without preserving MCP session ownership. A request after a rollout can land on any ready instance.

An instance can still fail during a request. A client may retry on another instance, so write tools need idempotency keys or another duplicate-write guard.

Long-running work needs its own durable design. A report generation job should keep a task handle and progress state outside the request process, whether the handler runs in a container or a function.

Stateless MCP makes those hosting choices easier. It does not make a function's timeout longer, a database faster, or a third-party API less rate-limited.

OAuth 2.1 authorization: what stays the same and what changes

Remote session-based MCP already used OAuth 2.1-style authorization for protected HTTP servers. The client discovered the authorization server, completed a user or machine authorization flow, and obtained an access token.

The access token was required on every protected HTTP request, even when those requests belonged to one logical MCP session. The session ID identified an ongoing protocol session; it was never proof that the caller could use a tool.

Authorization: Bearer <access-token> Mcp-Session-Id: s-42

For stateless MCP, the Mcp-Session-Id line disappears. For protected servers, the Authorization header stays. Any ready server instance can validate the principal and then apply tool-level policy to that request.

The 2026 authorization changes are related to the same release, but they are not caused by stateless transport. They harden the existing OAuth and OpenID Connect model:

  • Clients validate the iss value in an authorization response, preventing authorization-server mix-up attacks.
  • Registered client credentials are bound to the authorization server that issued them.
  • Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents. DCR remains available during the migration period; clients using it now declare their application type so desktop and CLI redirect URIs work correctly.

DCR: the authorization server creates a client record

DCR solved a practical problem: an MCP client may encounter an authorization server it has never used before.

Instead of asking a person to pre-register every possible client, the client sent its metadata to the authorization server at runtime.

The authorization server had to expose and secure a registration endpoint. It also had to store, rate-limit, and manage a record for every client that registered.

CIMD: the authorization server reads published client metadata

Client ID Metadata Documents reverse that lookup. The client uses an HTTPS URL as its client_id. That URL points to a JSON document with its name and permitted redirect URIs.

The authorization server advertises whether it supports CIMD in its metadata. When it does, a client does not first create a registration record with POST /register.

CIMD moves the client description to a stable HTTPS document. The authorization server still validates the client ID and redirect URIs before issuing tokens; using a URL as a client ID does not make an unknown client trusted.

Header-based routing does not authorize a tool. Mcp-Method and Mcp-Name let a gateway identify the operation, while the access token and server-side policy decide whether this user may run it.

What stateless MCP does not provide: tool authorization and policy

Stateless MCP helps a gateway route and rate-limit calls because the request says which MCP method and tool it carries.

It does not decide whether a user may call delete_customer, whether that user owns the customer record, or whether the request should be approved by a person.

Tool poisoning remains a risk. A description can still try to influence the model. A server can still change a description after approval. Multiple servers can still expose confusingly similar tool names.

Every tool still needs server-side authorization and an audit trail. A valid token does not automatically grant permission to call delete_customer or approve a payment.

An MCP gateway remains necessary. It can hold upstream credentials, pin approved tool descriptions, apply policy, log calls, and rate-limit users or tools.

Migration limits: remote servers built around callbacks need redesign

For a local stdio server, the difference is small. The client starts one process, and process lifetime already gives it a natural relationship with that server.

For a remote server with simple tools, the gain is large. The protocol now fits the way web infrastructure already handles requests, scaling, retries, and replacement instances.

The migration is harder for a server built around subscriptions, sampling, or server-initiated user interaction. Those capabilities were not mistakes. They were features of a different model.

The design question becomes: “What state does this workflow own, and how should the client carry it?”

That is a better question for a browser, a payment, a long-running task, or a human approval. It also happens to be a better fit for the web.