Spanly Docs

Session tracking

How Spanly groups MCP requests into sessions, including synthetic session IDs for sessionless servers.

MCP's Streamable HTTP transport has an optional session mechanism: the server assigns an Mcp-Session-Id header on the initialize response, and the client echoes it on every subsequent request. When that header is present, Spanly groups requests into sessions, so you can follow a single client's conversation (initialize, tool calls, prompts) as one thread instead of a flat list of requests.

Servers with sessions

If your server assigns session IDs (for example a stateful StreamableHTTPServerTransport with a sessionIdGenerator), there is nothing to configure. Spanly picks the ID up from the captured headers and session grouping works out of the box.

Sessionless servers: synthetic session IDs

Many production MCP servers run stateless: a fresh server instance per request, no session ID assigned, every request self-contained. That is a perfectly valid deployment shape, but without a session ID Spanly cannot tell which requests belong to the same client conversation.

To close that gap, every Spanly instrumentation method assigns a synthetic session ID when the server doesn't. On an initialize response that carries no Mcp-Session-Id, Spanly adds one, prefixed spanly- so it is recognizable. Per the MCP spec, the client then includes that ID on all of its subsequent requests, which is exactly what Spanly needs to group them. This is enabled by default.

The injection is designed to be invisible to your server:

  • It only happens when the server did not set a session ID itself, and only on successful initialize responses.
  • Stateless servers ignore the echoed header. The official TypeScript and Python SDK transports skip session validation entirely when session management is off.
  • The CLI and docker sidecar go one step further: they strip the synthetic ID from requests before forwarding them upstream, so your server never sees a header it didn't create.
  • Your server still serves each request as usual. Synthetic sessions are a grouping label, they do not make the server stateful and do not enable server-to-client notifications or resumability.

Stdio transports are unaffected: a stdio connection is a single conversation already, and there are no HTTP headers to carry a session ID.

Turning it off

If you'd rather Spanly never touch a response header, disable injection on the surface you use:

SurfaceToggle
TypeScript SDKspanly.monitor(server, { injectSessionId: false })
Python SDKclient.monitor(server, MonitorOptions(inject_session_id=False))
CLI (spanly run / spanly proxy)--inject-session-id=false
Docker sidecaradd --inject-session-id=false to the proxy args

With injection off, requests to sessionless servers are still captured and attributed; they just aren't grouped into sessions.

Notes and edge cases

  • Synthetic IDs are visible to MCP clients (that is how they get echoed back). The spanly- prefix makes them easy to identify in client logs.
  • Clients that don't implement the session part of the Streamable HTTP spec won't echo the header, and those requests stay ungrouped. All mainstream MCP clients echo it.
  • Load-balanced, multi-replica servers work fine: the ID lives in the client's echo, not in server state, so it doesn't matter which replica serves each request.
  • In the Python SDK, injection works through the app returned by streamable_http_app() (FastMCP). If you assemble the ASGI app yourself, wrap it in SessionIdInjectorMiddleware; see the Python SDK reference.

On this page