-
Notifications
You must be signed in to change notification settings - Fork 36
Description
Summary
The finally { transport.close() } block added in v1.2.1 of @decocms/runtime breaks Server-Sent Events (SSE) streaming for MCP endpoints. The transport is closed immediately after handleRequest() returns, but before the async MCP message handler can write the response to the stream.
Affected Versions
- Broken: 1.2.1, 1.2.2, 1.2.3, 1.2.4
- Working: 1.2.0 and earlier
Location
packages/runtime/src/tools.ts - the fetch function in createMCPServer()
Current Code (Broken)
const fetch = async (req: Request, env: TEnv) => {
const { server } = await createServer(env);
const transport = new HttpServerTransport();
await server.connect(transport);
try {
return await transport.handleRequest(req);
} finally {
// CRITICAL: Close transport to prevent memory leaks
// Without this, ReadableStream/WritableStream controllers accumulate
// causing thousands of stream objects to be retained in memory
try {
await transport.close?.();
} catch {
// Ignore close errors - transport may already be closed
}
}
};Problem
The MCP SDK's WebStandardStreamableHTTPServerTransport.handleRequest() returns a Response with an SSE stream immediately, then writes to the stream asynchronously when the MCP server processes messages via onmessage → send().
The timeline:
handleRequest()creates aReadableStream, callsonmessage(message)(not awaited), returnsResponseimmediatelyfinallyblock executes →transport.close()is calledclose()callscleanup()on all streams → closes the ReadableStream controller- MCP server finishes processing → tries to call
send()→ stream already closed!
Result: Client receives HTTP 200 with Content-Type: text/event-stream but empty body (0 bytes).
Reproduction
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
# Returns: HTTP 200, Content-Type: text/event-stream, Content-Length: 0 (empty body)Suggested Fix
The transport should not be closed in a finally block for SSE responses. Options:
- Don't close immediately - let the transport manage its own lifecycle:
const fetch = async (req: Request, env: TEnv) => {
const { server } = await createServer(env);
const transport = new HttpServerTransport();
await server.connect(transport);
return await transport.handleRequest(req);
};- Close only after stream completes - the transport already handles cleanup when all responses are sent via the stream's cleanup callback in
_streamMapping
Workaround
Pin to @decocms/runtime@1.2.0:
"@decocms/runtime": "1.2.0"