@@ -252,7 +252,31 @@ if (useHttp) {
252252 // Per-session transports and servers: each client gets its own transport+server pair.
253253 // Both must persist for the session lifetime; storing the server prevents GC from
254254 // reclaiming it before subsequent RPC calls.
255- const sessions: Record<string, { transport: StreamableHTTPServerTransport; server: McpServer }> = {}
255+ interface Session { transport: StreamableHTTPServerTransport; server: McpServer; lastActivity: number }
256+ const sessions = new Map<string, Session>()
257+
258+ /** Tear down a session by id, closing transport and server. Safe to call multiple times. */
259+ function destroySession (id: string): void {
260+ const s = sessions.get(id)
261+ if (!s) return
262+ sessions.delete(id)
263+ try { s.transport.close() } catch {}
264+ s.server.close().catch(() => {})
265+ logger.info(` Session ${id } destroyed `)
266+ }
267+
268+ // Reap idle sessions every 60 s. Sessions unused for 30 min are removed.
269+ const SESSION_TTL_MS = 30 * 60 * 1000
270+ const reapInterval = setInterval(() => {
271+ const now = Date.now()
272+ for (const [id, session] of sessions.entries()) {
273+ if (now - session.lastActivity > SESSION_TTL_MS) {
274+ logger.info(` Reaping idle session ${id } `)
275+ destroySession(id)
276+ }
277+ }
278+ }, 60_000)
279+ reapInterval.unref() // don't keep the process alive just for the reaper
256280
257281 const httpServer = createServer(async (req, res) => {
258282 // Parse URL first to check for health endpoint
@@ -370,30 +394,27 @@ if (useHttp) {
370394 try {
371395 const jsonData = JSON.parse(body)
372396 const sessionId = (req.headers['mcp-session-id'] as string) || undefined
373- const session = sessionId ? sessions[ sessionId] : undefined
397+ const session = sessionId ? sessions.get( sessionId) : undefined
374398 let transport = session?.transport
375399
376400 if (!transport && isInitializeRequest(jsonData)) {
377401 const clientInfo = jsonData.params?.clientInfo
378402 logger.info(` Client connected : ${clientInfo ?. name || 'unknown' } v$ { clientInfo ?. version || 'unknown' } from ${origin || host } `)
379403
380404 const server = createConfiguredServer()
381- transport = new StreamableHTTPServerTransport({
405+ const newTransport = new StreamableHTTPServerTransport({
382406 enableJsonResponse: true,
383407 sessionIdGenerator: () => randomUUID(),
384- onsessioninitialized: (id) => { sessions[id] = { transport: transport!, server } },
385- onsessionclosed: (id) => {
386- const s = sessions[id]
387- if (s) { s.server.close().catch(() => {}); delete sessions[id] }
388- }
408+ onsessioninitialized: (id) => {
409+ sessions.set(id, { transport: newTransport, server, lastActivity: Date.now() })
410+ },
411+ onsessionclosed: (id) => { destroySession(id) }
389412 })
390- transport.onclose = () => {
391- const id = transport?.sessionId
392- if (id) {
393- const s = sessions[id]
394- if (s) { s.server.close().catch(() => {}); delete sessions[id] }
395- }
413+ newTransport.onclose = () => {
414+ const id = newTransport.sessionId
415+ if (id) destroySession(id)
396416 }
417+ transport = newTransport
397418 await server.connect(transport as Transport)
398419 }
399420
@@ -407,6 +428,12 @@ if (useHttp) {
407428 return
408429 }
409430
431+ // Touch session activity for TTL tracking
432+ if (sessionId) {
433+ const activeSession = sessions.get(sessionId)
434+ if (activeSession) activeSession.lastActivity = Date.now()
435+ }
436+
410437 await transport.handleRequest(req, res, jsonData)
411438 } catch (error) {
412439 logger.error(` Error processing POST request : ${error } `)
@@ -422,8 +449,8 @@ if (useHttp) {
422449 })
423450 } else if (req.method === 'GET') {
424451 const sessionId = (req.headers['mcp-session-id'] as string) || undefined
425- const transport = sessionId ? sessions[sessionId]?.transport : undefined
426- if (!transport ) {
452+ const session = sessionId ? sessions.get(sessionId) : undefined
453+ if (!session ) {
427454 res.writeHead(404, { 'Content-Type': 'application/json' })
428455 res.end(JSON.stringify({
429456 jsonrpc: '2.0',
@@ -432,10 +459,23 @@ if (useHttp) {
432459 }))
433460 return
434461 }
435- await transport.handleRequest(req, res)
462+ try {
463+ session.lastActivity = Date.now()
464+ await session.transport.handleRequest(req, res)
465+ } catch (error) {
466+ logger.error(` Error processing GET request : ${error } `)
467+ if (!res.headersSent) {
468+ res.writeHead(500)
469+ res.end(JSON.stringify({
470+ jsonrpc: '2.0',
471+ error: { code: -32603, message: 'Internal server error' },
472+ id: null
473+ }))
474+ }
475+ }
436476 } else if (req.method === 'DELETE') {
437477 const sessionId = (req.headers['mcp-session-id'] as string) || undefined
438- const transport = sessionId ? sessions[ sessionId] ?.transport : undefined
478+ const transport = sessionId ? sessions.get( sessionId) ?.transport : undefined
439479 if (!transport) {
440480 res.writeHead(404, { 'Content-Type': 'application/json' })
441481 res.end(JSON.stringify({
@@ -445,7 +485,19 @@ if (useHttp) {
445485 }))
446486 return
447487 }
448- await transport.handleRequest(req, res)
488+ try {
489+ await transport.handleRequest(req, res)
490+ } catch (error) {
491+ logger.error(` Error processing DELETE request : ${error } `)
492+ if (!res.headersSent) {
493+ res.writeHead(500)
494+ res.end(JSON.stringify({
495+ jsonrpc: '2.0',
496+ error: { code: -32603, message: 'Internal server error' },
497+ id: null
498+ }))
499+ }
500+ }
449501 } else {
450502 res.writeHead(405)
451503 res.end('Method not allowed')
0 commit comments