Expand description
v0.11.0 P1 — MCP Mcp-Session-Id session store + middleware.
v0.10.2 shipped /mcp as a one-shot request/response surface — every
POST opened a fresh dispatcher with no cross-request state. v0.11.0
lifts that to the full MCP Streamable HTTP spec, and this module is
the foundation: a DashMap-backed SessionStore of
SessionState entries keyed by SessionId, plus an
mcp_session_middleware Axum middleware that validates the
Mcp-Session-Id request header against the store.
§Locked design (plan §3)
- Decision A — In-memory storage. A
DashMap<SessionId, SessionState>gives lock-free per-session reads on the dispatch hot path. Solo runs as a single-process daemon today; cross-process session persistence is deferred to a future release (clients re-initializeon daemon restart). - Decision D — TTL. 30 min inactivity + 4 hr absolute cap.
Background cleanup task runs every 60 s and removes expired
sessions; a lazy expiry check on every
getis the safety net for the window between sweeps. - Expired session → 404. The middleware returns 404 Not Found
with a body that includes a
re-initializeinstruction so clients can distinguish “session expired” from “server down”.
§Dispatcher integration (Option B — session-agnostic)
The brief leaves “does the dispatcher learn about sessions?” open.
P1 picks Option B: crate::mcp_dispatch::McpDispatcher stays
session-agnostic. Sessions are purely an HTTP-transport concern;
the dispatcher receives the resolved tenant + audit principal and
has no knowledge of SessionId. The stdio path (which has no
sessions) keeps working unchanged. v0.11.0 P3 will route per-tool
progress events through the session’s notification channel by
reading the session out of the request extension before building
the per-request ProgressEmitter — without baking sessions into
the dispatcher itself.
§v0.11.0 P2 — event buffer + publish API
P2 grows SessionState with the two fields the resumable GET
stream rides on top of:
event_tx: broadcast::Sender<McpStreamEvent>(capacityMCP_SESSION_EVENT_BUFFER_CAPACITY= 256 per Decision E).next_event_id: AtomicU64(monotonic per-session event id).
A new SessionState::publish_event helper allocates the next id,
constructs an McpStreamEvent, and fans it out to every live
subscriber. P3 (per-tool progress) and P4 (notifications/message
bridge) call this method on the same session record the HTTP POST
handler resolved; the GET handler in http.rs consumes via
SessionState::subscribe_events.
§What this module does NOT do
- No tenant/principal binding check. Cross-request tenant
mismatch (
409 Conflict) and cross-principal access (401 Unauthorized) still TBD — P2 wires the broadcast channel but leaves the auth-binding policy decisions to a follow-up priority (Plan §9 Q4). - No audit emission on session open/close. Plan §9 Q3 still open — P2 keeps the store as pure in-memory plumbing.
Structs§
- McpStream
Event - One event on a session’s SSE stream. Cloneable so the broadcast channel can fan out one event to N concurrent subscribers.
- Session
Id - Opaque session id assigned by the server. v7 UUID — time-ordered
for sortability per Solo’s
memory_iddiscipline; printed as a regular hyphenated UUID string on the wire. - Session
State - One session’s state. v0.11.0 P2 grows this from P1’s minimal “tenant + timestamps” record by adding the broadcast event channel
- Session
Store - In-memory, lock-free session store keyed by
SessionId. TheArc<SessionState>value lets the middleware hand a cheap clone to each request without holding aDashMapshard lock for the whole dispatch.
Enums§
- McpEvent
Kind - Discriminator for the per-session SSE event stream. The wire
event:field is rendered from the kebab-case spelling of each variant — kept in lock-step with the constants below so handlers + clients agree on the literal string.
Constants§
- MCP_
LAST_ EVENT_ ID_ HEADER - HTTP header name carrying the
Last-Event-IDper the SSE specification. v0.11.0 P2 reads this onGET /mcpto resume an interrupted stream from a known event id (Decision E). - MCP_
SESSION_ ABSOLUTE_ TTL_ MS - Absolute TTL (milliseconds). A session is unconditionally expired
this long after its
created_at_ms, regardless of activity (Decision D — bounds worst-case memory growth for orphaned sessions). - MCP_
SESSION_ EVENT_ BUFFER_ CAPACITY - Capacity of the per-session
tokio::sync::broadcastchannel that carries server-initiated SSE events (init, message, progress, heartbeat, lagged). Per plan §3 Decision E. A subscriber that drifts further behind than this sees aRecvError::Lagged(n)on its nextrecv, at which point the GET handler emits oneevent: laggedand resumes from the current cursor. - MCP_
SESSION_ EXPIRED_ ERROR - Body of the 404 response the middleware returns when a request
presents an unknown / expired
Mcp-Session-Id. There-initializefield is the contract for client retry logic: drop the stale id, POST/mcpwithout the header, capture theMcp-Session-Idfrom the response. - MCP_
SESSION_ ID_ HEADER - HTTP header name carrying the
Mcp-Session-Idper the MCP Streamable HTTP transport spec. Lowercase because HTTP headers are case-insensitive on the wire and axum stores them lowercased. - MCP_
SESSION_ INACTIVITY_ TTL_ MS - Inactivity TTL (milliseconds). A session whose
last_accessed_at_msis more than this old is considered expired (Decision D). - MCP_
SESSION_ SWEEP_ INTERVAL_ SECS - Cadence (seconds) for the background sweep task that removes
expired sessions. Lazy expiry on every
getis the primary safety net; the sweep keeps total memory bounded between accesses for idle sessions. - MCP_
STREAM_ EVENT_ HEARTBEAT_ NAME - SSE event name emitted on the periodic heartbeat tick.
- MCP_
STREAM_ EVENT_ INIT_ NAME - SSE event name emitted on session-connect (
event: init). - MCP_
STREAM_ EVENT_ LAGGED_ NAME - SSE event name emitted when a subscriber lags past the buffer.
- MCP_
STREAM_ EVENT_ MESSAGE_ NAME - SSE event name carrying a JSON-RPC
notifications/messagepayload. - MCP_
STREAM_ EVENT_ PROGRESS_ NAME - SSE event name carrying a JSON-RPC
notifications/progresspayload.
Functions§
- mcp_
session_ middleware - Axum middleware that enforces the
Mcp-Session-Idcontract. - set_
session_ id_ header - Insert a
Mcp-Session-Idresponse header so the client can echo it back on subsequent requests. Used by the POST handler when it freshly creates a session on a request that arrived without the header.