MCP Drops the Session: What Stateless Servers Change

MCP 2026-07-28 removes protocol sessions. Redesign routing, state, caching, tasks, identity, and traces without breaking legacy clients.

Saturday, July 25, 2026Omid Saffari
MCP Drops the Session: What Stateless Servers Change

MCP's 2026-07-28 core removes the protocol session, not application state. The winning server design is an ordinary stateless HTTP boundary with explicit state handles, per-request identity, cache scopes, and durable task IDs when work outlives a call.

The protocol is stateless, your application is not

Stateless MCP means any ordinary request can land on any healthy server instance. It does not mean your tools must forget baskets, browser sessions, jobs, drafts, or approval state between calls.

The 2026-07-28 release candidate removes the initialize and initialized handshake plus the Mcp-Session-Id header used by 2025-11-25. The candidate was locked on May 21, 2026, and the final specification is scheduled for July 28, 2026. It contains breaking changes.

That clean break removes a transport concern from application design. A load balancer no longer needs a sticky route just because one server instance issued a protocol session. A shared session store is no longer part of the protocol contract. The request carries the protocol version, client information, and relevant capabilities in _meta, so the server can interpret it without prior connection state.

Application state should become visible and deliberate. A shopping tool can return a basket_id; a browser tool can return a browser_id. A later call passes that handle as a normal argument. The state still exists, but its ownership, storage, expiry, and authorization are no longer hidden in transport metadata.

Make every request a complete operating unit

Every request should contain enough information to authenticate, authorize, route, execute, and trace the call without consulting connection history. This is the central design rule for a stateless MCP server.

A modern Streamable HTTP tool call has an enforceable shape:

Http
POST /mcp HTTP/1.1
Authorization: Bearer <access-token>
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json

{"jsonrpc":"2.0","id":"req-search","method":"tools/call","params":{"name":"search","arguments":{"query":"account policy"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"support-console","version":"release"},"io.modelcontextprotocol/clientCapabilities":{}}}}

The gateway can route and rate-limit on Mcp-Method and Mcp-Name without parsing the JSON body. The server must still compare those headers with the body and return HTTP 400 Bad Request when they disagree or a required routing header is missing. That comparison closes a split-brain security gap where the gateway routes one operation but the application executes another.

Authorization is also per request. The MCP authorization specification requires the bearer token on every HTTP request, requires audience validation at the MCP server, and forbids passing that client token through to an upstream API. If the tool calls another service, obtain a separate credential for that service. The detailed implementation belongs at your MCP authorization boundary, not in an in-memory session object.

Log the request ID, protocol version, client identity, authenticated subject, tenant, method, name, authorization outcome, result type, cache decision, and trace context. Avoid logging bearer tokens, raw requestState, or sensitive tool arguments. A session ID is no longer available as a convenient correlation key, so distributed trace context becomes the durable join.

A stateless MCP request flowing through authentication, routing, execution, and tracing
Each request carries its own identity, route, operation, and trace context.

Replace hidden session state with explicit handles

Use a server-minted handle whenever one tool call creates state that another call needs. The handle makes the dependency testable, portable across server instances, and visible to the model.

An illustrative support workflow makes the boundary concrete. A create_case_workspace tool returns a workspace_id. Later tools accept that handle when they attach evidence, draft a response, or request approval. The server stores the workspace outside the process and binds it to the caller and tenant. Any instance can load it, re-check access, and continue.

  1. Mint a narrow handle

    Return an opaque identifier for one domain object or workflow. Do not make the handle encode broad authority, and do not expose a raw database key when an unguessable public identifier is safer.

  2. Bind ownership at creation

    Persist the authenticated subject, tenant, allowed operations, lifecycle state, and expiry policy beside the handle. This turns implicit session ownership into a reviewable record.

  3. Authorize every reuse

    On each later tool call, load the record and compare its owner and tenant with the current request identity. Possession of a handle is not sufficient authorization unless the handle is intentionally designed as a bearer capability.

  4. Make cleanup explicit

    Define completion, cancellation, and expiry as domain events. A stateless server will not receive a session-close signal that can safely own cleanup for you.

Keep the model-facing contract small: return the handle, explain which later tools accept it, and surface a stable error when it is expired or unauthorized. The model can then compose workflows with explicit state instead of relying on an invisible connection.

Cache discovery by authorization shape

Cache only when the response is identical for the authorization contexts that may share it. The new cache fields are an operating contract, not a performance hint you can set optimistically.

The draft caching specification requires cache hints on completed results from server/discover, tools/list, prompts/list, resources/list, resources/templates/list, and resources/read. ttlMs says how long the response may be considered fresh. cacheScope says whether it may be shared.

Use cacheScope: "public" only when the result contains no user-specific data and is the same across callers. Use "private" when scopes, tenant policy, entitlements, feature flags, or resource ownership can change the result. The conservative TypeScript SDK v2 beta default is ttlMs: 0 with cacheScope: "private", which is the right launch setting until the sharing invariant is proven.

Consider a SaaS platform whose admin users can see destructive tools that ordinary users cannot. Its tools/list is identity-dependent, so a public cache could disclose tool names or schemas across roles. Mark it private and key the cache by the authorization context. If every caller truly receives the same read-only tool list, public caching becomes defensible after an access-control test proves that equality.

Do not cache an interim result with resultType: "input_required". Do not cache a retry that contains inputResponses or requestState. Those results depend on interaction state outside the normal cache key. For pagination, cache each page independently and assume there is no cross-page consistency guarantee.

The practical rule is simple: cache discovery data, never authorization decisions. Re-check access at execution even if the client discovered and cached the tool earlier.

Long-running work belongs in durable tasks

Use the Tasks extension when a call must outlive the request that created it. A process-local promise is not a task, and returning a task ID before its record is readable creates a race the specification explicitly forbids.

The io.modelcontextprotocol/tasks extension is server-directed. A client and server declare support, then the server decides whether a request becomes a task. The lifecycle uses tasks/get, tasks/update, and tasks/cancel; there is no tasks/list.

An illustrative document-export tool should commit the task record before returning its taskId. The client can then poll tasks/get, honoring pollIntervalMs. If the task needs a decision, its status becomes "input_required" and the client returns responses through tasks/update. Cancellation is cooperative and eventually consistent, so the acknowledgement cannot be treated as proof that downstream work stopped.

Persist the task owner, originating tool, status, outstanding input keys, result or error pointer, and cancellation intent. Generate task IDs with enough entropy that another party cannot enumerate them. For Streamable HTTP task methods, Mcp-Name carries the task ID so an intermediary can route correctly. Shared durable storage is still preferable to instance affinity because it survives restarts and rescheduling.

The MCP Tasks lifecycle from tool call through task ID, polling, input update, cancellation, and result
Durability comes before the task handle, then the client drives the lifecycle explicitly.

Human approval fits this model when the approval belongs to long-running work. Store the proposed action and reviewer-visible evidence with the task, surface the request as input required, and resume only after the authenticated response is written. Do not hide the approval state in a worker process or treat a transport retry as consent.

Roll out both protocol eras before you pin the new one

Negotiate first, observe compatibility, then pin 2026-07-28 only where both sides are proven. Calendar-based cutovers are brittle because SDK support and deployed clients do not upgrade at the same moment.

The TypeScript SDK v2 beta migration guide says the modern revision is an explicit opt-in. Its default remains the legacy handshake. Auto mode probes with server/discover and falls back to a legacy-only server at the cost of one extra round trip. A modern pin rejects a legacy-only server.

For an existing TypeScript v2 client, the opt-in is small:

TypeScript
const client = new Client(
  {
    name: process.env.MCP_CLIENT_NAME!,
    version: process.env.RELEASE_SHA!,
  },
  {
    versionNegotiation: { mode: "auto" },
  },
);

await client.connect(transport);

The rollout work is in the evidence around that switch. Run the same tool contract through legacy and modern paths. Send equivalent calls to different server instances and verify that no hidden process state changes the result. Exercise a cross-tenant handle and require a denial. Prove that public cache candidates are identical across identities and that private responses never cross contexts. Kill a worker after task creation and verify that another worker can resolve the task.

Propagate W3C traceparent, tracestate, and baggage through _meta so the host, client, gateway, MCP server, and downstream service appear in one trace. Compare authorization failures, tool errors, cache behavior, task transitions, and latency by protocol version. The dual-era migration playbook covers the broader compatibility sequence; this runtime design is the state boundary that sequence must preserve.

Pin the modern revision only after unsupported-version traffic is understood, SDK conformance is acceptable, and rollback no longer depends on reconstructing hidden session data.

Hidden state breaks before request handling

The first failures will come from assumptions around the request, not from parsing JSON-RPC. A stateless handler can look healthy while ownership, caching, or task continuity is wrong.

  • In-memory continuity: a tool succeeds until the next call lands on another instance. Replace the dependency with an explicit handle backed by durable state.
  • Identity-blind caching: one caller receives a tool or resource list generated for another. Default to private and prove a response is public before sharing it.
  • Premature task handles: the client receives a task ID that another worker cannot resolve. Commit the task before returning the handle.
  • Session-keyed observability: traces fragment after Mcp-Session-Id disappears. Correlate on request and W3C trace context instead.
  • Token passthrough: the MCP bearer token leaks into a downstream service. Exchange or obtain a resource-specific token and keep the trust boundaries separate.

The most important control is explicit state ownership. Once every handle and task record has an owner, tenant, lifecycle, and authorization path, routing, caching, and tracing become ordinary infrastructure decisions. Without that ownership model, stateless transport only makes hidden coupling harder to see.

Frequently asked questions

Does stateless MCP mean the server cannot keep state?

No. The protocol session is gone, but application state can live behind an explicit server-minted handle passed as a normal tool argument. Store it outside the process and authorize every reuse.

Does MCP 2026-07-28 still need sticky sessions?

Ordinary requests do not need protocol-level stickiness. Task traffic can be routed on the task ID in Mcp-Name when state is instance-local, but durable shared task storage is the safer design.

How does an MCP server request user input without a persistent connection?

It returns InputRequiredResult. The client gathers the requested input and retries the original request with inputResponses and the echoed requestState, allowing any server instance to continue.

Can clients cache tools/list across users?

Only when the server marks the result cacheScope: "public" and the tool list is identical across authorization contexts. If roles, scopes, tenants, or entitlements affect the list, use "private".

Last Updated

Jul 25, 2026

CategoryMCP
Newsletter

One letter, every week. Working systems — not hot takes.

Build logs, agentic engineering decisions, agent failures, evals, and what survives real users. Sent weekly, never more.

Weekly. No spam. Unsubscribe anytime.