DEV Community

Cover image for MCP Went Stateless. Your Recovery Logic Didn't.
Dmytro Nasyrov
Dmytro Nasyrov

Posted on

MCP Went Stateless. Your Recovery Logic Didn't.

The 2026-07-28 MCP revision removed the initialization handshake and protocol-level sessions. That makes a remote server easier to route and scale, but it does not make tool execution stateless. If a call can charge a card, create a ticket, send a message, or mutate a repository, retries still need durable facts. The practical conclusion is simple: move state out of the transport, then make execution, side effects, results, and delivery explicit. Otherwise a stateless server will recover cleanly at the HTTP layer while duplicating work in the system that actually matters.

Stateless transport removed one state owner, not state

The new MCP version retires initialize/initialized and the Mcp-Session-Id header. Each request carries what the server needs to interpret it, and any request can land on any compatible instance behind a normal load balancer. The official 2026-07-28 specification release describes the result as a stateless protocol core.

That is an important infrastructure simplification. It removes sticky routing and protocol-owned session storage from the default path. It does not answer these application questions:

  • Did this logical operation already start?
  • Did an external side effect happen?
  • Was the result persisted?
  • Did the caller receive that result?
  • Who is allowed to retry each step?

The discipline of production AI infrastructure engineering begins by naming those owners before choosing queues, databases, or worker frameworks. A session used to hide some of those decisions by giving related calls a convenient container. Once the container disappears, an implementation either models the decisions directly or leaves them implicit in process memory.

Implicit state is the dangerous option. Process memory can tell the current worker what it has seen, but it cannot tell the next worker what already happened after a crash, timeout, reschedule, or deployment.

The four crash windows that matter

Consider a tool called issue_refund. The client sends one request, but the business operation crosses several boundaries:

  1. The server accepts the intent.
  2. A worker starts the operation.
  3. A payment provider applies the refund.
  4. The server records the provider result.
  5. The result reaches the client.

“The request failed” is not one state. It can mean the process died before work began, during local work, after the provider committed the refund, or after the result was stored but before the response arrived. Those states require different recovery behavior.

If the server blindly replays the whole handler after every timeout, the third window can issue the refund twice. If it never retries once a worker marked the operation started, the first or second window can strand legitimate work forever. If it treats a stored result as proof that the caller received it, the fifth step can silently lose delivery.

The protocol cannot resolve this ambiguity because the ambiguity belongs to the application. Stateless MCP makes that boundary visible; it does not remove it.

A recovery-owner matrix

Before writing retry code, define one logical operation_id and map every crash window to durable evidence and one owner:

Crash window Durable fact required Safe recovery action Recovery owner
Before execution starts operation ID, input hash, accepted status claim the operation and execute once task service or queue
During local execution, before any external effect attempt ID and lease expiry let the lease expire, then run a new attempt worker coordinator
After an external effect, before its result is recorded stable idempotency key and side-effect intent query or replay through the same idempotency key; never issue a fresh command external adapter
After the result is stored, before the client receives it immutable result plus delivery status return the stored result and retry delivery separately API or delivery worker

The table separates three ideas that are often collapsed into status = done:

  • Execution state says whether the logical operation produced a result.
  • Side-effect state says what changed outside the worker's database.
  • Delivery state says whether the result crossed the final boundary to its consumer.

A workable stateless MCP server architecture therefore needs a task record whose lifetime is independent of any one request or server instance. It can be compact, but it must survive the exact failures that are safe to retry.

This also clarifies what an explicit MCP state handle is for. SEP-2567 replaces implicit session-scoped application state with ordinary server-minted handles that a model can carry between calls. A handle identifies a basket, browser, workflow, or operation. It is not evidence that an effect did or did not happen. The resource behind the handle still needs its own concurrency and recovery contract.

Make the operation record the source of truth

A minimal operation record might look like this:

{
  "operation_id": "refund_01J...",
  "input_hash": "sha256:...",
  "status": "effect_pending",
  "attempt": 2,
  "effect_key": "refund_01J...",
  "effect_receipt": null,
  "result": null,
  "delivery_status": "not_ready"
}
Enter fullscreen mode Exit fullscreen mode

The fields are less important than the invariants around them:

  1. Reusing an operation_id with different inputs must fail closed.
  2. Only one live attempt may own the current lease.
  3. Every non-idempotent adapter receives a stable effect key.
  4. A provider receipt is persisted before execution is marked complete.
  5. Delivery can be retried without rerunning execution.

This design changes a retry from “call the handler again” into “advance the known operation from its last durable state.” That is the difference between resilience and duplicate execution.

It also gives observability a useful unit. Logs from server instance A and worker instance B are no longer unrelated request traces; they are attempts attached to one operation. Alerts can distinguish a stuck lease, an uncertain side effect, a completed result awaiting delivery, and a client that simply stopped polling.

Tasks help with lifecycle, not business idempotency

The 2026-07-28 release also moves long-running work into the Tasks extension. A server can return a task handle, and the client can poll or update that task. That is a better fit for work that outlives one request, but a task handle alone does not make a payment, email, deployment, or repository mutation idempotent.

The task service can own lifecycle transitions such as accepted, working, input required, completed, failed, or cancelled. The adapter that talks to an external system must still own the effect key and reconciliation behavior. The delivery layer must still own the distinction between “result exists” and “consumer received it.”

This division prevents a common architectural mistake: asking one status field to describe three independent systems. A queue can truthfully say that a job completed while the email provider timed out after accepting the message. An MCP task can truthfully expose a result while the client disconnects before seeing it. Both statements can be true at the same time.

When this machinery is unnecessary

Not every tool needs an operation ledger. A read-only search can usually be retried from scratch. A deterministic transformation with no external effects may only need request-level timeout handling. A handler whose downstream system provides strong idempotency and queryable receipts can delegate much of the effect recovery to that system.

There is also a cost to durable recovery: more states, retention rules, reconciliation paths, access control, and tests. Explicit handles can appear in chat logs or subagent context, so authenticated servers should bind a handle to the request's authorization context. For unauthenticated servers, SEP-2567 recommends treating a handle as a short-lived capability token with high entropy.

The goal is not to make every tool a workflow engine. It is to match the recovery model to the irreversible boundary.

The decision rule

Use one test: if retrying the operation can make the outside world different twice, persist the operation and a stable idempotency key before the first side effect. If execution and delivery can fail independently, persist them independently. If neither condition is true, keep the design smaller.

MCP's stateless core is a real improvement because it stops transport sessions from pretending to be application state. The migration is complete only when every durable fact has an explicit home and every retry has one owner.

Which failure does your implementation still treat as session-owned after the stateless change?

Top comments (0)