Skip to main content

Module mcp_session

Module mcp_session 

Source
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-initialize on 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 get is the safety net for the window between sweeps.
  • Expired session → 404. The middleware returns 404 Not Found with a body that includes a re-initialize instruction 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> (capacity MCP_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§

McpStreamEvent
One event on a session’s SSE stream. Cloneable so the broadcast channel can fan out one event to N concurrent subscribers.
SessionId
Opaque session id assigned by the server. v7 UUID — time-ordered for sortability per Solo’s memory_id discipline; printed as a regular hyphenated UUID string on the wire.
SessionState
One session’s state. v0.11.0 P2 grows this from P1’s minimal “tenant + timestamps” record by adding the broadcast event channel
SessionStore
In-memory, lock-free session store keyed by SessionId. The Arc<SessionState> value lets the middleware hand a cheap clone to each request without holding a DashMap shard lock for the whole dispatch.

Enums§

McpEventKind
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-ID per the SSE specification. v0.11.0 P2 reads this on GET /mcp to 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::broadcast channel 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 a RecvError::Lagged(n) on its next recv, at which point the GET handler emits one event: lagged and 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. The re-initialize field is the contract for client retry logic: drop the stale id, POST /mcp without the header, capture the Mcp-Session-Id from the response.
MCP_SESSION_ID_HEADER
HTTP header name carrying the Mcp-Session-Id per 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_ms is 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 get is 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/message payload.
MCP_STREAM_EVENT_PROGRESS_NAME
SSE event name carrying a JSON-RPC notifications/progress payload.

Functions§

mcp_session_middleware
Axum middleware that enforces the Mcp-Session-Id contract.
set_session_id_header
Insert a Mcp-Session-Id response 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.