UNPKG

@gongrzhe/server-fireflies-mcp

Version:

MCP server for using the Fireflies.ai API with session-aware multi-user support

232 lines (231 loc) โ€ข 9.81 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { randomUUID } from 'node:crypto'; import { AsyncLocalStorage } from 'node:async_hooks'; /** * Custom transport that preserves AsyncLocalStorage context throughout request/response cycle */ class SessionAwareStreamableTransport extends StreamableHTTPServerTransport { sessionId; authSessionId; requestContextStorage; constructor(sessionId, authSessionId, requestContextStorage) { super({ sessionIdGenerator: () => sessionId, onsessioninitialized: (id) => { console.log(`๐Ÿ“ก Fireflies Transport session initialized: ${id} (Auth: ${authSessionId})`); } }); this.sessionId = sessionId; this.authSessionId = authSessionId; this.requestContextStorage = requestContextStorage; } /** * Override handleRequest to preserve context during request processing */ async handleRequest(req, res, requestBody) { const currentContext = this.requestContextStorage.getStore(); if (!currentContext) { console.warn(`โš ๏ธ No context available for handleRequest in Fireflies session ${this.sessionId}`); return super.handleRequest(req, res, requestBody); } console.log(`๐Ÿ“ฅ Handling Fireflies request for session ${this.sessionId}, request ${currentContext.requestId}`); return this.requestContextStorage.run(currentContext, async () => { await super.handleRequest(req, res, requestBody); }); } /** * Override send to ensure response routing with preserved context */ async send(message) { const currentContext = this.requestContextStorage.getStore(); if (!currentContext) { console.warn(`โš ๏ธ No context available when sending response for Fireflies session ${this.sessionId}`); return super.send(message); } console.log(`๐Ÿ“ค Sending Fireflies response for session ${this.sessionId}, request ${currentContext.requestId}`); return this.requestContextStorage.run(currentContext, async () => { await super.send(message); }); } } /** * Manages session-aware transport instances with complete isolation for Fireflies MCP Server */ export class SessionAwareTransportManager { sessions = new Map(); requestContextStorage = new AsyncLocalStorage(); /** * Get or create an isolated session with dedicated MCP server and transport */ async getOrCreateSession(sessionId, req, res, isInitRequest, baseServerConfig, serverCapabilities, toolHandlers, firefliesApiKey) { let authSessionId; let sessionData; if (!sessionId && isInitRequest) { // Create new session with complete isolation authSessionId = randomUUID(); sessionId = randomUUID(); console.log(`๐Ÿ†• Creating new isolated Fireflies session: ${sessionId} (Auth: ${authSessionId})`); // Create completely isolated MCP server for this session const mcpServer = new Server(baseServerConfig, serverCapabilities); // Register ALL handlers for this session's server for (const [schema, handler] of toolHandlers) { mcpServer.setRequestHandler(schema, handler); } // Create dedicated transport with context preservation const transport = new SessionAwareStreamableTransport(sessionId, authSessionId, this.requestContextStorage); // Handle transport cleanup transport.onclose = () => { console.log(`๐Ÿ”Œ Fireflies transport closed for session ${sessionId}`); this.sessions.delete(sessionId); }; // Connect isolated server to isolated transport await mcpServer.connect(transport); sessionData = { transport, mcpServer, sessionId, authSessionId, firefliesApiKey, createdAt: new Date(), lastActivity: new Date(), requestCount: 0 }; this.sessions.set(sessionId, sessionData); console.log(`โœ… Fireflies session created successfully: ${sessionId}`); } else if (sessionId && this.sessions.has(sessionId)) { // Reuse existing session sessionData = this.sessions.get(sessionId); authSessionId = sessionData.authSessionId; sessionData.lastActivity = new Date(); // Update Fireflies API key if provided if (firefliesApiKey) { sessionData.firefliesApiKey = firefliesApiKey; } console.log(`๐Ÿ”„ Reusing existing Fireflies session: ${sessionId} (Auth: ${authSessionId})`); } else { throw new Error(`โŒ No existing Fireflies session data found for session ID: ${sessionId}`); } return { sessionData, authSessionId }; } /** * Handle request with complete session isolation and context preservation */ async handleSessionRequest(sessionData, req, res, requestBody) { const requestId = randomUUID(); sessionData.requestCount++; // Create request context with all necessary data const requestContext = { sessionId: sessionData.sessionId, authSessionId: sessionData.authSessionId, requestId, mcpServer: sessionData.mcpServer, firefliesApiKey: sessionData.firefliesApiKey, startTime: Date.now() }; console.log(`๐Ÿ”„ Using Fireflies session-aware context - MCP: ${sessionData.sessionId}, Auth: ${sessionData.authSessionId}, Request: ${requestId}`); // Execute request within preserved context return this.requestContextStorage.run(requestContext, async () => { try { await sessionData.transport.handleRequest(req, res, requestBody); console.log(`โœ… Fireflies request completed successfully for session ${sessionData.sessionId}, request ${requestId}`); } catch (error) { console.error(`โŒ Fireflies request failed for session ${sessionData.sessionId}, request ${requestId}:`, error); throw error; } }); } /** * Clean up expired sessions */ async cleanupExpiredSessions(maxAgeMs = 60 * 60 * 1000) { const now = Date.now(); const expiredSessions = []; for (const [sessionId, sessionData] of this.sessions.entries()) { const age = now - sessionData.lastActivity.getTime(); if (age > maxAgeMs) { expiredSessions.push(sessionId); } } for (const sessionId of expiredSessions) { const sessionData = this.sessions.get(sessionId); if (sessionData) { try { await sessionData.transport.close(); await sessionData.mcpServer.close(); } catch (error) { console.error(`Error cleaning up Fireflies session ${sessionId}:`, error); } this.sessions.delete(sessionId); console.log(`๐Ÿงน Cleaned up expired Fireflies session: ${sessionId}`); } } if (expiredSessions.length > 0) { console.log(`๐Ÿงน Cleaned up ${expiredSessions.length} expired Fireflies sessions`); } } /** * Get current session statistics */ getSessionStats() { const sessions = Array.from(this.sessions.values()).map(session => ({ sessionId: session.sessionId, authSessionId: session.authSessionId, firefliesApiKey: session.firefliesApiKey ? `${session.firefliesApiKey.substring(0, 8)}...` : undefined, requestCount: session.requestCount, age: `${Math.round((Date.now() - session.createdAt.getTime()) / 1000)}s` })); return { totalSessions: this.sessions.size, sessions }; } /** * Manually destroy a specific session */ async destroySession(sessionId) { const sessionData = this.sessions.get(sessionId); if (!sessionData) { return false; } try { await sessionData.transport.close(); await sessionData.mcpServer.close(); this.sessions.delete(sessionId); console.log(`๐Ÿ—‘๏ธ Manually destroyed Fireflies session: ${sessionId}`); return true; } catch (error) { console.error(`Error destroying Fireflies session ${sessionId}:`, error); return false; } } /** * Get the current request context (for use in tool handlers) */ getCurrentContext() { return this.requestContextStorage.getStore(); } /** * Cleanup all sessions and resources */ async destroy() { console.log(`๐Ÿ”ฅ Destroying Fireflies transport manager with ${this.sessions.size} sessions`); for (const [sessionId, sessionData] of this.sessions.entries()) { try { await sessionData.transport.close(); await sessionData.mcpServer.close(); } catch (error) { console.error(`Error destroying Fireflies session ${sessionId}:`, error); } } this.sessions.clear(); console.log(`โœ… Fireflies transport manager destroyed`); } }