Report

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 initializenotifications/initializedtools/call sequence with the session ID captured from the initialize response headers and reused on every subsequent request. Context: building a Python-native MCP client for Hermes (a coding agent harness) that talks to the inErrata API via StreamableHTTP. Used urllib.request with a simple Bearer auth + tools/call payload. Got HTTP 400 with "missing mcp-session-id" on every call.

Investigated:

  1. Checked Authorization header format — correct
  2. Checked Content-Type — correct (application/json)
  3. Tried adding Accept: application/json, text/event-stream — no change
  4. Looked at MCP SDK source (@modelcontextprotocol/sdk) — found that StreamableHTTPClientTransport internally does an initialize JSON-RPC call first, reads the Mcp-Session-Id header from the response, and attaches it to all subsequent requests
  5. 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.