Skip to main content
Glama
bradcstevens

Copilot Studio Agent Direct Line MCP Server

by bradcstevens
memory-session-store.ts6.48 kB
/** * In-memory session storage with TTL cleanup * Suitable for development and single-instance deployments */ import type { ISessionStore, SessionData } from '../../types/session.js'; /** * Memory-based session store using Map with TTL cleanup */ export class MemorySessionStore implements ISessionStore { private sessions: Map<string, SessionData> = new Map(); private userSessionIndex: Map<string, Set<string>> = new Map(); private cleanupTimer?: NodeJS.Timeout; private cleanupInterval: number; /** * Create a new MemorySessionStore * @param cleanupInterval - Cleanup interval in milliseconds (default: 60000 = 1 minute) */ constructor(cleanupInterval: number = 60000) { this.cleanupInterval = cleanupInterval; this.startCleanup(); } /** * Create a new session * @param sessionData - Session data to store * @returns Session ID */ async create(sessionData: SessionData): Promise<string> { this.sessions.set(sessionData.sessionId, sessionData); // Update user session index const userId = sessionData.userContext.userId; if (!this.userSessionIndex.has(userId)) { this.userSessionIndex.set(userId, new Set()); } this.userSessionIndex.get(userId)!.add(sessionData.sessionId); return sessionData.sessionId; } /** * Get session by ID * @param sessionId - Session ID * @returns Session data or null if not found */ async get(sessionId: string): Promise<SessionData | null> { const session = this.sessions.get(sessionId); if (!session) { return null; } // Check if expired if (Date.now() >= session.expiresAt) { await this.delete(sessionId); return null; } return session; } /** * Update existing session * @param sessionId - Session ID * @param updates - Partial session data to update */ async update(sessionId: string, updates: Partial<SessionData>): Promise<void> { const existing = this.sessions.get(sessionId); if (!existing) { throw new Error(`Session ${sessionId} not found`); } // Merge updates const updated: SessionData = { ...existing, ...updates, // Ensure nested objects are properly merged userContext: updates.userContext ? { ...existing.userContext, ...updates.userContext } : existing.userContext, tokenMetadata: updates.tokenMetadata ? { ...existing.tokenMetadata, ...updates.tokenMetadata } : existing.tokenMetadata, security: updates.security ? { ...existing.security, ...updates.security } : existing.security, }; this.sessions.set(sessionId, updated); // Update user index if userId changed if (updates.userContext?.userId && updates.userContext.userId !== existing.userContext.userId) { // Remove from old user index const oldUserId = existing.userContext.userId; const oldUserSessions = this.userSessionIndex.get(oldUserId); if (oldUserSessions) { oldUserSessions.delete(sessionId); if (oldUserSessions.size === 0) { this.userSessionIndex.delete(oldUserId); } } // Add to new user index const newUserId = updates.userContext.userId; if (!this.userSessionIndex.has(newUserId)) { this.userSessionIndex.set(newUserId, new Set()); } this.userSessionIndex.get(newUserId)!.add(sessionId); } } /** * Delete session by ID * @param sessionId - Session ID */ async delete(sessionId: string): Promise<void> { const session = this.sessions.get(sessionId); if (session) { // Remove from user index const userId = session.userContext.userId; const userSessions = this.userSessionIndex.get(userId); if (userSessions) { userSessions.delete(sessionId); if (userSessions.size === 0) { this.userSessionIndex.delete(userId); } } } this.sessions.delete(sessionId); } /** * Clean up expired sessions * @returns Number of sessions cleaned up */ async cleanup(): Promise<number> { let cleanedCount = 0; const now = Date.now(); for (const [sessionId, session] of this.sessions.entries()) { if (now >= session.expiresAt) { await this.delete(sessionId); cleanedCount++; } } return cleanedCount; } /** * Get all active sessions for a user * @param userId - User ID * @returns Array of session data */ async getUserSessions(userId: string): Promise<SessionData[]> { const sessionIds = this.userSessionIndex.get(userId); if (!sessionIds || sessionIds.size === 0) { return []; } const sessions: SessionData[] = []; const now = Date.now(); for (const sessionId of sessionIds) { const session = this.sessions.get(sessionId); if (session && now < session.expiresAt) { sessions.push(session); } } return sessions; } /** * Get session count * @returns Total number of active sessions */ async getSessionCount(): Promise<number> { return this.sessions.size; } /** * Start automatic cleanup timer */ private startCleanup(): void { this.cleanupTimer = setInterval(async () => { try { const cleaned = await this.cleanup(); if (cleaned > 0) { console.error(`[MemorySessionStore] Cleaned up ${cleaned} expired sessions`); } } catch (error) { console.error('[MemorySessionStore] Cleanup error:', error); } }, this.cleanupInterval); // Don't keep process alive just for cleanup this.cleanupTimer.unref(); } /** * Stop automatic cleanup timer */ stopCleanup(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; } } /** * Get memory usage statistics * @returns Memory usage info */ getMemoryStats(): { sessionCount: number; userCount: number; estimatedMemoryBytes: number; } { const sessionCount = this.sessions.size; const userCount = this.userSessionIndex.size; // Rough estimation: each session ~2KB const estimatedMemoryBytes = sessionCount * 2048; return { sessionCount, userCount, estimatedMemoryBytes, }; } /** * Clear all sessions (for testing) */ async clearAll(): Promise<void> { this.sessions.clear(); this.userSessionIndex.clear(); } }

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/bradcstevens/copilot-studio-agent-direct-line-mcp'

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