diff --git a/src/server/mcp/transport.test.ts b/src/server/mcp/transport.test.ts index 23cf363..0b4aedc 100644 --- a/src/server/mcp/transport.test.ts +++ b/src/server/mcp/transport.test.ts @@ -160,6 +160,28 @@ describe("handleSelfHostedOpenSeoMcpRequest", () => { }); }); + // The OOM came from the GET SSE stream pinning a per-request McpServer, so + // GET must 405 without ever building one. + it("returns 405 for the standalone GET SSE stream without building a server", async () => { + const { handleSelfHostedOpenSeoMcpRequest } = + await import("@/server/mcp/transport"); + + const response = await handleSelfHostedOpenSeoMcpRequest( + new Request("https://open-seo.test/mcp", { + method: "GET", + headers: { Accept: "text/event-stream" }, + }), + "local_noauth", + {}, + ctx, + ); + + expect(response.status).toBe(405); + expect(response.headers.get("Allow")).toContain("POST"); + // nextServerId only advances when a server is built — a GET must not. + expect(serverMocks.nextServerId).toBe(0); + }); + it("lets the MCP transport handle OPTIONS without auth context", async () => { const { handleSelfHostedOpenSeoMcpRequest } = await import("@/server/mcp/transport"); diff --git a/src/server/mcp/transport.ts b/src/server/mcp/transport.ts index d0f963b..603b4c8 100644 --- a/src/server/mcp/transport.ts +++ b/src/server/mcp/transport.ts @@ -97,6 +97,24 @@ function handleOpenSeoMcpRequest( env: unknown, ctx: ExecutionContext, ): Promise { + // Decline the optional standalone GET SSE stream: this server is stateless + // (POST returns JSON) and pushes no server-initiated messages, so the stream + // does nothing but leak memory — each GET is held open by a keepalive and + // pins a per-request McpServer (~5MB), so a few dozen concurrent clients OOM + // the 128MB isolate. 405 is the spec's "no stream" response; returning it + // before building the server means a GET allocates nothing. + if (request.method === "GET") { + return Promise.resolve( + new Response("Method Not Allowed", { + status: 405, + headers: { + Allow: "POST, DELETE, OPTIONS", + "Access-Control-Allow-Origin": "*", + }, + }), + ); + } + const server = createOpenSeoMcpServer(); const handler = createMcpHandler(server, { route: MCP_ROUTE,