Skip to main content
Glama
session-transport-manager.ts7.19 kB
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { BaseHttpStreamServer, type SessionInfo } from '../base/base-httpstream-server'; import { logError } from '../../utils/logger'; /** * Service responsible for managing session transports * Handles transport lifecycle, cleanup, and session management */ export class SessionTransportManager extends BaseHttpStreamServer { private sessionMetadata = new Map<string, { createdAt: Date; lastActivity: Date }>(); /** * Create a new session transport */ createTransport( sessionIdGenerator: () => string, onSessionInitialized?: (sessionId: string) => void, ): StreamableHTTPServerTransport { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator, enableJsonResponse: true, onsessioninitialized: (newSessionId: string) => { this.setTransport(newSessionId, transport); this.sessionMetadata.set(newSessionId, { createdAt: new Date(), lastActivity: new Date(), }); this.logger.debug({ sessionId: newSessionId }, 'New session initialized'); if (onSessionInitialized) { onSessionInitialized(newSessionId); } }, }); // Set up cleanup when transport closes transport.onclose = () => { if (transport.sessionId) { this.cleanupSession(transport.sessionId); } }; return transport; } /** * Get session information with metadata */ getSessionInfoWithMetadata(sessionId: string): | (SessionInfo & { createdAt: Date; lastActivity: Date; duration: number; }) | undefined { const transport = this.getTransport(sessionId); const metadata = this.sessionMetadata.get(sessionId); if (!transport || !metadata) { return undefined; } return { sessionId, transport, createdAt: metadata.createdAt, lastActivity: metadata.lastActivity, duration: Date.now() - metadata.createdAt.getTime(), }; } /** * Update session activity timestamp */ updateSessionActivity(sessionId: string): void { const metadata = this.sessionMetadata.get(sessionId); if (metadata) { metadata.lastActivity = new Date(); } } /** * Get all active sessions with metadata */ getAllSessionsInfo(): Array< SessionInfo & { createdAt: Date; lastActivity: Date; duration: number; } > { const sessions: Array< SessionInfo & { createdAt: Date; lastActivity: Date; duration: number; } > = []; for (const sessionId of this.getActiveSessionIds()) { const sessionInfo = this.getSessionInfoWithMetadata(sessionId); if (sessionInfo) { sessions.push(sessionInfo); } } return sessions; } /** * Clean up a specific session */ async cleanupSession(sessionId: string): Promise<void> { const transport = this.getTransport(sessionId); if (transport) { try { await transport.close(); this.logger.debug({ sessionId }, 'Transport closed'); } catch (error) { logError(this.logger, error as Error, { sessionId, operation: 'transport-close' }); } } this.removeTransport(sessionId); this.sessionMetadata.delete(sessionId); this.logger.debug({ sessionId }, 'Session cleaned up'); } /** * Clean up all active sessions */ async cleanupAllSessions(): Promise<void> { this.logger.info('Cleaning up all active sessions'); const sessionIds = this.getActiveSessionIds(); const cleanupPromises = sessionIds.map((sessionId) => this.cleanupSession(sessionId)); try { await Promise.allSettled(cleanupPromises); this.logger.info({ sessionCount: sessionIds.length }, 'All sessions cleaned up'); } catch (error) { logError(this.logger, error as Error, { operation: 'cleanup-all-sessions' }); } } /** * Clean up inactive sessions based on timeout */ async cleanupInactiveSessions(inactivityTimeoutMs: number = 30 * 60 * 1000): Promise<void> { const now = Date.now(); const inactiveSessions: string[] = []; for (const [sessionId, metadata] of this.sessionMetadata.entries()) { const timeSinceLastActivity = now - metadata.lastActivity.getTime(); if (timeSinceLastActivity > inactivityTimeoutMs) { inactiveSessions.push(sessionId); } } if (inactiveSessions.length > 0) { this.logger.info( { inactiveSessionCount: inactiveSessions.length, inactivityTimeoutMs }, 'Cleaning up inactive sessions', ); const cleanupPromises = inactiveSessions.map((sessionId) => this.cleanupSession(sessionId)); await Promise.allSettled(cleanupPromises); } } /** * Get session statistics */ getSessionStatistics(): { totalSessions: number; averageDuration: number; oldestSession: Date | null; newestSession: Date | null; } { const sessions = this.getAllSessionsInfo(); if (sessions.length === 0) { return { totalSessions: 0, averageDuration: 0, oldestSession: null, newestSession: null, }; } const totalDuration = sessions.reduce((sum, session) => sum + session.duration, 0); const averageDuration = totalDuration / sessions.length; const sortedByCreation = sessions.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); const oldestSession = sortedByCreation[0]?.createdAt || null; const newestSession = sortedByCreation[sortedByCreation.length - 1]?.createdAt || null; return { totalSessions: sessions.length, averageDuration, oldestSession, newestSession, }; } /** * Check if session exists and is active */ isSessionActive(sessionId: string): boolean { return this.getTransport(sessionId) !== undefined; } /** * Force close a session */ async forceCloseSession(sessionId: string): Promise<boolean> { const transport = this.getTransport(sessionId); if (!transport) { return false; } try { await this.cleanupSession(sessionId); this.logger.info({ sessionId }, 'Session force closed'); return true; } catch (error) { logError(this.logger, error as Error, { sessionId, operation: 'force-close-session' }); return false; } } /** * Start periodic cleanup of inactive sessions */ startPeriodicCleanup(intervalMs: number = 10 * 60 * 1000): NodeJS.Timeout { this.logger.info({ intervalMs }, 'Starting periodic session cleanup'); return setInterval(async () => { try { await this.cleanupInactiveSessions(); } catch (error) { logError(this.logger, error as Error, { operation: 'periodic-cleanup' }); } }, intervalMs); } // Implement abstract methods from base class async start(): Promise<void> { this.logger.info('Session transport manager initialized'); } async stop(): Promise<void> { await this.cleanupAllSessions(); this.logger.info('Session transport manager stopped'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/KuzuMem-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server