@@ -9,8 +9,6 @@ import { join } from 'path'
99import { readFileSync } from 'fs'
1010import { tmpdir } from 'os'
1111import { createServer } from 'http'
12- import { randomUUID } from 'crypto'
13- import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
1412
1513const __dirname = import . meta. dirname
1614
@@ -67,8 +65,7 @@ async function getApiKeyInteractively (): Promise<string> {
6765// Initialize API key
6866let SOCKET_API_KEY = process . env [ 'SOCKET_API_KEY' ] || ''
6967
70- // Transport management
71- const transports : Record < string , StreamableHTTPServerTransport > = { }
68+ // No session management: each HTTP request is handled statelessly
7269
7370// Create server instance
7471const server = new McpServer ( {
@@ -246,6 +243,9 @@ if (useHttp) {
246243 // HTTP mode with Server-Sent Events
247244 logger . info ( `Starting HTTP server on port ${ port } ` )
248245
246+ // Singleton transport to preserve initialization state without explicit sessions
247+ let httpTransport : StreamableHTTPServerTransport | null = null
248+
249249 const httpServer = createServer ( async ( req , res ) => {
250250 // Validate Origin header as required by MCP spec
251251 const origin = req . headers . origin
@@ -275,9 +275,8 @@ if (useHttp) {
275275 } else {
276276 res . setHeader ( 'Access-Control-Allow-Origin' , 'http://localhost:3000' )
277277 }
278- res . setHeader ( 'Access-Control-Allow-Methods' , 'GET, POST, DELETE, OPTIONS' )
279- res . setHeader ( 'Access-Control-Allow-Headers' , 'Content-Type, mcp-session-id, Accept, Last-Event-ID' )
280- res . setHeader ( 'Access-Control-Expose-Headers' , 'mcp-session-id' )
278+ res . setHeader ( 'Access-Control-Allow-Methods' , 'POST, OPTIONS' )
279+ res . setHeader ( 'Access-Control-Allow-Headers' , 'Content-Type, Accept' )
281280
282281 if ( req . method === 'OPTIONS' ) {
283282 res . writeHead ( 200 )
@@ -313,93 +312,38 @@ if (useHttp) {
313312
314313 if ( url . pathname === '/' ) {
315314 if ( req . method === 'POST' ) {
316- // Validate Accept header as required by MCP spec
317- const acceptHeader = req . headers . accept
318- if ( ! acceptHeader || ( ! acceptHeader . includes ( 'application/json' ) && ! acceptHeader . includes ( 'text/event-stream' ) ) ) {
319- logger . warn ( `Invalid Accept header: ${ acceptHeader } ` )
320- res . writeHead ( 400 , { 'Content-Type' : 'application/json' } )
321- res . end ( JSON . stringify ( {
322- jsonrpc : '2.0' ,
323- error : { code : - 32000 , message : 'Bad Request: Accept header must include application/json or text/event-stream' } ,
324- id : null
325- } ) )
326- return
327- }
328-
329- // Handle JSON-RPC messages
315+ // Handle JSON-RPC messages statelessly
330316 let body = ''
331317 req . on ( 'data' , chunk => ( body += chunk ) )
332318 req . on ( 'end' , async ( ) => {
333319 try {
334320 const jsonData = JSON . parse ( body )
335- const sessionId = req . headers [ 'mcp-session-id' ] as string
336-
337- // Validate session ID format if provided (must contain only visible ASCII characters)
338- if ( sessionId && ! / ^ [ \x21 - \x7E ] + $ / . test ( sessionId ) ) {
339- logger . warn ( `Invalid session ID format: ${ sessionId } ` )
340- res . writeHead ( 400 , { 'Content-Type' : 'application/json' } )
341- res . end ( JSON . stringify ( {
342- jsonrpc : '2.0' ,
343- error : { code : - 32000 , message : 'Bad Request: Session ID must contain only visible ASCII characters' } ,
344- id : jsonData . id || null
345- } ) )
346- return
347- }
348321
349- let transport : StreamableHTTPServerTransport
350-
351- if ( sessionId && transports [ sessionId ] ) {
352- // Reuse existing transport
353- transport = transports [ sessionId ]
354- } else if ( ! sessionId ) {
355- // Create new session (either for initialize request or fallback)
356- const newSessionId = randomUUID ( )
357- const isInit = isInitializeRequest ( jsonData )
358-
359- if ( isInit ) {
360- logger . info ( `Creating new session for initialize request: ${ newSessionId } ` )
361- } else {
362- logger . warn ( `Creating fallback session for non-initialize request: ${ newSessionId } ` )
322+ // If this is an initialize, reset the singleton transport so clients can (re)initialize cleanly
323+ if ( jsonData && jsonData . method === 'initialize' ) {
324+ if ( httpTransport ) {
325+ try { httpTransport . close ( ) } catch { }
363326 }
364-
365- transport = new StreamableHTTPServerTransport ( {
366- sessionIdGenerator : ( ) => newSessionId ,
367- onsessioninitialized : ( id ) => {
368- transports [ id ] = transport
369- logger . info ( `Session initialized: ${ id } ` )
370- // Set session ID in response headers as required by MCP spec
371- res . setHeader ( 'mcp-session-id' , id )
372- }
327+ httpTransport = new StreamableHTTPServerTransport ( {
328+ // Stateless mode: no session management required
329+ sessionIdGenerator : undefined ,
330+ // Return JSON responses to avoid SSE streaming
331+ enableJsonResponse : true
373332 } )
374-
375- transport . onclose = ( ) => {
376- const sid = transport . sessionId
377- if ( sid && transports [ sid ] ) {
378- delete transports [ sid ]
379- logger . info ( `Session closed: ${ sid } ` )
380- }
381- }
382-
383- await server . connect ( transport )
384- await transport . handleRequest ( req , res , jsonData )
385- return
386- } else {
387- // Invalid request - session ID provided but not found
388- logger . error ( `Invalid session ID: ${ sessionId } . Active sessions count: ${ Object . keys ( transports ) . length } ` )
389- res . writeHead ( 400 )
390- res . end ( JSON . stringify ( {
391- jsonrpc : '2.0' ,
392- error : {
393- code : - 32000 ,
394- message : 'Bad Request: Invalid session ID. Please initialize a new session first.'
395- } ,
396- id : jsonData . id || null
397- } ) )
333+ await server . connect ( httpTransport )
334+ await httpTransport . handleRequest ( req , res , jsonData )
398335 return
399336 }
400337
401- // Handle request with existing transport
402- await transport . handleRequest ( req , res , jsonData )
338+ // For non-initialize requests, ensure transport exists (client should have initialized already)
339+ if ( ! httpTransport ) {
340+ httpTransport = new StreamableHTTPServerTransport ( {
341+ sessionIdGenerator : undefined ,
342+ enableJsonResponse : true
343+ } )
344+ await server . connect ( httpTransport )
345+ }
346+ await httpTransport . handleRequest ( req , res , jsonData )
403347 } catch ( error ) {
404348 logger . error ( `Error processing POST request: ${ error } ` )
405349 if ( ! res . headersSent ) {
@@ -412,117 +356,6 @@ if (useHttp) {
412356 }
413357 }
414358 } )
415- } else if ( req . method === 'GET' ) {
416- // Validate Accept header for SSE as required by MCP spec
417- const acceptHeader = req . headers . accept
418- if ( ! acceptHeader || ! acceptHeader . includes ( 'text/event-stream' ) ) {
419- logger . warn ( `GET request without text/event-stream Accept header: ${ acceptHeader } ` )
420- res . writeHead ( 405 , { 'Content-Type' : 'application/json' } )
421- res . end ( JSON . stringify ( {
422- jsonrpc : '2.0' ,
423- error : { code : - 32000 , message : 'Method Not Allowed: GET requires Accept: text/event-stream' } ,
424- id : null
425- } ) )
426- return
427- }
428-
429- // Handle SSE streams
430- const sessionId = req . headers [ 'mcp-session-id' ] as string
431-
432- // Validate session ID format
433- if ( sessionId && ! / ^ [ \x21 - \x7E ] + $ / . test ( sessionId ) ) {
434- logger . warn ( `Invalid session ID format in GET request: ${ sessionId } ` )
435- res . writeHead ( 400 , { 'Content-Type' : 'application/json' } )
436- res . end ( JSON . stringify ( {
437- jsonrpc : '2.0' ,
438- error : { code : - 32000 , message : 'Bad Request: Session ID must contain only visible ASCII characters' } ,
439- id : null
440- } ) )
441- return
442- }
443-
444- if ( ! sessionId || ! transports [ sessionId ] ) {
445- logger . warn ( `SSE request with invalid session ID: ${ sessionId } ` )
446- res . writeHead ( 400 , { 'Content-Type' : 'application/json' } )
447- res . end ( JSON . stringify ( {
448- jsonrpc : '2.0' ,
449- error : { code : - 32000 , message : 'Bad Request: Invalid or missing session ID for SSE stream' } ,
450- id : null
451- } ) )
452- return
453- }
454-
455- // Check for Last-Event-ID header for resumability (optional MCP feature)
456- const lastEventId = req . headers [ 'last-event-id' ] as string
457- if ( lastEventId ) {
458- logger . info ( `SSE resumability requested with Last-Event-ID: ${ lastEventId } ` )
459- // Note: Actual resumability implementation would require message storage
460- // For now, we log the request but don't implement full resumability
461- }
462-
463- logger . info ( `Opening SSE stream for session: ${ sessionId } ` )
464-
465- // Prevent connection timeout and keep it alive
466- req . socket ?. setTimeout ( 0 )
467- req . socket ?. setKeepAlive ( true , 30000 )
468-
469- let streamClosed = false
470-
471- // Handle client disconnection gracefully
472- req . on ( 'close' , ( ) => {
473- streamClosed = true
474- logger . info ( `Client disconnected SSE stream for session: ${ sessionId } ` )
475- } )
476-
477- req . on ( 'aborted' , ( ) => {
478- streamClosed = true
479- logger . info ( `Client aborted SSE stream for session: ${ sessionId } ` )
480- } )
481-
482- // Let the MCP transport handle the SSE stream completely
483- const transport = transports [ sessionId ]
484-
485- try {
486- await transport . handleRequest ( req , res )
487-
488- // If the transport completes without the client disconnecting,
489- // it might have closed the stream prematurely. Keep it open with heartbeat.
490- if ( ! streamClosed && ! res . destroyed ) {
491- logger . info ( `Transport completed, maintaining SSE stream for session: ${ sessionId } ` )
492-
493- // Send periodic heartbeat to keep connection alive
494- const heartbeat = setInterval ( ( ) => {
495- if ( streamClosed || res . destroyed ) {
496- clearInterval ( heartbeat )
497- return
498- }
499-
500- try {
501- res . write ( ': heartbeat\n\n' )
502- } catch ( error ) {
503- logger . error ( error , `Heartbeat error for session ${ sessionId } :` )
504- clearInterval ( heartbeat )
505- }
506- } , 30000 )
507-
508- // Clean up heartbeat when connection closes
509- req . on ( 'close' , ( ) => clearInterval ( heartbeat ) )
510- res . on ( 'close' , ( ) => clearInterval ( heartbeat ) )
511- }
512- } catch ( error ) {
513- logger . error ( error , `SSE transport error for session ${ sessionId } :` )
514- }
515- } else if ( req . method === 'DELETE' ) {
516- // Handle session termination
517- const sessionId = req . headers [ 'mcp-session-id' ] as string
518- if ( ! sessionId || ! transports [ sessionId ] ) {
519- res . writeHead ( 400 )
520- res . end ( 'Invalid or missing session ID' )
521- return
522- }
523-
524- const transport = transports [ sessionId ]
525- await transport . handleRequest ( req , res )
526359 } else {
527360 res . writeHead ( 405 )
528361 res . end ( 'Method not allowed' )
0 commit comments