mcp-web-ui
Version:
Ultra-lightweight vanilla JavaScript framework for MCP servers - Zero dependencies, perfect security, 2-3KB bundle size
197 lines • 6.91 kB
JavaScript
import { v4 as uuidv4 } from 'uuid';
/**
* Manages UI sessions with automatic cleanup and lifecycle management
*/
export class SessionManager {
sessions = new Map();
usedPorts = new Set();
portRange;
baseUrl;
userSessionLimits = new Map();
constructor(sessionTimeoutMs = 30 * 60 * 1000, // 30 minutes default
portRange = [3000, 65535], baseUrl = 'localhost') {
this.portRange = portRange;
this.baseUrl = baseUrl;
// Note: Cleanup is now handled by MCPWebUI to properly stop UI servers
}
/**
* Create a new session for a user
* Automatically terminates any existing session for the same user
*/
createSession(userId) {
// Rate limiting: max 5 sessions per user per hour
const sessionTime = new Date();
const userLimits = this.userSessionLimits.get(userId);
if (userLimits) {
const hourAgo = new Date(sessionTime.getTime() - 60 * 60 * 1000);
if (userLimits.lastCreated > hourAgo && userLimits.count >= 5) {
throw new Error('Session creation rate limit exceeded. Maximum 5 sessions per hour.');
}
if (userLimits.lastCreated > hourAgo) {
userLimits.count++;
}
else {
userLimits.count = 1;
}
userLimits.lastCreated = sessionTime;
}
else {
this.userSessionLimits.set(userId, { count: 1, lastCreated: sessionTime });
}
// Check for and terminate any existing session for this user
const existingSession = this.getSessionByUserId(userId);
if (existingSession) {
this.log('INFO', `Terminating existing session ${existingSession.id} for user ${userId} before creating new one`);
this.terminateSession(existingSession.id);
}
const sessionId = uuidv4();
const token = uuidv4();
const port = this.allocatePort();
const now = new Date();
const expiresAt = new Date(now.getTime() + 30 * 60 * 1000); // 30 minutes
const session = {
id: sessionId,
token,
userId,
url: `http://${this.baseUrl}:${port}?token=${token}`,
port,
startTime: now,
lastActivity: now,
expiresAt,
isActive: true
};
this.sessions.set(sessionId, session);
this.log('INFO', `Created session ${sessionId} for user ${userId} on port ${port}`);
return session;
}
/**
* Get session by token (for authentication)
* @param token Session token
* @param updateActivity Whether to update lastActivity (default: false for polling, true for user actions)
*/
getSessionByToken(token, updateActivity = false) {
for (const session of this.sessions.values()) {
if (session.token === token && session.isActive) {
// Only update last activity for actual user interactions, not polling
if (updateActivity) {
session.lastActivity = new Date();
}
return session;
}
}
return null;
}
/**
* Get session by ID
*/
getSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session && session.isActive) {
session.lastActivity = new Date();
return session;
}
return null;
}
/**
* Get active session by user ID
*/
getSessionByUserId(userId) {
for (const session of this.sessions.values()) {
if (session.userId === userId && session.isActive) {
session.lastActivity = new Date();
return session;
}
}
return null;
}
/**
* Extend session timeout
*/
extendSession(sessionId, additionalMinutes = 30) {
const session = this.sessions.get(sessionId);
if (session && session.isActive) {
session.expiresAt = new Date(session.expiresAt.getTime() + additionalMinutes * 60 * 1000);
session.lastActivity = new Date();
this.log('INFO', `Extended session ${sessionId} by ${additionalMinutes} minutes`);
return true;
}
return false;
}
/**
* Terminate a specific session
*/
terminateSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.isActive = false;
this.usedPorts.delete(session.port);
this.sessions.delete(sessionId);
this.log('INFO', `Terminated session ${sessionId}, freed port ${session.port}`);
return true;
}
return false;
}
/**
* Get all active sessions for debugging
*/
getActiveSessions() {
return Array.from(this.sessions.values()).filter(s => s.isActive);
}
/**
* Get session stats
*/
getStats() {
const active = this.getActiveSessions();
const userSessions = new Map();
// Count sessions per user
for (const session of active) {
userSessions.set(session.userId, (userSessions.get(session.userId) || 0) + 1);
}
return {
totalSessions: active.length,
usedPorts: Array.from(this.usedPorts).sort(),
oldestSession: active.length > 0 ?
Math.min(...active.map(s => s.startTime.getTime())) : null,
nextExpiry: active.length > 0 ?
Math.min(...active.map(s => s.expiresAt.getTime())) : null,
sessionsPerUser: Object.fromEntries(userSessions),
uniqueUsers: userSessions.size
};
}
/**
* Shutdown session manager and cleanup all sessions
*/
shutdown() {
// Cleanup all active sessions
for (const sessionId of this.sessions.keys()) {
this.terminateSession(sessionId);
}
this.usedPorts.clear();
this.log('INFO', 'SessionManager shutdown complete');
}
/**
* Allocate a random available port
*/
allocatePort() {
const [minPort, maxPort] = this.portRange;
let attempts = 0;
const maxAttempts = 100;
while (attempts < maxAttempts) {
const port = Math.floor(Math.random() * (maxPort - minPort + 1)) + minPort;
if (!this.usedPorts.has(port)) {
this.usedPorts.add(port);
return port;
}
attempts++;
}
throw new Error(`Unable to allocate port after ${maxAttempts} attempts`);
}
/**
* Simple logging utility
*/
log(level, message) {
const timestamp = new Date().toISOString();
console.error(`[${timestamp}][${level}][SessionManager] ${message}`);
}
}
//# sourceMappingURL=SessionManager.js.map