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. diff --git a/packages/middleware/node/src/streamableHttp.ts b/packages/middleware/node/src/streamableHttp.ts index 2c107fdf5..f8a155f3a 100644 --- a/packages/middleware/node/src/streamableHttp.ts +++ b/packages/middleware/node/src/streamableHttp.ts @@ -69,14 +69,19 @@ 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 - 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 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 } + ); } /** @@ -157,12 +162,17 @@ export class NodeStreamableHTTPServerTransport implements Transport { const authInfo = req.auth; // Create a custom handler that includes our context - const handler = getRequestListener(async (webRequest: Request) => { - return this._webStandardTransport.handleRequest(webRequest, { - authInfo, - parsedBody - }); - }); + // 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 3cc07e8c2..65f74d175 100644 --- a/packages/middleware/node/test/streamableHttp.test.ts +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -2932,6 +2932,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