Skip to main content
Glama

MCP Bridge Server

session.ts9.84 kB
import { createHash } from 'crypto'; import { join } from 'path'; import { PersistentStorage } from '../storage/persistentStorage.js'; import { UserSession, IdentityError, IdentityErrorType, ValidationRules } from './types.js'; import { UserManager } from './user.js'; import { ClientManager } from './client.js'; /** * Session manager options */ interface SessionManagerOptions { storage: PersistentStorage; userManager: UserManager; clientManager: ClientManager; validationRules?: ValidationRules; sessionDataPath?: string; sessionTimeout?: number; maxSessionsPerUser?: number; } /** * Session manager */ export class SessionManager { private storage: PersistentStorage; private userManager: UserManager; private clientManager: ClientManager; private validationRules: ValidationRules; private sessionDataPath: string; private sessionTimeout: number; private maxSessionsPerUser: number; private cleanupInterval?: NodeJS.Timeout; constructor(options: SessionManagerOptions) { this.storage = options.storage; this.userManager = options.userManager; this.clientManager = options.clientManager; this.validationRules = options.validationRules || {}; this.sessionDataPath = options.sessionDataPath || 'sessions'; this.sessionTimeout = options.sessionTimeout || 30 * 60 * 1000; // 30 minutes this.maxSessionsPerUser = options.maxSessionsPerUser || 5; } /** * Start session cleanup interval */ public startCleanup(interval: number = 5 * 60 * 1000): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.cleanupInterval = setInterval(() => this.cleanupExpiredSessions(), interval); } /** * Stop session cleanup interval */ public stopCleanup(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } } /** * Create new session */ public async createSession( userId: string, machineId: string, clientId: string ): Promise<UserSession> { try { // Verify user exists and has access to machine const user = await this.userManager.getUser(userId); if (!user.machineIds.includes(machineId)) { throw new IdentityError( IdentityErrorType.SESSION_INVALID, 'User does not have access to this machine' ); } // Verify client exists and belongs to user const client = await this.clientManager.getClient(clientId); if (client.components.userId !== userId) { throw new IdentityError( IdentityErrorType.SESSION_INVALID, 'Client does not belong to user' ); } // Check session limit const sessions = await this.getSessionsByUser(userId); if (sessions.length >= this.maxSessionsPerUser) { throw new IdentityError( IdentityErrorType.SESSION_INVALID, 'Maximum number of sessions reached' ); } // Generate session ID const id = await this.generateSessionId(userId, machineId, clientId); // Create session const session: UserSession = { id, userId, machineId, created: new Date(), lastActive: new Date(), expiresAt: new Date(Date.now() + this.sessionTimeout), clients: [clientId] }; // Validate session if (!this.validateSession(session)) { throw new IdentityError( IdentityErrorType.SESSION_INVALID, 'Invalid session data' ); } // Store session await this.storage.write( this.getSessionPath(id), session ); // Update client await this.clientManager.updateClient(clientId, { sessions: [...client.sessions, id] }); return session; } catch (error) { if (error instanceof IdentityError) { throw error; } throw new IdentityError( IdentityErrorType.SESSION_INVALID, 'Failed to create session', error ); } } /** * Get session by ID */ public async getSession(sessionId: string): Promise<UserSession> { try { const session = await this.storage.read<UserSession>( this.getSessionPath(sessionId) ); if (!this.validateSession(session)) { throw new IdentityError( IdentityErrorType.SESSION_INVALID, 'Invalid session data' ); } // Check expiration if (new Date() > new Date(session.expiresAt)) { throw new IdentityError( IdentityErrorType.SESSION_EXPIRED, 'Session expired' ); } return session; } catch (error) { if (error instanceof IdentityError) { throw error; } throw new IdentityError( IdentityErrorType.SESSION_NOT_FOUND, 'Session not found', error ); } } /** * Get sessions by user ID */ public async getSessionsByUser(userId: string): Promise<UserSession[]> { try { // Get user to verify it exists await this.userManager.getUser(userId); // List all session files const files = await this.storage.read<string[]>(this.sessionDataPath); // Load and filter sessions const sessions = await Promise.all( files .filter(file => file.endsWith('.json')) .map(file => this.storage.read<UserSession>(join(this.sessionDataPath, file))) ); return sessions .filter(session => session.userId === userId) .filter(session => new Date() <= new Date(session.expiresAt)); } catch (error) { if (error instanceof IdentityError) { throw error; } throw new IdentityError( IdentityErrorType.SESSION_NOT_FOUND, 'Failed to get sessions', error ); } } /** * Update session activity */ public async updateActivity(sessionId: string): Promise<UserSession> { try { const session = await this.getSession(sessionId); // Update timestamps const updated: UserSession = { ...session, lastActive: new Date(), expiresAt: new Date(Date.now() + this.sessionTimeout) }; // Store updated session await this.storage.write( this.getSessionPath(sessionId), updated ); return updated; } catch (error) { if (error instanceof IdentityError) { throw error; } throw new IdentityError( IdentityErrorType.SESSION_INVALID, 'Failed to update session', error ); } } /** * End session */ public async endSession(sessionId: string): Promise<void> { try { const session = await this.getSession(sessionId); // Remove session from clients await Promise.all( session.clients.map(async clientId => { const client = await this.clientManager.getClient(clientId); await this.clientManager.updateClient(clientId, { sessions: client.sessions.filter(id => id !== sessionId) }); }) ); // Delete session await this.storage.delete(this.getSessionPath(sessionId)); } catch (error) { if (error instanceof IdentityError) { throw error; } throw new IdentityError( IdentityErrorType.SESSION_NOT_FOUND, 'Failed to end session', error ); } } /** * Clean up expired sessions */ private async cleanupExpiredSessions(): Promise<void> { try { // List all session files const files = await this.storage.read<string[]>(this.sessionDataPath); // Load all sessions const sessions = await Promise.all( files .filter(file => file.endsWith('.json')) .map(file => this.storage.read<UserSession>(join(this.sessionDataPath, file))) ); // End expired sessions await Promise.all( sessions .filter(session => new Date() > new Date(session.expiresAt)) .map(session => this.endSession(session.id)) ); } catch (error) { // Log cleanup errors but don't throw console.error('Session cleanup failed:', error); } } /** * Generate session ID */ private async generateSessionId( userId: string, machineId: string, clientId: string ): Promise<string> { // Create hash from components and timestamp const hash = createHash('sha256') .update(`${userId}:${machineId}:${clientId}:${Date.now()}`) .digest('hex'); // Use first 32 characters as session ID return hash.slice(0, 32); } /** * Get session file path */ private getSessionPath(sessionId: string): string { return join(this.sessionDataPath, `${sessionId}.json`); } /** * Validate session data */ private validateSession(session: UserSession): boolean { // Basic structure validation if (!session || typeof session !== 'object') return false; if (!session.id || typeof session.id !== 'string') return false; if (!session.userId || typeof session.userId !== 'string') return false; if (!session.machineId || typeof session.machineId !== 'string') return false; if (!(session.created instanceof Date)) return false; if (!(session.lastActive instanceof Date)) return false; if (!(session.expiresAt instanceof Date)) return false; if (!Array.isArray(session.clients)) return false; // ID format validation if (this.validationRules.sessionId?.pattern) { if (!this.validationRules.sessionId.pattern.test(session.id)) { return false; } } // Client validation for (const clientId of session.clients) { if (typeof clientId !== 'string') return false; } return true; } }

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/glassBead-tc/SubspaceDomain'

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