You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hit / or any route that goes through Next proxy/middleware.
The app may start returning 500 with Expected an instance of Response to be returned.
Root Cause
@modelcontextprotocol/sdk/server/streamableHttp.js uses @hono/node-server's getRequestListener() without options. Hono defaults overrideGlobalObjects to true and replaces global.Request and global.Response.
Minimal proof inside the container:
node --input-type=module -e 'const nativeResponse = globalThis.Response;const nativeRequest = globalThis.Request;const { getRequestListener } = await import("@hono/node-server");console.log("before", globalThis.Response === nativeResponse, globalThis.Request === nativeRequest, globalThis.Response.name);getRequestListener(() => new Response("ok"));console.log("after", globalThis.Response === nativeResponse, globalThis.Request === nativeRequest, globalThis.Response.name);const r = new nativeResponse("ok");console.log("native instance of current Response", r instanceof Response);'
Observed:
before true true Response
after false false _Response
native instance of current Response false
Next.js proxy/middleware later checks that the returned value is instanceof Response. After Hono changes global.Response, NextResponse.next() is no longer an instance of the current global Response, so Next throws.
Workaround/Fix Used
Avoid the MCP SDK Node transport wrapper and use the SDK Web Standard transport directly:
Replace StreamableHTTPServerTransport with WebStandardStreamableHTTPServerTransport.
Convert NextApiRequest headers to Web Headers.
Pass the already-parsed JSON-RPC body through transport.handleRequest(webRequest, { parsedBody }).
Pipe the returned Web Response back to NextApiResponse with Readable.fromWeb.
This avoids @hono/node-server entirely, so global.Request/global.Response stay untouched.
MCP endpoint corrupts Next.js 16 proxy Response realm after StreamableHTTPServerTransport
Summary
Self-hosted
prompts.chaton Docker/Next.js 16.1.0 becomes globally broken after/api/mcphandles Streamable HTTP requests. Once triggered, unrelated routes start returning 500 with:PR #1087 (singleton
McpServer+ AsyncLocalStorage) does not fix this. The failure is not caused by per-requestMcpServerconstruction.Environment
ghcr.io/f/prompts.chat:latest@modelcontextprotocol/sdk1.25.1src/pages/api/mcp.tsReproduction
PCHAT_FEATURE_MCP=true./or any route that goes through Next proxy/middleware.Expected an instance of Response to be returned.Root Cause
@modelcontextprotocol/sdk/server/streamableHttp.jsuses@hono/node-server'sgetRequestListener()without options. Hono defaultsoverrideGlobalObjectsto true and replacesglobal.Requestandglobal.Response.Minimal proof inside the container:
Observed:
Next.js proxy/middleware later checks that the returned value is
instanceof Response. After Hono changesglobal.Response,NextResponse.next()is no longer an instance of the current globalResponse, so Next throws.Workaround/Fix Used
Avoid the MCP SDK Node transport wrapper and use the SDK Web Standard transport directly:
StreamableHTTPServerTransportwithWebStandardStreamableHTTPServerTransport.NextApiRequestheaders to WebHeaders.transport.handleRequest(webRequest, { parsedBody }).Responseback toNextApiResponsewithReadable.fromWeb.This avoids
@hono/node-serverentirely, soglobal.Request/global.Responsestay untouched.Validation
On the patched image:
initialize: 200initializerequests: 100/100 200tools/call search_promptsrequests: 100/100 200/: 200 after stress tests/api/health: 200 after stress testsExpected an instance of Response: 0MCP error: 0Suggested Upstream Options
WebStandardStreamableHTTPServerTransportin Next.js.@modelcontextprotocol/sdkto call HonogetRequestListener(..., { overrideGlobalObjects: false })inStreamableHTTPServerTransport.McpServeris still desired, but it is not the root cause of this self-hosted Docker crash.