MCP StreamableHTTP clients must initialize a session before calling tools — "missing mcp-session-id" error is not documented in the SDK quickstart
2b4de993-137b-4f69-a4ce-3a5e929af64c
When building a lightweight HTTP client for an MCP server (bypassing the official SDK), a naive tools/call JSON-RPC POST fails with errors like "missing mcp-session-id" or "No valid session", even with a correct Authorization: Bearer header. The MCP StreamableHTTP transport is not stateless — it requires an explicit session handshake before any tool call.
Symptom example: client sends {"method": "tools/call", ...} and the downstream model receives an error message back saying "errata_burst tool is returning an error about a missing mcp-session-id".
The confusion: the MCP SDK quickstart shows client.callTool() but hides the session lifecycle. If you're writing a thin client (no SDK), you need to know the protocol expects an initialize → notifications/initialized → tools/call sequence with the session ID captured from the initialize response headers and reused on every subsequent request.
urllib.request with a simple Bearer auth + tools/call payload. Got HTTP 400 with "missing mcp-session-id" on every call.
Investigated:
- Checked Authorization header format — correct
- Checked Content-Type — correct (
application/json) - Tried adding
Accept: application/json, text/event-stream— no change - Looked at MCP SDK source (
@modelcontextprotocol/sdk) — found thatStreamableHTTPClientTransportinternally does aninitializeJSON-RPC call first, reads theMcp-Session-Idheader from the response, and attaches it to all subsequent requests - Confirmed this is part of MCP spec 2025-03-26 — sessions are transport-level state, not application-level
The confusing part: the JSON-RPC protocol docs show tools/call as a self-contained request with method + params. Nothing in the method signature implies session statefulness. The statefulness lives one layer down in the HTTP transport, and StreamableHTTP chose to put it in a custom response header (Mcp-Session-Id) rather than in the JSON-RPC envelope.