Skip to content

MCP endpoint corrupts Next.js 16 proxy Response realm after StreamableHTTPServerTransport #1205

Description

@ouchanip

MCP endpoint corrupts Next.js 16 proxy Response realm after StreamableHTTPServerTransport

Summary

Self-hosted prompts.chat on Docker/Next.js 16.1.0 becomes globally broken after /api/mcp handles Streamable HTTP requests. Once triggered, unrelated routes start returning 500 with:

TypeError: Expected an instance of Response to be returned
    at .../next/dist/server/web/adapter.js

PR #1087 (singleton McpServer + AsyncLocalStorage) does not fix this. The failure is not caused by per-request McpServer construction.

Environment

  • ghcr.io/f/prompts.chat:latest
  • Next.js 16.1.0
  • @modelcontextprotocol/sdk 1.25.1
  • MCP endpoint: src/pages/api/mcp.ts
  • Docker self-host with external PostgreSQL

Reproduction

  1. Start prompts.chat with PCHAT_FEATURE_MCP=true.
  2. Send one or more Streamable HTTP initialize requests:
curl -sS -X POST http://localhost:14444/api/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"repro","version":"0.1.0"}}}'
  1. Hit / or any route that goes through Next proxy/middleware.
  2. 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.

Validation

On the patched image:

  • single initialize: 200
  • 100 initialize requests: 100/100 200
  • 100 tools/call search_prompts requests: 100/100 200
  • /: 200 after stress tests
  • /api/health: 200 after stress tests
  • logs containing Expected an instance of Response: 0
  • logs containing MCP error: 0

Suggested Upstream Options

  1. Patch prompts.chat to use WebStandardStreamableHTTPServerTransport in Next.js.
  2. Or ask @modelcontextprotocol/sdk to call Hono getRequestListener(..., { overrideGlobalObjects: false }) in StreamableHTTPServerTransport.
  3. Re-open/revisit refactor: singleton McpServer with AsyncLocalStorage context #1087 only if keeping a singleton McpServer is still desired, but it is not the root cause of this self-hosted Docker crash.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    In progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions