Skip to content

Commit 821e9fd

Browse files
committed
Enhance session management and cleanup in HTTP server
- Introduced a session interface to manage transport and server pairs for each client, including tracking last activity. - Implemented a session reaping mechanism to automatically destroy idle sessions after 30 minutes. - Updated session handling in request processing to ensure proper resource cleanup and activity tracking. These changes improve the reliability and efficiency of session management in the HTTP server.
1 parent 37af573 commit 821e9fd

1 file changed

Lines changed: 71 additions & 19 deletions

File tree

index.ts

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)