ADR-0020 — Stateless request model; sessions removed¶
Context¶
MCP 2026-07-28 removes sessions from the protocol (SEP-2567) and the
initialize handshake with them (SEP-2575). There is no
Mcp-Session-Id header, no negotiated per-client state, and no
"initialized" lifecycle to enforce. Instead, every request is
self-contained: the client sends its protocol version, identity, and
capabilities in _meta on each call, and a mandatory server/discover
method lets clients learn the server's supported versions,
capabilities, and identity up front.
Mocapi's previous architecture was organized around the session.
ADR-0009 defined the
session lifecycle: createContext(sessionId, protocolVersion) returned
a sealed McpContextResult whose error variants (SessionIdRequired,
SessionNotFound, ProtocolVersionMismatch) transports mapped to
native errors, with a carve-out for the session-less initialize
bootstrap. ADR-0007 made session
storage durable and multi-node via Substrate's AtomFactory, alongside
Substrate's three other roles: Mailbox rendezvous for server-initiated
requests, journal-backed SSE resumability, and cross-node notification
fan-out. Under the clean break (ADR-0019),
all of that machinery serves a protocol that no longer exists.
Decision¶
Every request is self-contained; mocapi holds no per-client state
between calls. A per-request, immutable McpExchange replaces
McpSession.
Rules:
- Protocol version, client info, and client capabilities arrive in
_metaunder the spec-defined keysio.modelcontextprotocol/protocolVersion,io.modelcontextprotocol/clientInfo, andio.modelcontextprotocol/clientCapabilities. The server parses them into anMcpExchangeat dispatch time; the exchange is immutable and scoped to the single request. server/discover— mandatory in the spec — advertises the server's supported protocol versions, capabilities, and identity. It replaces handshake-time negotiation entirely.- A request carrying an unsupported protocol version is rejected with
UnsupportedProtocolVersionError. There is no lenient default and no negotiation. - Sessions are deleted, not bypassed:
McpSession, the session store, theMcp-Session-Idheader handling, session TTLs, and theinitializecarve-out are all removed from the codebase. - Application state that must cross calls uses the spec's
explicit-handle pattern: a tool returns an identifier (e.g.,
basket_id) and the model passes it back as an argument on subsequent calls. This is a userland pattern documented in the guides — mocapi ships no framework machinery for it. - Substrate is removed entirely. Its four mocapi consumers are
all obsolete in this revision: the session store dies with sessions;
the Mailbox rendezvous dies with server-initiated requests
(ADR-0021); journal-backed
SSE resumability dies with the
draft transport spec's
statement that "Resumable SSE streams via
Last-Event-IDare not supported"; and cross-node notification fan-out dies with the standalone GET stream it fed — the only remaining delivery channel is the response stream of the request a notification relates to, which by construction lives on the node executing that request (subscriptions/listen, the new cross-call channel, is not implemented — ADR-0022). - Odyssey is removed with it. Odyssey's only mocapi role was
managing named, session-keyed, resumable SSE streams
(
DefaultSseStream/DefaultSseStreamFactory) — the standalone GET stream andLast-Event-IDreplay. Both mechanisms are gone from the transport. The only SSE remaining is a response stream scoped to a single POST, carrying request-related notifications and the final response; a plain SpringSseEmitterowned by the controller covers it without a stream-management library.
Consequences¶
What this buys us. Multi-node deployment becomes trivial: any node
can serve any request with no shared store, no sticky sessions, and no
clustered backend to configure. The Substrate and Odyssey dependencies
— and the entire "pick a backend" decision surface of ADR-0007 —
disappear from every pom and every deployment guide. The sealed session-validation
result of ADR-0009 collapses to a single error case
(UnsupportedProtocolVersionError), since there is no session to be
missing or expired. Serverless and scale-to-zero deployments, which the
stateful design explicitly could not serve (see the old ADR-0018's
"Stateless / Serverless Mode" entry), are now the natural shape.
Costs. Per-request _meta parsing replaces a one-time handshake —
a small, bounded cost on every call. Handlers can no longer stash state
on a session object; cross-call state is the application's problem, via
explicit handles backed by whatever store the application already has.
Capability gating (e.g., "does this client support elicitation?") reads
the per-request McpExchange rather than a negotiated session, so a
client that sends inconsistent capabilities across requests gets
inconsistent behavior — the spec accepts this.
Non-goals. Mocapi does not provide a session-emulation layer, a server-side state store, or helpers for the explicit-handle pattern beyond documentation. It also does not retain Substrate for "future use" — if a future feature needs durable state, that is a new decision with its own ADR.
This ADR supersedes ADR-0007
(Substrate storage SPI and session store) and
ADR-0009 (session
lifecycle validation via McpContextResult).
Code anchors: mocapi-server/.../server/DefaultMcpServer.java
(stateless handleCall), mocapi-server/.../server/exchange/McpExchange.java
(per-request immutable exchange replacing McpSession),
mocapi-server/.../server/exchange/MetaEnvelopeParser.java (parses the
_meta protocol-version / client-info / client-capabilities envelope).