Stateless transport (2026-07-28)
The MCP 2026-07-28 revision (SEP-2575) cuts the initialize handshake and Mcp-Session-Id out of the wire entirely. Every request carries its own protocol version, client info, and capability block in a _meta field, and a server's capabilities are fetched on demand via server/discover rather than handed back from initialize. Any request can land on any server instance, no session-store assumption.
mcptest tracks the cut-over through the ProtocolVersion enum and an is_stateless() predicate. The streamable-HTTP transport routes between the two modes based on a TransportMode selector the caller sets at connect time. Session (the default) keeps the pre-2026-07-28 wire behavior unchanged; Stateless drops every Mcp-Session-Id touch and splices a _meta block into every outbound request.
Version table
| Wire id | Variant | Mode | Notes |
|---|---|---|---|
2024-11-05 | V2024_11_05 | session | First public revision. |
2025-03-26 | V2025_03_26 | session | First stabilization. |
2025-06-18 | V2025_06_18 | session | Production default. |
2025-11-25 | V2025_11_25 | session | Last revision before stateless cut-over. |
2026-03-26 | V2026_03_26 | session | Interim 2026 draft. |
2026-07-28 | V2026_07_28 | stateless | New shape. SEP-2575. |
ProtocolVersion::current() returns V2025_06_18. We hold the default at the session-based path so callers that do not opt in to 2026-07-28 do not silently switch transport behavior. The default flips once the runner ships the stateless wire-level routing.
server/discover
server/discover (the stateless replacement for initialize) returns the same capability shape:
{
"protocolVersion": "2026-07-28",
"serverInfo": { "name": "demo-mcp", "version": "1.2.3" },
"capabilities": { "tools": { "listChanged": true } }
}
Client::discover() sends the request and parses it through parse_capability_block, the same function initialize uses, so the two entry points stay in sync.
Per-request _meta
protocol::build_request_meta(version, &client_capabilities) returns the _meta block the stateless transport attaches to every request:
{
"protocolVersion": "2026-07-28",
"clientInfo": { "name": "mcptest", "version": "1.0.0" },
"capabilities": { "experimental": { "x": true } }
}
Carried as a serde_json::Value so callers splice it into any request payload (params["_meta"] = ...) without allocating a custom envelope type per call site. The session-based path does not use this helper; it sends the same fields once during initialize.
Routing the transport
StreamableHttpConfig carries two new fields:
mode: TransportModedefaults toSession. Set toStatelesswhen the caller targetsV2026_07_28.stateless_meta: Option<Value>carries the_metablock the transport splices into every outbound request when running stateless. Build withprotocol::build_request_meta(version, &client_capabilities).
In stateless mode the transport:
- Skips
Mcp-Session-Idcapture (the response header is ignored). - Skips
Mcp-Session-Idreplay (no session header is attached on the POST or the SSE GET). - Splices
stateless_metainto theparamsobject of every outbound request before serialization, so the signer commits to the exact bytes the server sees. Emits the SEP-2243 routing headers on every request:
Mcp-Method: the JSON-RPC method name (for exampletools/call).Mcp-Name: the target name for the three name-bearing methods (tools/call→ the tool name,resources/read→ the URI,prompts/get→ the prompt name). Skipped for methods without a named target (tools/list,initialize,ping, etc.). The headers let a server route a request without parsing the body. Emitting them in session mode would risk tripping a strict proxy in front of a 2025-* server that does not know the values, so they are stateless-only.
A request whose envelope has no params object stays as-is; the spec says _meta rides on a params object, not on a scalar or absent field.
What ships today
V2025_11_25andV2026_07_28variants onProtocolVersion.is_stateless()predicate scoped toV2026_07_28.Client::discover()sendsserver/discoverand parses the capability shape.build_request_metabuilds the per-request_metavalue.- Shared
parse_capability_blockkeepsinitializeandserver/discoverparsing identical. StreamableHttpConfig.modeand.stateless_metaroute the streamable-HTTP transport between session and stateless wire shapes at connect time.
Planned follow-up
- Skipping the
initializeround trip when the caller pins toV2026_07_28(the protocol Client still callsinitializeeven in stateless mode today; flipping that off is the next step). - Cassette adjustments for the stateless shape.
- Flipping
ProtocolVersion::current()toV2026_07_28once the initialize-skip lands.