@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
JavaScript
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`);
}
}