@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
145 lines (144 loc) • 5.06 kB
JavaScript
/**
* 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();
});