1- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
2- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js" ;
1+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
2+ import type { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js" ;
33import {
44 type IncomingHttpHeaders ,
55 IncomingMessage ,
88import { createClient } from "redis" ;
99import { Socket } from "node:net" ;
1010import { Readable } from "node:stream" ;
11- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" ;
11+ import type { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" ;
1212import type { BodyType } from "./server-response-adapter" ;
1313import assert from "node:assert" ;
1414import type {
@@ -19,6 +19,36 @@ import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types";
1919import { getAuthContext } from "../auth/auth-context" ;
2020import { ServerOptions } from "." ;
2121
22+ // Lazy-loaded SDK modules. The @modelcontextprotocol/sdk has transitive
23+ // dependencies (via undici) that replace globalThis.Response at import
24+ // time. Eagerly importing the SDK at the module level causes Next.js
25+ // route handlers to fail the `instanceof Response` check with
26+ // "No response is returned from route handler" errors.
27+ // See: https://github.com/vercel/mcp-handler/issues/140
28+ let McpServerClass : typeof McpServer ;
29+ let SSEServerTransportClass : typeof SSEServerTransport ;
30+ let WebStandardStreamableHTTPServerTransportClass : typeof WebStandardStreamableHTTPServerTransport ;
31+
32+ async function loadSdk ( ) {
33+ if ( McpServerClass ) return ;
34+
35+ const OriginalResponse = globalThis . Response ;
36+
37+ const [ mcpMod , sseMod , httpMod ] = await Promise . all ( [
38+ import ( "@modelcontextprotocol/sdk/server/mcp.js" ) ,
39+ import ( "@modelcontextprotocol/sdk/server/sse.js" ) ,
40+ import ( "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" ) ,
41+ ] ) ;
42+
43+ // Restore the original Response so that other route handlers in the
44+ // same process continue to pass Next.js's instanceof check.
45+ globalThis . Response = OriginalResponse ;
46+
47+ McpServerClass = mcpMod . McpServer ;
48+ SSEServerTransportClass = sseMod . SSEServerTransport ;
49+ WebStandardStreamableHTTPServerTransportClass = httpMod . WebStandardStreamableHTTPServerTransport ;
50+ }
51+
2252interface SerializedRequest {
2353 requestId : string ;
2454 url : string ;
@@ -275,13 +305,13 @@ export function initializeMcpApiHandler(
275305
276306 // Note: In SDK 1.26.0+, stateless transports cannot be reused across requests.
277307 // We create a fresh transport and server per POST request.
278-
308+
279309 // Start periodic cleanup if not already running
280310 if ( ! cleanupInterval ) {
281311 cleanupInterval = setInterval ( ( ) => {
282312 const now = Date . now ( ) ;
283313 const staleThreshold = 5 * 60 * 1000 ; // 5 minutes
284-
314+
285315 servers = servers . filter ( server => {
286316 const metadata = serverMetadata . get ( server ) ;
287317 if ( ! metadata ) {
@@ -296,7 +326,7 @@ export function initializeMcpApiHandler(
296326 }
297327 return false ;
298328 }
299-
329+
300330 const age = now - metadata . createdAt . getTime ( ) ;
301331 if ( age > staleThreshold ) {
302332 logger . log ( `Removing stale server (session ${ metadata . sessionId } , age: ${ age } ms)` ) ;
@@ -313,13 +343,15 @@ export function initializeMcpApiHandler(
313343 serverMetadata . delete ( server ) ;
314344 return false ;
315345 }
316-
346+
317347 return true ;
318348 } ) ;
319349 } , 30 * 1000 ) ; // Run every 30 seconds
320350 }
321351
322352 return async function mcpApiHandler ( req : Request , res : ServerResponse ) {
353+ await loadSdk ( ) ;
354+
323355 const url = new URL ( req . url || "" , "https://example.com" ) ;
324356 if ( url . pathname === streamableHttpEndpoint ) {
325357 if ( req . method === "GET" ) {
@@ -369,10 +401,10 @@ export function initializeMcpApiHandler(
369401 // In SDK 1.26.0+, stateless transports cannot be reused across requests.
370402 // Create a fresh transport and server per POST request and use the
371403 // WebStandard transport directly since we already have a Web Request.
372- const transport = new WebStandardStreamableHTTPServerTransport ( {
404+ const transport = new WebStandardStreamableHTTPServerTransportClass ( {
373405 sessionIdGenerator : sessionIdGenerator ,
374406 } ) ;
375- const server = new McpServer ( serverInfo , mcpServerOptions ) ;
407+ const server = new McpServerClass ( serverInfo , mcpServerOptions ) ;
376408 await initializeServer ( server ) ;
377409 await server . connect ( transport ) ;
378410
@@ -472,7 +504,7 @@ export function initializeMcpApiHandler(
472504 } ) ;
473505 logger . log ( "Got new SSE connection" ) ;
474506 assert ( sseMessageEndpoint , "sseMessageEndpoint is required" ) ;
475- const transport = new SSEServerTransport ( sseMessageEndpoint , res ) ;
507+ const transport = new SSEServerTransportClass ( sseMessageEndpoint , res ) ;
476508 const sessionId = transport . sessionId ;
477509
478510 const eventRes = new EventEmittingResponse (
@@ -488,23 +520,23 @@ export function initializeMcpApiHandler(
488520 undefined ,
489521 } ) ;
490522
491- const server = new McpServer ( serverInfo , serverOptions ) ;
492-
523+ const server = new McpServerClass ( serverInfo , serverOptions ) ;
524+
493525 // Track cleanup state to prevent double cleanup
494526 let isCleanedUp = false ;
495527 let interval : NodeJS . Timeout | null = null ;
496528 let timeout : NodeJS . Timeout | null = null ;
497529 let abortHandler : ( ( ) => void ) | null = null ;
498530 let handleMessage : ( ( message : string ) => Promise < void > ) | null = null ;
499531 let logs : { type : LogLevel ; messages : string [ ] ; } [ ] = [ ] ;
500-
532+
501533 // Comprehensive cleanup function
502534 const cleanup = async ( reason : string ) => {
503535 if ( isCleanedUp ) return ;
504536 isCleanedUp = true ;
505-
537+
506538 logger . log ( `Cleaning up SSE connection: ${ reason } ` ) ;
507-
539+
508540 // Clear timers
509541 if ( timeout ) {
510542 clearTimeout ( timeout ) ;
@@ -514,13 +546,13 @@ export function initializeMcpApiHandler(
514546 clearInterval ( interval ) ;
515547 interval = null ;
516548 }
517-
549+
518550 // Remove abort event listener
519551 if ( abortHandler ) {
520552 req . signal . removeEventListener ( "abort" , abortHandler ) ;
521553 abortHandler = null ;
522554 }
523-
555+
524556 // Unsubscribe from Redis
525557 if ( handleMessage ) {
526558 try {
@@ -530,7 +562,7 @@ export function initializeMcpApiHandler(
530562 logger . error ( "Error unsubscribing from Redis:" , error ) ;
531563 }
532564 }
533-
565+
534566 // Close server and transport
535567 try {
536568 if ( server ?. server ) {
@@ -540,30 +572,30 @@ export function initializeMcpApiHandler(
540572 await transport . close ( ) ;
541573 }
542574 } catch ( error ) {
543- logger . error ( "Error closing server/transport :" , error ) ;
575+ logger . error ( "Error closing stale server:" , error ) ;
544576 }
545-
577+
546578 // Remove server from array and WeakMap
547579 servers = servers . filter ( ( s ) => s !== server ) ;
548580 serverMetadata . delete ( server ) ;
549-
581+
550582 // End session event
551583 eventRes . endSession ( "SSE" ) ;
552-
584+
553585 // Clear logs array to free memory
554586 logs = [ ] ;
555-
587+
556588 // End response if not already ended
557589 if ( ! res . headersSent ) {
558590 res . statusCode = 200 ;
559591 res . end ( ) ;
560592 }
561593 } ;
562-
594+
563595 try {
564596 await initializeServer ( server ) ;
565597 servers . push ( server ) ;
566-
598+
567599 // Store metadata in WeakMap
568600 serverMetadata . set ( server , {
569601 sessionId,
@@ -686,12 +718,12 @@ export function initializeMcpApiHandler(
686718
687719 abortHandler = ( ) => resolveTimeout ( "client hang up" ) ;
688720 req . signal . addEventListener ( "abort" , abortHandler ) ;
689-
721+
690722 // Handle response close event
691723 res . on ( "close" , ( ) => {
692724 cleanup ( "response closed" ) ;
693725 } ) ;
694-
726+
695727 // Handle response error event
696728 res . on ( "error" , ( error ) => {
697729 logger . error ( "Response error:" , error ) ;
@@ -748,24 +780,24 @@ export function initializeMcpApiHandler(
748780 let timeout : NodeJS . Timeout | null = null ;
749781 let hasResponded = false ;
750782 let isCleanedUp = false ;
751-
783+
752784 // Cleanup function to ensure all resources are freed
753785 const cleanup = async ( ) => {
754786 if ( isCleanedUp ) return ;
755787 isCleanedUp = true ;
756-
788+
757789 if ( timeout ) {
758790 clearTimeout ( timeout ) ;
759791 timeout = null ;
760792 }
761-
793+
762794 try {
763795 await redis . unsubscribe ( `responses:${ sessionId } :${ requestId } ` ) ;
764796 } catch ( error ) {
765797 logger . error ( "Error unsubscribing from Redis response channel:" , error ) ;
766798 }
767799 } ;
768-
800+
769801 // Safe response handler to prevent double res.end()
770802 const sendResponse = async ( status : number , body : string ) => {
771803 if ( ! hasResponded ) {
@@ -775,7 +807,7 @@ export function initializeMcpApiHandler(
775807 await cleanup ( ) ;
776808 }
777809 } ;
778-
810+
779811 // Response handler
780812 const handleResponse = async ( message : string ) => {
781813 try {
@@ -817,7 +849,7 @@ export function initializeMcpApiHandler(
817849 await cleanup ( ) ;
818850 }
819851 } ) ;
820-
852+
821853 // Handle response error event
822854 res . on ( "error" , async ( error ) => {
823855 logger . error ( "Response error in message handler:" , error ) ;
@@ -892,9 +924,9 @@ function createFakeIncomingMessage(
892924 req . method = method ;
893925 req . url = url ;
894926 req . headers = headers ;
895- req . rawHeaders = Object . entries ( headers ) . flatMap ( ( [ key , value ] ) =>
896- Array . isArray ( value )
897- ? value . flatMap ( v => [ key , v ] )
927+ req . rawHeaders = Object . entries ( headers ) . flatMap ( ( [ key , value ] ) =>
928+ Array . isArray ( value )
929+ ? value . flatMap ( v => [ key , v ] )
898930 : [ key , value ?? "" ]
899931 ) ;
900932
0 commit comments