Skip to content

fix: use per-session transports in HTTP mode instead of one shared transport#3

Open
informatJonas wants to merge 1 commit into
zcube:mainfrom
informatJonas:fix/http-transport-shared-session
Open

fix: use per-session transports in HTTP mode instead of one shared transport#3
informatJonas wants to merge 1 commit into
zcube:mainfrom
informatJonas:fix/http-transport-shared-session

Conversation

@informatJonas

Copy link
Copy Markdown

What's broken

StreamableHTTPServerTransport (from @modelcontextprotocol/sdk) supports exactly one session per instance — it throws Server already initialized on a second initialize call against the same transport. The HTTP server (src/http-server.ts) creates a single shared transport instance for the entire process and routes every incoming connection through it:

const mcpServer = createMcpServer();
const transport = new StreamableHTTPServerTransport({ ... });
// ...
await transport.handleRequest(req, res, parsedBody); // same transport for every request

Result: only the first client that ever connects can use the server. Any subsequent initialize — a second client, or the same client reconnecting after an idle timeout / health check — fails permanently until the process is restarted:

{"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Mcp-Session-Id header is required"},"id":null}

and the server logs:

[MCP Error] Error: Invalid Request: Server already initialized

This makes the HTTP transport unreliable behind any client that periodically re-checks connectivity (e.g. Claude Code's claude mcp list health check does a fresh handshake) — filed in more detail as #2 with full repro steps.

The fix

Keep a Map<sessionId, StreamableHTTPServerTransport>. On each request:

  • if the Mcp-Session-Id header matches a known session, route to that session's transport
  • otherwise (a fresh initialize), create a new Server + StreamableHTTPServerTransport pair, register it in the map once the SDK assigns a session id (onsessioninitialized), and remove it on onsessionclosed

This is the standard multi-session pattern for StreamableHTTPServerTransport (one transport instance per session, looked up by session id).

Testing

Verified against a self-hosted Penpot 2.16.2 instance:

  • Three independent clients can initialize concurrently without a restart (previously the 2nd+ would fail)
  • Tool calls (get_profile, list_teams, etc.) work correctly on each session
  • /health endpoint still reports the right activeSessionCount
  • Graceful shutdown now closes all open transports instead of just one

Closes #2.

…ansport

StreamableHTTPServerTransport supports exactly one session per instance
(it throws "Server already initialized" on a second initialize call).
The original code created a single transport shared across every
incoming connection, so only the first client to ever connect could
use the server — every reconnect after that (health checks, idle
timeouts, a second client) failed permanently until the process was
restarted.

Fixed by keeping a Map of sessionId -> transport, creating a fresh
MCP server + transport pair on each new initialize request, and
routing subsequent requests by the Mcp-Session-Id header.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP transport: server-wide 'already initialized' error blocks reconnection after client disconnect

1 participant