What's the difference between a stateless and stateful MCP server implementation?

5 minadvancedmcpstatearchitecturescaling

Quick Answer

A stateless server treats every request independently, keeping no memory of prior calls in that session beyond what the protocol itself carries (the negotiated capabilities from initialize) — each tools/call is handled purely from its own arguments plus whatever persistent backing store (a database, an external API) it queries fresh each time. A stateful server maintains additional in-memory context across requests within a session — accumulated conversation-specific data, an open transaction, a cached expensive computation, active resource subscriptions. Stateless servers are simpler to scale horizontally (any server instance can handle any request, since there's no session-pinned memory) and simpler to reason about; stateful servers can be more efficient for genuinely session-scoped work but require care around session affinity (routing all of one session's requests to the same instance) if deployed behind a load balancer.

Detailed Answer

Whether an MCP server needs to hold state in memory across requests is mostly a question about what it's wrapping, not something the protocol forces one way or the other.

A stateless tool handler

@mcp.tool()
def get_stock_price(symbol: str) -> str:
    # Nothing kept in memory between calls — a fresh lookup every time
    price = external_api.fetch_price(symbol)
    return f"{symbol}: ${price}"

Any server instance, anywhere, can handle this call correctly given just the arguments. Scale it by running N identical instances behind a load balancer with no coordination needed between them.

A stateful example: an active subscription

active_subscriptions: dict[str, set[str]] = {}  # session_id -> set of subscribed URIs

async def handle_subscribe(session_id: str, uri: str):
    active_subscriptions.setdefault(session_id, set()).add(uri)

async def notify_resource_updated(uri: str):
    for session_id, uris in active_subscriptions.items():
        if uri in uris:
            await push_notification(session_id, uri)

This server needs to remember, per session, which resources that specific client subscribed to. That in-memory map is real session state. It doesn't survive the server instance restarting, or a request landing on a different instance.

Scaling implications

StatelessStateful
Horizontal scalingTrivial — any instance handles any requestNeeds session affinity (sticky routing) or a shared state store (Redis, a database)
Failure recoveryA crashed instance loses nothing meaningfulA crashed instance can lose in-memory session state unless externalized
Implementation complexityLowerHigher — must track session lifecycle, clean up on disconnect

Practical guidance

Default to stateless wherever the underlying operation genuinely doesn't need memory between calls. Most tools mapping to "look something up" or "perform one action" fit this. Reach for stateful design specifically when a feature requires it — resource subscriptions, an expensive multi-step operation you want to resume rather than restart. When you do, externalize that state to a shared store rather than relying purely on in-process memory, the moment you need more than one server instance for scale or availability.

Related Resources