Summary
In HTTP mode (src/http.ts / dist/http.js), createHttpApp creates a single McpServer at construction time and calls mcpServer.connect(transport) on every /mcp request:
const mcpServer = createPerplexityServer(); // one shared server
app.all("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, ... });
await mcpServer.connect(transport); // 2nd concurrent connect throws
...
});
Because the streamable-HTTP transport is stateless (sessionIdGenerator: undefined), there is no session to reuse. Once one request's transport is connected, any request that arrives before it closes calls connect() on the already-connected server and throws:
Error: Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.
and the handler returns HTTP 500.
Impact
This breaks real MCP clients over HTTP. A standards-compliant client sends initialize, then tools/list (and typically holds a stream open), so its second request 500s and no tools ever register. A strictly-sequential caller happens to dodge it, because res.on("close") closes the transport and frees the shared server between requests — which is why a single one-shot curl "works" while an actual client does not.
Reproduce
Run the HTTP server and point any real MCP client (or two overlapping requests) at it:
PERPLEXITY_API_KEY=... npm run start:http
initialize succeeds; the follow-up request returns 500 and the server logs:
ERROR: Error handling MCP request {"error":"Error: Already connected to a transport. ..."}
Fix
Create the McpServer (and transport) per request — the SDK's documented stateless pattern — and close it on response close. PR attached.
Environment
@perplexity-ai/mcp-server 0.9.0, streamable-HTTP transport (start:http), stateless mode.
Summary
In HTTP mode (
src/http.ts/dist/http.js),createHttpAppcreates a singleMcpServerat construction time and callsmcpServer.connect(transport)on every/mcprequest:Because the streamable-HTTP transport is stateless (
sessionIdGenerator: undefined), there is no session to reuse. Once one request's transport is connected, any request that arrives before it closes callsconnect()on the already-connected server and throws:and the handler returns HTTP 500.
Impact
This breaks real MCP clients over HTTP. A standards-compliant client sends
initialize, thentools/list(and typically holds a stream open), so its second request 500s and no tools ever register. A strictly-sequential caller happens to dodge it, becauseres.on("close")closes the transport and frees the shared server between requests — which is why a single one-shotcurl"works" while an actual client does not.Reproduce
Run the HTTP server and point any real MCP client (or two overlapping requests) at it:
initializesucceeds; the follow-up request returns 500 and the server logs:Fix
Create the
McpServer(and transport) per request — the SDK's documented stateless pattern — and close it on response close. PR attached.Environment
@perplexity-ai/mcp-server0.9.0, streamable-HTTP transport (start:http), stateless mode.