11/**
2- * Shared utilities for running MCP servers with HTTP transports.
3- *
4- * Supports:
5- * - Streamable HTTP transport (/mcp) - stateful sessions
6- * - Legacy SSE transport (/sse, /messages) - backwards compatibility
2+ * Shared utilities for running MCP servers with Streamable HTTP transport.
73 */
84
9- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
105import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js" ;
11- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse .js" ;
6+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp .js" ;
127import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js" ;
13- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js" ;
148import cors from "cors" ;
15- import { randomUUID } from "node:crypto" ;
169import type { Request , Response } from "express" ;
1710
1811export interface ServerOptions {
19- /** Port to listen on (required). */
2012 port : number ;
21- /** Server name for logging. */
2213 name ?: string ;
2314}
2415
25- type Transport = StreamableHTTPServerTransport | SSEServerTransport ;
26-
27- /** Session state: transport + its dedicated server instance */
28- interface Session {
29- transport : Transport ;
30- server : McpServer ;
31- }
32-
3316/**
34- * Starts an MCP server with HTTP transports .
17+ * Starts an MCP server with Streamable HTTP transport in stateless mode .
3518 *
36- * Provides:
37- * - /mcp (GET/POST/DELETE): Streamable HTTP with stateful sessions
38- * - /sse (GET) + /messages (POST): Legacy SSE for older clients
39- *
40- * @param createServer - Factory function that creates a new McpServer instance per session.
41- * Each session needs its own server because McpServer only supports
42- * one transport at a time.
19+ * @param createServer - Factory function that creates a new McpServer instance per request.
20+ * @param options - Server configuration options.
4321 */
4422export async function startServer (
4523 createServer : ( ) => McpServer ,
4624 options : ServerOptions ,
4725) : Promise < void > {
4826 const { port, name = "MCP Server" } = options ;
4927
50- // Session store: each session has its own transport AND server instance
51- const sessions = new Map < string , Session > ( ) ;
52-
53- // Express app - bind to all interfaces for development/testing
5428 const app = createMcpExpressApp ( { host : "0.0.0.0" } ) ;
55- app . use (
56- cors ( {
57- exposedHeaders : [ "mcp-session-id" ] ,
58- } ) ,
59- ) ;
29+ app . use ( cors ( ) ) ;
6030
61- // Streamable HTTP (stateful)
6231 app . all ( "/mcp" , async ( req : Request , res : Response ) => {
63- try {
64- const sessionId = req . headers [ "mcp-session-id" ] as string | undefined ;
65- let session = sessionId ? sessions . get ( sessionId ) : undefined ;
66-
67- // Session exists but wrong transport type
68- if (
69- session &&
70- ! ( session . transport instanceof StreamableHTTPServerTransport )
71- ) {
72- return res . status ( 400 ) . json ( {
73- jsonrpc : "2.0" ,
74- error : { code : - 32000 , message : "Session uses different transport" } ,
75- id : null ,
76- } ) ;
77- }
78-
79- // New session requires initialize request
80- if ( ! session ) {
81- if ( req . method !== "POST" || ! isInitializeRequest ( req . body ) ) {
82- return res . status ( 400 ) . json ( {
83- jsonrpc : "2.0" ,
84- error : { code : - 32000 , message : "Bad request: not initialized" } ,
85- id : null ,
86- } ) ;
87- }
88-
89- // Create new server instance for this session (McpServer supports only one transport)
90- const serverInstance = createServer ( ) ;
32+ const server = createServer ( ) ;
33+ const transport = new StreamableHTTPServerTransport ( {
34+ sessionIdGenerator : undefined ,
35+ } ) ;
9136
92- const transport = new StreamableHTTPServerTransport ( {
93- sessionIdGenerator : ( ) => randomUUID ( ) ,
94- onsessioninitialized : ( id ) => {
95- sessions . set ( id , { transport, server : serverInstance } ) ;
96- } ,
97- } ) ;
98- transport . onclose = ( ) => {
99- if ( transport . sessionId ) sessions . delete ( transport . sessionId ) ;
100- } ;
101- await serverInstance . connect ( transport ) ;
102- session = { transport, server : serverInstance } ;
103- }
37+ res . on ( "close" , ( ) => {
38+ transport . close ( ) . catch ( ( ) => { } ) ;
39+ server . close ( ) . catch ( ( ) => { } ) ;
40+ } ) ;
10441
105- await ( session . transport as StreamableHTTPServerTransport ) . handleRequest (
106- req ,
107- res ,
108- req . body ,
109- ) ;
42+ try {
43+ await server . connect ( transport ) ;
44+ await transport . handleRequest ( req , res , req . body ) ;
11045 } catch ( error ) {
11146 console . error ( "MCP error:" , error ) ;
11247 if ( ! res . headersSent ) {
@@ -119,63 +54,15 @@ export async function startServer(
11954 }
12055 } ) ;
12156
122- // Legacy SSE
123- app . get ( "/sse" , async ( _req : Request , res : Response ) => {
124- try {
125- // Create new server instance for this session (McpServer supports only one transport)
126- const serverInstance = createServer ( ) ;
127- const transport = new SSEServerTransport ( "/messages" , res ) ;
128- sessions . set ( transport . sessionId , { transport, server : serverInstance } ) ;
129- res . on ( "close" , ( ) => sessions . delete ( transport . sessionId ) ) ;
130- await serverInstance . connect ( transport ) ;
131- } catch ( error ) {
132- console . error ( "SSE error:" , error ) ;
133- if ( ! res . headersSent ) res . status ( 500 ) . end ( ) ;
134- }
135- } ) ;
136-
137- app . post ( "/messages" , async ( req : Request , res : Response ) => {
138- try {
139- const session = sessions . get ( req . query . sessionId as string ) ;
140- if ( ! session || ! ( session . transport instanceof SSEServerTransport ) ) {
141- return res . status ( 404 ) . json ( {
142- jsonrpc : "2.0" ,
143- error : { code : - 32001 , message : "Session not found" } ,
144- id : null ,
145- } ) ;
146- }
147- await session . transport . handlePostMessage ( req , res , req . body ) ;
148- } catch ( error ) {
149- console . error ( "Message error:" , error ) ;
150- if ( ! res . headersSent ) {
151- res . status ( 500 ) . json ( {
152- jsonrpc : "2.0" ,
153- error : { code : - 32603 , message : "Internal server error" } ,
154- id : null ,
155- } ) ;
156- }
157- }
57+ const httpServer = app . listen ( port , ( ) => {
58+ console . log ( `${ name } listening on http://localhost:${ port } /mcp` ) ;
15859 } ) ;
15960
160- return new Promise < void > ( ( resolve , reject ) => {
161- const httpServer = app . listen ( port ) ;
61+ const shutdown = ( ) => {
62+ console . log ( "\nShutting down..." ) ;
63+ httpServer . close ( ( ) => process . exit ( 0 ) ) ;
64+ } ;
16265
163- httpServer . on ( "listening" , ( ) => {
164- console . log ( `${ name } listening on http://localhost:${ port } /mcp` ) ;
165- resolve ( ) ;
166- } ) ;
167-
168- httpServer . on ( "error" , ( err : Error ) => {
169- reject ( err ) ;
170- } ) ;
171-
172- const shutdown = ( ) => {
173- console . log ( "\nShutting down..." ) ;
174- sessions . forEach ( ( session ) => session . transport . close ( ) . catch ( ( ) => { } ) ) ;
175- httpServer . close ( ( ) => process . exit ( 0 ) ) ;
176- } ;
177-
178- process . on ( "SIGINT" , shutdown ) ;
179- process . on ( "SIGTERM" , shutdown ) ;
180- } ) ;
66+ process . on ( "SIGINT" , shutdown ) ;
67+ process . on ( "SIGTERM" , shutdown ) ;
18168}
0 commit comments