Skip to content

Commit 6b27264

Browse files
committed
Refactor HTTP server to handle requests statelessly, removing session management and simplifying JSON-RPC handling.
1 parent 7edb88f commit 6b27264

File tree

1 file changed

+27
-194
lines changed

1 file changed

+27
-194
lines changed

index.ts

Lines changed: 27 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import { join } from 'path'
99
import { readFileSync } from 'fs'
1010
import { tmpdir } from 'os'
1111
import { createServer } from 'http'
12-
import { randomUUID } from 'crypto'
13-
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
1412

1513
const __dirname = import.meta.dirname
1614

@@ -67,8 +65,7 @@ async function getApiKeyInteractively (): Promise<string> {
6765
// Initialize API key
6866
let 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
7471
const 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

Comments
 (0)