mcptest docs GitHub

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 idVariantModeNotes
2024-11-05V2024_11_05sessionFirst public revision.
2025-03-26V2025_03_26sessionFirst stabilization.
2025-06-18V2025_06_18sessionProduction default.
2025-11-25V2025_11_25sessionLast revision before stateless cut-over.
2026-03-26V2026_03_26sessionInterim 2026 draft.
2026-07-28V2026_07_28statelessNew 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:

In stateless mode the transport:

  1. Skips Mcp-Session-Id capture (the response header is ignored).
  2. Skips Mcp-Session-Id replay (no session header is attached on the POST or the SSE GET).
  3. Splices stateless_meta into the params object of every outbound request before serialization, so the signer commits to the exact bytes the server sees.
  4. Emits the SEP-2243 routing headers on every request:

    • Mcp-Method: the JSON-RPC method name (for example tools/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

Planned follow-up