UNPKG

@the_cfdude/productboard-mcp

Version:

Model Context Protocol server for Productboard REST API with dynamic tool loading

145 lines (144 loc) 5.06 kB
/** * Session management for ProductBoard MCP Server * Provides isolated state per connection to prevent conflicts between multiple clients */ import { randomBytes } from 'crypto'; import { debugLog } from './utils/debug-logger.js'; class ProductBoardSessionManager { sessions = new Map(); cleanupInterval = null; sessionTimeout = 300000; // 5 minutes cleanupIntervalMs = 60000; // 1 minute constructor() { this.startCleanupTimer(); debugLog('session-manager', 'SessionManager initialized', { sessionTimeout: this.sessionTimeout, cleanupInterval: this.cleanupIntervalMs, }); } createSession(sessionId) { const id = sessionId || this.generateSessionId(); const session = { sessionId: id, createdAt: new Date(), lastActivity: new Date(), apiInstances: new Map(), configCache: new Map(), requestCount: 0, activeRequests: new Set(), }; this.sessions.set(id, session); debugLog('session-manager', 'Session created', { sessionId: id, totalSessions: this.sessions.size, }); return session; } getSession(sessionId) { const session = this.sessions.get(sessionId); if (session) { this.updateActivity(sessionId); } return session; } updateActivity(sessionId) { const session = this.sessions.get(sessionId); if (session) { session.lastActivity = new Date(); } } removeSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { return; } // Clean up any remaining active requests if (session.activeRequests.size > 0) { debugLog('session-manager', 'Cleaning up active requests during session removal', { sessionId, activeRequestCount: session.activeRequests.size, }); session.activeRequests.clear(); } // Clear cached instances and configurations session.apiInstances.clear(); session.configCache.clear(); this.sessions.delete(sessionId); debugLog('session-manager', 'Session removed', { sessionId, remainingSessions: this.sessions.size, }); } cleanupInactiveSessions() { const now = new Date(); const expiredSessions = []; for (const [sessionId, session] of this.sessions.entries()) { const timeSinceActivity = now.getTime() - session.lastActivity.getTime(); if (timeSinceActivity > this.sessionTimeout) { expiredSessions.push(sessionId); } } if (expiredSessions.length > 0) { debugLog('session-manager', 'Cleaning up expired sessions', { expiredCount: expiredSessions.length, expiredSessions, }); for (const sessionId of expiredSessions) { this.removeSession(sessionId); } } } getActiveSessionCount() { return this.sessions.size; } getSessionStats() { const sessions = Array.from(this.sessions.values()); const totalRequests = sessions.reduce((sum, session) => sum + session.requestCount, 0); return { totalSessions: sessions.length, activeSessions: sessions.length, totalRequests, averageRequestsPerSession: sessions.length > 0 ? totalRequests / sessions.length : 0, oldestSession: sessions.length > 0 ? new Date(Math.min(...sessions.map(s => s.createdAt.getTime()))) : undefined, }; } generateSessionId() { const timestamp = Date.now().toString(36); const randomPart = randomBytes(8).toString('hex'); return `pb-${timestamp}-${randomPart}`; } startCleanupTimer() { this.cleanupInterval = setInterval(() => { this.cleanupInactiveSessions(); }, this.cleanupIntervalMs); // Ensure cleanup timer doesn't keep the process alive if (this.cleanupInterval.unref) { this.cleanupInterval.unref(); } } shutdown() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } // Clean up all sessions const sessionIds = Array.from(this.sessions.keys()); for (const sessionId of sessionIds) { this.removeSession(sessionId); } debugLog('session-manager', 'SessionManager shutdown completed', { cleanedUpSessions: sessionIds.length, }); } } // Global session manager instance export const sessionManager = new ProductBoardSessionManager(); // Graceful shutdown handling process.on('SIGINT', () => { sessionManager.shutdown(); }); process.on('SIGTERM', () => { sessionManager.shutdown(); });