fix(http): create MCP transport per session/request, not one shared instance - #68
Merged
Merged
Conversation
…nstance startHttpServer built a single StreamableHTTPServerTransport at startup and handed it to every request. In stateless mode the SDK throws "Stateless transport cannot be reused across requests" on request two, and because setupHttpMiddleware never awaited handleRequest the rejection surfaced as an empty-body HTTP 500 -- so a hosted server accepted exactly one `initialize` per container start. Stateful mode was broken too, differently: the second client got 400 "Server already initialized". Now each request either reuses the transport for its Mcp-Session-Id or gets a fresh transport plus a fresh MCP Server, and stateless transports are closed when the response ends. setupHttpMiddleware returns the handleRequest promise so failures become a JSON-RPC 500 instead of a dead socket. Also fixes port resolution: `config.port || ...` treated port 0 (bind any free port) as unset and fell through to 3000. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
howethomas
added a commit
that referenced
this pull request
Aug 18, 2026
Minor, not patch: PR #65 adds a new embedding provider, a new edge function and a migration, which is backwards-compatible new functionality. Ships three things that were unreleased on main: - Supabase-native gte-small embeddings, so a dataset needs no third-party embedding API key anywhere in the stack (#65) - MCP over HTTP works past the first request. One transport instance was shared across all requests, so a hosted server accepted exactly one `initialize` per container start and then returned empty-body 500s (#68) - Attachment purpose='tags' is honoured, and the vcon_embeddings upsert is idempotent. Both failed silently before: tags in a spec-correct 0.4.0 document were unreachable, and every backfill pass duplicated vectors (#67, #65) Supersedes the abandoned 1.3.1 release commit on claude/peaceful-nobel, which covered only the tags fix. Also gitignores .omc/ and .claude/worktrees/, agent session state that was swept into a commit once already. 820 tests pass, 14 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The MCP-over-HTTP endpoint accepted exactly one request per container start. Verified against a live deployment (1.3.0,
MCP_TRANSPORT=http,MCP_HTTP_STATELESS=true): firstinitializereturned 200 with a correct envelope, every subsequent request returned an empty-body HTTP 500.tools/listas a separate POST also 500'd. REST under/api/v1was unaffected because it is routed to Koa before the MCP fall-through.Root cause
startHttpServercreated oneStreamableHTTPServerTransportat startup and passed that same instance tosetupHttpMiddlewarefor every request. The SDK rejects this outright (webStandardStreamableHttp.js):setupHttpMiddlewarecalledtransport.handleRequest(req, res)without awaiting, so that rejection never became a response — hence the empty body behind the 500.Stateful mode was broken too, differently: a second client's
initializegot400 Invalid Request: Server already initialized, and sessions would have collided between clients anyway.Reproduced both against the old shape:
enableJsonResponseFix
startHttpServernow takes a() => Serverfactory instead of a pre-built server + transport. An MCPServerbinds to exactly one transport, so HTTP needs one per session (stateful) / per request (stateless).Mcp-Session-Id, or gets a fresh transport plus a freshServer. Stateful sessions are tracked in a map keyed by session id, populated fromonsessioninitializedand dropped ononclose.setupHttpMiddlewarereturns thehandleRequestpromise, so a failure becomes a JSON-RPC 500 body instead of a dead socket.config.port || parseInt(...)treated port 0 (bind any free port) as unset and fell through to 3000. Now??.Notes on the target design
MCP_HTTP_JSON_ONLY/enableJsonResponsemakes no difference to any of this — the reuse guard runs before response shaping. The regression tests are parametrized over both values to keep that documented.MCP spec revision 2026-07-28 (SEP-2567/SEP-2575) removes sessions and the handshake entirely, but the pinned SDK (1.26.0) still tops out at protocol
2025-11-25and has no sessionless path, so the session-aware design is still required today. Per-request transport is what that spec makes mandatory anyway, so this moves toward it rather than away.Tests
New
tests/transport/http-lifecycle.test.ts: two full client handshakes in sequence against one running server,tools/listas a separate request afterinitialize, and two concurrent clients — each across stateless/stateful × jsonOnly on/off (12 tests). Full suite: 820 passed.🤖 Generated with Claude Code