From 2bf9b8f30850b18e62dde3cc1807b070b560ff0e Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Tue, 20 Jan 2026 11:55:07 +0100 Subject: [PATCH 1/3] fix: prevent Hono from overriding global Response object Pass `overrideGlobalObjects: false` to `getRequestListener()` calls in NodeStreamableHTTPServerTransport to prevent Hono from overwriting the global Response object. This fixes an issue where frameworks like Next.js whose response classes extend the native Response would break after initializing MCP transport, as `instanceof Response` checks would fail. Fixes #1369 --- .../middleware/node/src/streamableHttp.ts | 8 +- .../node/test/streamableHttp.test.ts | 83 +++++++++++++++++++ pnpm-lock.yaml | 18 +++- pnpm-workspace.yaml | 2 +- 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/packages/middleware/node/src/streamableHttp.ts b/packages/middleware/node/src/streamableHttp.ts index 2c107fdf5..8a0cbc945 100644 --- a/packages/middleware/node/src/streamableHttp.ts +++ b/packages/middleware/node/src/streamableHttp.ts @@ -69,6 +69,8 @@ export class NodeStreamableHTTPServerTransport implements Transport { // Create a request listener that wraps the web standard transport // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming + // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would + // break frameworks like Next.js whose response classes extend the native Response this._requestListener = getRequestListener(async (webRequest: Request) => { // Get context if available (set during handleRequest) const context = this._requestContext.get(webRequest); @@ -76,7 +78,7 @@ export class NodeStreamableHTTPServerTransport implements Transport { authInfo: context?.authInfo, parsedBody: context?.parsedBody }); - }); + }, { overrideGlobalObjects: false }); } /** @@ -157,12 +159,14 @@ export class NodeStreamableHTTPServerTransport implements Transport { const authInfo = req.auth; // Create a custom handler that includes our context + // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would + // break frameworks like Next.js whose response classes extend the native Response const handler = getRequestListener(async (webRequest: Request) => { return this._webStandardTransport.handleRequest(webRequest, { authInfo, parsedBody }); - }); + }, { overrideGlobalObjects: false }); // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion // including proper SSE streaming support diff --git a/packages/middleware/node/test/streamableHttp.test.ts b/packages/middleware/node/test/streamableHttp.test.ts index 5f865ef49..6d4a13e28 100644 --- a/packages/middleware/node/test/streamableHttp.test.ts +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -2933,6 +2933,89 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); }); +describe('NodeStreamableHTTPServerTransport global Response preservation', () => { + it('should not override the global Response object', () => { + // Store reference to the original global Response constructor + const OriginalResponse = globalThis.Response; + + // Create a custom class that extends Response (similar to Next.js's NextResponse) + class CustomResponse extends Response { + customProperty = 'test'; + } + + // Verify instanceof works before creating transport + const customResponseBefore = new CustomResponse('test body'); + expect(customResponseBefore instanceof Response).toBe(true); + expect(customResponseBefore instanceof OriginalResponse).toBe(true); + + // Create the transport - this should NOT override globalThis.Response + const transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID() + }); + + // Verify the global Response is still the original + expect(globalThis.Response).toBe(OriginalResponse); + + // Verify instanceof still works after creating transport + const customResponseAfter = new CustomResponse('test body'); + expect(customResponseAfter instanceof Response).toBe(true); + expect(customResponseAfter instanceof OriginalResponse).toBe(true); + + // Verify that instances created before transport initialization still work + expect(customResponseBefore instanceof Response).toBe(true); + + // Clean up + transport.close(); + }); + + it('should not override the global Response object when calling handleRequest', async () => { + // Store reference to the original global Response constructor + const OriginalResponse = globalThis.Response; + + // Create a custom class that extends Response + class CustomResponse extends Response { + customProperty = 'test'; + } + + const transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID() + }); + + // Create a mock server to test handleRequest + const port = await getFreePort(); + const httpServer = createServer(async (req, res) => { + await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res); + }); + + await new Promise(resolve => { + httpServer.listen(port, () => resolve()); + }); + + try { + // Make a request to trigger handleRequest + await fetch(`http://localhost:${port}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + // Verify the global Response is still the original after handleRequest + expect(globalThis.Response).toBe(OriginalResponse); + + // Verify instanceof still works + const customResponse = new CustomResponse('test body'); + expect(customResponse instanceof Response).toBe(true); + expect(customResponse instanceof OriginalResponse).toBe(true); + } finally { + await transport.close(); + httpServer.close(); + } + }); +}); + /** * Helper to create test server with DNS rebinding protection options */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66600384f..4c2faac26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,8 +87,8 @@ catalogs: version: 6.1.3 runtimeServerOnly: '@hono/node-server': - specifier: ^1.19.7 - version: 1.19.7 + specifier: ^1.19.8 + version: 1.19.8 cors: specifier: ^2.8.5 version: 2.8.5 @@ -310,7 +310,7 @@ importers: dependencies: '@hono/node-server': specifier: catalog:runtimeServerOnly - version: 1.19.7(hono@4.11.3) + version: 1.19.8(hono@4.11.3) '@modelcontextprotocol/examples-shared': specifier: workspace:^ version: link:../shared @@ -700,7 +700,7 @@ importers: dependencies: '@hono/node-server': specifier: catalog:runtimeServerOnly - version: 1.19.7(hono@4.11.3) + version: 1.19.8(hono@4.11.3) devDependencies: '@eslint/js': specifier: catalog:devTools @@ -1211,6 +1211,12 @@ packages: peerDependencies: hono: ^4 + '@hono/node-server@1.19.8': + resolution: {integrity: sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -4225,6 +4231,10 @@ snapshots: dependencies: hono: 4.11.3 + '@hono/node-server@1.19.8(hono@4.11.3)': + dependencies: + hono: 4.11.3 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 61f34ddb3..338341955 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -34,7 +34,7 @@ catalogs: eventsource-parser: ^3.0.0 jose: ^6.1.1 runtimeServerOnly: - '@hono/node-server': ^1.19.7 + '@hono/node-server': ^1.19.8 content-type: ^1.0.5 cors: ^2.8.5 express: ^5.2.1 From d182e225875aabd5c7c89ba078a1f481167b09ca Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Tue, 20 Jan 2026 12:02:43 +0100 Subject: [PATCH 2/3] chore: add changeset for hono overrideGlobalObjects fix --- .changeset/brave-lions-glow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-lions-glow.md diff --git a/.changeset/brave-lions-glow.md b/.changeset/brave-lions-glow.md new file mode 100644 index 000000000..59a1f3a3a --- /dev/null +++ b/.changeset/brave-lions-glow.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/node": patch +--- + +Prevent Hono from overriding global Response object by passing `overrideGlobalObjects: false` to `getRequestListener()`. This fixes compatibility with frameworks like Next.js whose response classes extend the native Response. From 2ddf20048533b7a5742ad440499a55bc998430dd Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Tue, 20 Jan 2026 12:05:35 +0100 Subject: [PATCH 3/3] style: fix prettier formatting --- .../middleware/node/src/streamableHttp.ts | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/middleware/node/src/streamableHttp.ts b/packages/middleware/node/src/streamableHttp.ts index 8a0cbc945..f8a155f3a 100644 --- a/packages/middleware/node/src/streamableHttp.ts +++ b/packages/middleware/node/src/streamableHttp.ts @@ -71,14 +71,17 @@ export class NodeStreamableHTTPServerTransport implements Transport { // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would // break frameworks like Next.js whose response classes extend the native Response - this._requestListener = getRequestListener(async (webRequest: Request) => { - // Get context if available (set during handleRequest) - const context = this._requestContext.get(webRequest); - return this._webStandardTransport.handleRequest(webRequest, { - authInfo: context?.authInfo, - parsedBody: context?.parsedBody - }); - }, { overrideGlobalObjects: false }); + this._requestListener = getRequestListener( + async (webRequest: Request) => { + // Get context if available (set during handleRequest) + const context = this._requestContext.get(webRequest); + return this._webStandardTransport.handleRequest(webRequest, { + authInfo: context?.authInfo, + parsedBody: context?.parsedBody + }); + }, + { overrideGlobalObjects: false } + ); } /** @@ -161,12 +164,15 @@ export class NodeStreamableHTTPServerTransport implements Transport { // Create a custom handler that includes our context // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would // break frameworks like Next.js whose response classes extend the native Response - const handler = getRequestListener(async (webRequest: Request) => { - return this._webStandardTransport.handleRequest(webRequest, { - authInfo, - parsedBody - }); - }, { overrideGlobalObjects: false }); + const handler = getRequestListener( + async (webRequest: Request) => { + return this._webStandardTransport.handleRequest(webRequest, { + authInfo, + parsedBody + }); + }, + { overrideGlobalObjects: false } + ); // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion // including proper SSE streaming support