@gongrzhe/server-fireflies-mcp
Version:
MCP server for using the Fireflies.ai API with session-aware multi-user support
1,131 lines (1,127 loc) • 54.7 kB
JavaScript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
import axios from 'axios';
import express from 'express';
import { isInitializeRequest, } from "@modelcontextprotocol/sdk/types.js";
import { SessionAwareTransportManager } from "./session-aware-transport.js";
// Define tool configurations
const TOOLS = [
{
name: "fireflies_get_transcripts",
description: "Retrieve a list of meeting transcripts with optional filtering. By default, returns up to 20 most recent transcripts with no date filtering. Note that this operation may take longer for large datasets and might timeout. If a timeout occurs, a minimal set of transcript data will be returned.",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Maximum number of transcripts to return (default: 20). Consider using a smaller limit if experiencing timeouts."
},
from_date: {
type: "string",
description: "Start date in ISO format (YYYY-MM-DD). If not specified, no lower date bound is applied. Using a narrower date range can help prevent timeouts."
},
to_date: {
type: "string",
description: "End date in ISO format (YYYY-MM-DD). If not specified, no upper date bound is applied. Using a narrower date range can help prevent timeouts."
}
}
}
},
{
name: "fireflies_get_transcript_details",
description: "Retrieve detailed information about a specific transcript. Returns a human-readable formatted transcript with speaker names and text, along with metadata and summary information.",
inputSchema: {
type: "object",
properties: {
transcript_id: {
type: "string",
description: "ID of the transcript to retrieve"
}
},
required: ["transcript_id"]
}
},
{
name: "fireflies_search_transcripts",
description: "Search for transcripts containing specific keywords, with optional date filtering. Returns a human-readable list of matching transcripts with metadata and summary information.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query to find relevant transcripts"
},
limit: {
type: "number",
description: "Maximum number of transcripts to return (default: 20)"
},
from_date: {
type: "string",
description: "Start date in ISO format (YYYY-MM-DD) to filter transcripts by date. If not specified, no lower date bound is applied."
},
to_date: {
type: "string",
description: "End date in ISO format (YYYY-MM-DD) to filter transcripts by date. If not specified, no upper date bound is applied."
}
},
required: ["query"]
}
},
{
name: "fireflies_generate_summary",
description: "Generate a summary of a meeting transcript",
inputSchema: {
type: "object",
properties: {
transcript_id: {
type: "string",
description: "ID of the transcript to summarize"
},
format: {
type: "string",
enum: ["bullet_points", "paragraph"],
description: "Format of the summary (bullet_points or paragraph)"
}
},
required: ["transcript_id"]
}
}
];
class FirefliesApiClient {
baseUrl;
apiKey;
constructor(apiKey) {
this.baseUrl = 'https://api.fireflies.ai/graphql';
this.apiKey = apiKey;
}
async executeQuery(query, variables = {}) {
try {
// Log to stderr to avoid breaking MCP protocol
process.stderr.write(`Executing GraphQL query with variables: ${JSON.stringify(variables)}\n`);
const response = await axios.post(this.baseUrl, { query, variables }, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
// Increase timeout to 60 seconds
timeout: 60000
});
if (response.data.errors) {
process.stderr.write(`GraphQL errors: ${JSON.stringify(response.data.errors)}\n`);
throw new Error(`GraphQL error: ${response.data.errors[0].message}`);
}
return response.data.data;
}
catch (error) {
if (axios.isAxiosError(error)) {
process.stderr.write(`API request failed: ${error.message}\n`);
if (error.response) {
process.stderr.write(`Response status: ${error.response.status}\n`);
process.stderr.write(`Response data: ${JSON.stringify(error.response.data)}\n`);
}
if (error.code === 'ECONNABORTED') {
throw new McpError(ErrorCode.InternalError, `API request timed out after 60 seconds`);
}
else if (error.response?.status === 400) {
throw new McpError(ErrorCode.InvalidParams, `Bad request: ${error.response.data?.message || 'Invalid request parameters'}`);
}
else if (error.response?.status === 401) {
throw new McpError(ErrorCode.InvalidRequest, 'Invalid API key or unauthorized access');
}
else if (error.response?.status === 404) {
throw new McpError(ErrorCode.InvalidParams, 'Resource not found');
}
else {
throw new McpError(ErrorCode.InternalError, `API request failed: ${error.message}`);
}
}
throw new McpError(ErrorCode.InternalError, `Unknown error: ${error.message}`);
}
}
async getTranscripts(limit, fromDate, toDate, minimal = false) {
// Set a reasonable default limit if not provided
const actualLimit = limit || 20;
process.stderr.write(`Getting transcripts with limit: ${actualLimit}, fromDate: ${fromDate || 'not specified'}, toDate: ${toDate || 'not specified'}, minimal: ${minimal}\n`);
// Use a more optimized query with fewer fields to reduce response size
let query;
if (minimal) {
// Super minimal query for fallback
query = `
query Transcripts(
$limit: Int
$skip: Int
$fromDate: DateTime
$toDate: DateTime
) {
transcripts(
limit: $limit
skip: $skip
fromDate: $fromDate
toDate: $toDate
) {
id
title
date
}
}
`;
}
else {
// Standard optimized query
query = `
query Transcripts(
$limit: Int
$skip: Int
$fromDate: DateTime
$toDate: DateTime
) {
transcripts(
limit: $limit
skip: $skip
fromDate: $fromDate
toDate: $toDate
) {
id
title
date
dateString
duration
transcript_url
speakers {
id
name
}
summary {
keywords
overview
}
}
}
`;
}
// Prepare variables
const variables = {
limit: actualLimit,
skip: 0
};
// Add date filters if provided
if (fromDate) {
// Use ISO string format for DateTime
variables.fromDate = fromDate;
process.stderr.write(`Using fromDate: ${fromDate}\n`);
}
if (toDate) {
// Use ISO string format for DateTime
variables.toDate = toDate;
process.stderr.write(`Using toDate: ${toDate}\n`);
}
process.stderr.write(`Executing getTranscripts query with variables: ${JSON.stringify(variables)}\n`);
const startTime = Date.now();
try {
const data = await this.executeQuery(query, variables);
const endTime = Date.now();
process.stderr.write(`getTranscripts query completed in ${endTime - startTime}ms\n`);
const transcripts = data.transcripts || [];
process.stderr.write(`Retrieved ${transcripts.length} transcripts\n`);
if (transcripts.length <= 1) {
process.stderr.write(`WARNING: Only ${transcripts.length} transcript(s) returned. This might be due to:\n`);
process.stderr.write(`1. Limited data in your Fireflies account\n`);
process.stderr.write(`2. Date filters restricting results\n`);
process.stderr.write(`3. API permissions or visibility settings\n`);
}
return transcripts;
}
catch (error) {
process.stderr.write(`Error in getTranscripts: ${error instanceof Error ? error.message : String(error)}\n`);
// If this wasn't already a minimal query and we got a timeout, try again with minimal fields
if (!minimal && error instanceof Error &&
(error.message.includes('timeout') || error.message.includes('ECONNABORTED'))) {
process.stderr.write(`Retrying with minimal fields...\n`);
return this.getTranscripts(actualLimit, fromDate, toDate, true);
}
throw error;
}
}
async getTranscriptDetails(transcriptId, formatText = false) {
const query = `
query Transcript($transcriptId: String!) {
transcript(id: $transcriptId) {
id
dateString
privacy
speakers {
id
name
}
sentences {
index
speaker_name
speaker_id
text
raw_text
start_time
end_time
ai_filters {
task
pricing
metric
question
date_and_time
text_cleanup
sentiment
}
}
title
host_email
organizer_email
calendar_id
user {
user_id
email
name
num_transcripts
recent_meeting
minutes_consumed
is_admin
integrations
}
fireflies_users
participants
date
transcript_url
audio_url
video_url
duration
meeting_attendees {
displayName
email
phoneNumber
name
location
}
summary {
keywords
action_items
outline
shorthand_bullet
overview
bullet_gist
gist
short_summary
short_overview
meeting_type
topics_discussed
transcript_chapters
}
cal_id
calendar_type
apps_preview {
outputs {
transcript_id
user_id
app_id
created_at
title
prompt
response
}
}
meeting_link
}
}
`;
const variables = {
transcriptId: transcriptId
};
try {
process.stderr.write(`Getting transcript details for ID: ${transcriptId}\n`);
const data = await this.executeQuery(query, variables);
const transcript = data.transcript;
if (!transcript) {
throw new McpError(ErrorCode.InvalidParams, `Transcript with ID ${transcriptId} not found`);
}
// If formatText is true, format the sentences as a simple string
if (formatText && transcript.sentences && transcript.sentences.length > 0) {
// Create a formatted text version of the transcript
const formattedText = transcript.sentences.map((sentence) => {
return `${sentence.speaker_name}: ${sentence.text}`;
}).join('\n');
// Replace the sentences array with the formatted text
transcript.formatted_text = formattedText;
// Keep a minimal version of sentences for reference
transcript.sentences = transcript.sentences.map((sentence) => ({
index: sentence.index,
speaker_name: sentence.speaker_name,
text: sentence.text
}));
}
return transcript;
}
catch (error) {
process.stderr.write(`Error getting transcript details: ${error instanceof Error ? error.message : String(error)}\n`);
throw error;
}
}
async searchTranscripts(searchQuery, limit, fromDate, toDate) {
// Using the transcripts query with title parameter for search
const query = `
query Transcripts(
$title: String
$limit: Int
$skip: Int
$fromDate: DateTime
$toDate: DateTime
) {
transcripts(
title: $title
limit: $limit
skip: $skip
fromDate: $fromDate
toDate: $toDate
) {
id
title
date
dateString
duration
transcript_url
speakers {
id
name
}
summary {
keywords
overview
}
}
}
`;
// Set a reasonable default limit if not provided
const actualLimit = limit || 20;
process.stderr.write(`Searching transcripts with query: "${searchQuery}", limit: ${actualLimit}, fromDate: ${fromDate || 'not specified'}, toDate: ${toDate || 'not specified'}\n`);
// Prepare variables
const variables = {
title: searchQuery,
limit: actualLimit,
skip: 0
};
// Add date filters if provided
if (fromDate) {
// Use ISO string format for DateTime
variables.fromDate = fromDate;
process.stderr.write(`Using fromDate: ${fromDate}\n`);
}
if (toDate) {
// Use ISO string format for DateTime
variables.toDate = toDate;
process.stderr.write(`Using toDate: ${toDate}\n`);
}
try {
process.stderr.write(`Executing searchTranscripts query...\n`);
const startTime = Date.now();
const data = await this.executeQuery(query, variables);
const endTime = Date.now();
process.stderr.write(`searchTranscripts query completed in ${endTime - startTime}ms\n`);
const transcripts = data.transcripts || [];
process.stderr.write(`Found ${transcripts.length} matching transcripts\n`);
return transcripts;
}
catch (error) {
process.stderr.write(`Error in searchTranscripts: ${error instanceof Error ? error.message : String(error)}\n`);
throw error;
}
}
async generateTranscriptSummary(transcriptId, format = 'bullet_points') {
// First, get the transcript details with focus on summary fields
const query = `
query Transcript($transcriptId: String!) {
transcript(id: $transcriptId) {
id
title
summary {
keywords
action_items
overview
topics_discussed
}
}
}
`;
const variables = {
transcriptId: transcriptId
};
try {
process.stderr.write(`Generating summary for transcript ID: ${transcriptId}\n`);
const data = await this.executeQuery(query, variables);
const transcript = data.transcript;
// Extract the summary based on the requested format
if (!transcript || !transcript.summary) {
throw new McpError(ErrorCode.InvalidParams, 'Summary not available for this transcript');
}
// Log the summary structure to help with debugging
process.stderr.write(`Summary structure: ${JSON.stringify(transcript.summary)}\n`);
// Helper function to safely check if a field is an array
const isArray = (field) => Array.isArray(field);
// Helper function to safely join array elements or handle non-array values
const safeJoin = (field, separator) => {
if (isArray(field)) {
return field.join(separator);
}
else if (field && typeof field === 'string') {
return field;
}
else if (field) {
return String(field);
}
return '';
};
if (format === 'bullet_points') {
// Return bullet point format
const bullets = [];
if (transcript.summary.overview) {
bullets.push(`Overview: ${transcript.summary.overview}`);
}
// Safely handle action_items which might not be an array
if (transcript.summary.action_items) {
if (isArray(transcript.summary.action_items) && transcript.summary.action_items.length > 0) {
bullets.push('Action Items:');
transcript.summary.action_items.forEach((item) => {
bullets.push(`- ${item}`);
});
}
else if (typeof transcript.summary.action_items === 'string' && transcript.summary.action_items.trim()) {
bullets.push('Action Items:');
bullets.push(`- ${transcript.summary.action_items}`);
}
}
// Safely handle topics_discussed which might not be an array
if (transcript.summary.topics_discussed) {
if (isArray(transcript.summary.topics_discussed) && transcript.summary.topics_discussed.length > 0) {
bullets.push('Topics Discussed:');
transcript.summary.topics_discussed.forEach((topic) => {
bullets.push(`- ${topic}`);
});
}
else if (typeof transcript.summary.topics_discussed === 'string' && transcript.summary.topics_discussed.trim()) {
bullets.push('Topics Discussed:');
bullets.push(`- ${transcript.summary.topics_discussed}`);
}
}
// Safely handle keywords which might not be an array
if (transcript.summary.keywords) {
if (isArray(transcript.summary.keywords) && transcript.summary.keywords.length > 0) {
bullets.push(`Keywords: ${transcript.summary.keywords.join(', ')}`);
}
else if (typeof transcript.summary.keywords === 'string' && transcript.summary.keywords.trim()) {
bullets.push(`Keywords: ${transcript.summary.keywords}`);
}
}
return bullets.join('\n');
}
else {
// Return paragraph format
let summary = '';
if (transcript.summary.overview) {
summary += transcript.summary.overview + ' ';
}
// Safely handle topics_discussed
if (transcript.summary.topics_discussed) {
summary += 'Topics discussed include: ' + safeJoin(transcript.summary.topics_discussed, '; ') + '. ';
}
// Safely handle action_items
if (transcript.summary.action_items) {
summary += 'Action items include: ' + safeJoin(transcript.summary.action_items, '; ') + '. ';
}
// Safely handle keywords
if (transcript.summary.keywords) {
summary += 'Key topics: ' + safeJoin(transcript.summary.keywords, ', ') + '.';
}
return summary;
}
}
catch (error) {
process.stderr.write(`Error generating summary: ${error instanceof Error ? error.message : String(error)}\n`);
throw error;
}
}
}
class FirefliesServer {
apiClient = null;
server;
sessionAware;
constructor(apiKey, sessionAware = false) {
this.sessionAware = sessionAware;
if (!sessionAware) {
// For non-session-aware mode, require API key
const finalApiKey = apiKey || process.env.FIREFLIES_API_KEY;
if (!finalApiKey) {
console.error('Error: FIREFLIES_API_KEY environment variable is required for non-session-aware mode');
process.exit(1);
}
this.apiClient = new FirefliesApiClient(finalApiKey);
}
// For session-aware mode, apiClient will be created per request
this.server = new Server({
name: "fireflies-mcp-server",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
});
this.setupHandlers();
this.setupErrorHandling();
}
setupErrorHandling() {
this.server.onerror = (error) => {
// Write to stderr instead of using console.log to avoid breaking MCP protocol
process.stderr.write(`[MCP Error] ${error.message}\n`);
};
process.on('SIGINT', async () => {
await this.stop();
process.exit(0);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
process.stderr.write(`[Uncaught Exception] ${error.message}\n`);
process.exit(1);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason) => {
process.stderr.write(`[Unhandled Rejection] ${reason}\n`);
});
}
setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS
}));
// Handle tool calls (this will be overridden in session-aware mode)
this.server.setRequestHandler(CallToolRequestSchema, async (request) => this.handleToolCall(request.params.name, request.params.arguments ?? {}));
}
/**
* Get API client for current request context
*/
getApiClient(transportManager) {
if (!this.sessionAware && this.apiClient) {
return this.apiClient;
}
if (this.sessionAware && transportManager) {
const context = transportManager.getCurrentContext();
if (context?.firefliesApiKey) {
return new FirefliesApiClient(context.firefliesApiKey);
}
throw new Error('No Fireflies API key found in session context');
}
throw new Error('Unable to get API client');
}
/**
* Handles tool call requests
*/
async handleToolCall(name, args, transportManager) {
try {
const apiClient = this.getApiClient(transportManager);
switch (name) {
case "fireflies_get_transcripts": {
const { limit, from_date, to_date } = args;
process.stderr.write(`Handling fireflies_get_transcripts with args: ${JSON.stringify(args)}\n`);
try {
// Create a promise that will timeout after 90 seconds
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timed out after 90 seconds')), 90000);
});
// Race the actual API call against the timeout
const transcripts = await Promise.race([
apiClient.getTranscripts(limit, from_date, to_date),
timeoutPromise
]);
process.stderr.write(`Successfully retrieved ${transcripts.length} transcripts\n`);
let resultText = JSON.stringify(transcripts, null, 2);
// Add helpful message if only a few results were returned
if (transcripts.length <= 1) {
resultText += `\n\nNote: Only ${transcripts.length} transcript(s) were found. This might be due to:
1. Limited data in your Fireflies account
2. Date filters restricting results
3. API permissions or visibility settings
To retrieve more transcripts, you can:
- Specify a wider date range using from_date and to_date parameters
- Increase the limit parameter (default is 20)
- Check your Fireflies account permissions and settings`;
}
return {
toolResult: {
content: [{
type: "text",
text: resultText
}]
}
};
}
catch (error) {
process.stderr.write(`Error in fireflies_get_transcripts: ${error instanceof Error ? error.message : String(error)}\n`);
// If we hit a timeout, try with minimal fields
if (error instanceof Error && error.message.includes('timeout')) {
process.stderr.write(`Trying with minimal fields due to timeout...\n`);
const minimalTranscripts = await apiClient.getTranscripts(limit, from_date, to_date, true);
let resultText = JSON.stringify(minimalTranscripts, null, 2);
// Add helpful message
resultText += `\n\nNote: Due to timeout, only minimal transcript data was retrieved.
For more details, try requesting specific transcripts using their IDs.
If you're only seeing a few results, this might be due to:
1. Limited data in your Fireflies account
2. Default date range (no specific dates were provided)
3. API permissions or visibility settings
To retrieve more transcripts, you can:
- Specify a wider date range using from_date and to_date parameters
- Increase the limit parameter (default is 20)
- Check your Fireflies account permissions and settings`;
return {
toolResult: {
content: [{
type: "text",
text: resultText
}]
}
};
}
// Re-throw if it's not a timeout
throw error;
}
}
case "fireflies_get_transcript_details": {
const { transcript_id } = args;
if (!transcript_id) {
throw new McpError(ErrorCode.InvalidParams, 'transcript_id parameter is required');
}
process.stderr.write(`Getting transcript details for ID: ${transcript_id}\n`);
try {
// Get the transcript with formatted text
const transcript = await apiClient.getTranscriptDetails(transcript_id, true);
// Create a more readable output
let resultText = `Title: ${transcript.title}\n`;
resultText += `Date: ${transcript.dateString}\n`;
resultText += `Duration: ${Math.floor(transcript.duration / 60)}m ${Math.floor(transcript.duration % 60)}s\n`;
if (transcript.participants && transcript.participants.length > 0) {
resultText += `Participants: ${transcript.participants.join(', ')}\n`;
}
resultText += `\n--- Transcript ---\n\n`;
// Use the formatted text if available
if (transcript.formatted_text) {
resultText += transcript.formatted_text;
}
else {
// Fallback to formatting the sentences array
resultText += transcript.sentences.map((sentence) => `${sentence.speaker_name}: ${sentence.text}`).join('\n');
}
// Add summary if available
if (transcript.summary) {
resultText += `\n\n--- Summary ---\n\n`;
if (transcript.summary.overview) {
resultText += `Overview: ${transcript.summary.overview}\n\n`;
}
if (transcript.summary.action_items && Array.isArray(transcript.summary.action_items) &&
transcript.summary.action_items.length > 0) {
resultText += `Action Items:\n`;
transcript.summary.action_items.forEach((item) => {
resultText += `- ${item}\n`;
});
resultText += '\n';
}
if (transcript.summary.keywords && Array.isArray(transcript.summary.keywords) &&
transcript.summary.keywords.length > 0) {
resultText += `Keywords: ${transcript.summary.keywords.join(', ')}\n`;
}
}
return {
toolResult: {
content: [{
type: "text",
text: resultText
}]
}
};
}
catch (error) {
process.stderr.write(`Error in fireflies_get_transcript_details: ${error instanceof Error ? error.message : String(error)}\n`);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Error retrieving transcript details: ${error instanceof Error ? error.message : String(error)}`);
}
}
case "fireflies_search_transcripts": {
const { query, limit, from_date, to_date } = args;
if (!query) {
throw new McpError(ErrorCode.InvalidParams, 'query parameter is required');
}
process.stderr.write(`Searching transcripts with query: "${query}", limit: ${limit || 'default'}, from_date: ${from_date || 'not specified'}, to_date: ${to_date || 'not specified'}\n`);
try {
const transcripts = await apiClient.searchTranscripts(query, limit, from_date, to_date);
// Create a more readable output
let resultText = `Found ${transcripts.length} matching transcripts for query: "${query}"\n\n`;
if (transcripts.length === 0) {
resultText += `No transcripts found matching your search criteria. Try:\n`;
resultText += `- Using different search terms\n`;
resultText += `- Widening your date range\n`;
resultText += `- Increasing the limit parameter\n`;
}
else {
transcripts.forEach((transcript, index) => {
resultText += `${index + 1}. ${transcript.title}\n`;
resultText += ` ID: ${transcript.id}\n`;
resultText += ` Date: ${transcript.dateString}\n`;
resultText += ` Duration: ${Math.floor(transcript.duration / 60)}m ${Math.floor(transcript.duration % 60)}s\n`;
if (transcript.summary && transcript.summary.overview) {
resultText += ` Overview: ${transcript.summary.overview}\n`;
}
if (transcript.summary && transcript.summary.keywords &&
Array.isArray(transcript.summary.keywords) &&
transcript.summary.keywords.length > 0) {
resultText += ` Keywords: ${transcript.summary.keywords.join(', ')}\n`;
}
resultText += `\n`;
});
resultText += `To view the full transcript, use the fireflies_get_transcript_details tool with the transcript ID.`;
}
return {
toolResult: {
content: [{
type: "text",
text: resultText
}]
}
};
}
catch (error) {
process.stderr.write(`Error in fireflies_search_transcripts: ${error instanceof Error ? error.message : String(error)}\n`);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Error searching transcripts: ${error instanceof Error ? error.message : String(error)}`);
}
}
case "fireflies_generate_summary": {
const { transcript_id, format = 'bullet_points' } = args;
if (!transcript_id) {
throw new McpError(ErrorCode.InvalidParams, 'transcript_id parameter is required');
}
process.stderr.write(`Generating summary for transcript ID: ${transcript_id} with format: ${format}\n`);
try {
const summary = await apiClient.generateTranscriptSummary(transcript_id, format);
return {
toolResult: {
content: [{
type: "text",
text: summary
}]
}
};
}
catch (error) {
process.stderr.write(`Error generating summary: ${error instanceof Error ? error.message : String(error)}\n`);
// If the error is related to missing summary data, provide a helpful message
if (error instanceof McpError &&
error.message.includes('Summary not available')) {
return {
toolResult: {
content: [{
type: "text",
text: `No summary is available for this transcript (ID: ${transcript_id}). This might be because:
1. The transcript is still being processed
2. The transcript is too short to generate a meaningful summary
3. The summary feature is not enabled for your account
You can try:
- Checking the transcript details to see if it has been fully processed
- Using a different transcript ID
- Contacting Fireflies support if you believe this is an error`
}]
}
};
}
// For other errors, re-throw
throw error;
}
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
process.stderr.write(`Error in handleToolCall for ${name}: ${error instanceof Error ? error.message : String(error)}\n`);
if (error instanceof McpError) {
throw error;
}
// Ensure we're returning a proper McpError
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `Error processing request: ${error.message}`);
}
else {
throw new McpError(ErrorCode.InternalError, `Unknown error occurred`);
}
}
}
/**
* Starts the server
*/
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
// Write to stderr to avoid breaking MCP protocol
process.stderr.write('Fireflies MCP server is running\n');
}
/**
* Stops the server
*/
async stop() {
try {
await this.server.close();
}
catch (error) {
process.stderr.write(`Error while stopping server: ${error instanceof Error ? error.message : String(error)}\n`);
}
}
}
// Helper function to validate Fireflies API key
async function validateFirefliesToken(token) {
if (!token) {
return { isValid: false, error: "No API token provided" };
}
try {
// Use a simple GraphQL query to validate the token
const testQuery = `
query {
transcripts(limit: 1) {
id
}
}
`;
const response = await axios.post('https://api.fireflies.ai/graphql', { query: testQuery }, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
timeout: 10000
});
if (response.data.errors) {
return { isValid: false, error: `API Error: ${response.data.errors[0].message}` };
}
return { isValid: true };
}
catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
return { isValid: false, error: 'Invalid API key or unauthorized access' };
}
return { isValid: false, error: `API request failed: ${error.message}` };
}
return { isValid: false, error: `Network error: ${error.message}` };
}
}
// HTTP/SSE server setup function
async function startHttpServer(transportMode) {
const app = express();
app.use(express.json());
console.log(`🚀 Starting Fireflies MCP Server with Session-Aware Transport (${transportMode.toUpperCase()})...`);
// Initialize session-aware transport manager
const transportManager = new SessionAwareTransportManager();
// Create base server configuration that will be used for each session
const baseServerConfig = {
name: 'fireflies-mcp-server',
version: '1.0.0',
};
const serverCapabilities = {
capabilities: {
tools: {}
}
};
// Create tool handlers map for session isolation
const listToolsHandler = async () => {
return {
tools: TOOLS
};
};
const callToolHandler = async (request) => {
// Create a temporary FirefliesServer instance for this session
const server = new FirefliesServer(undefined, true);
return await server.handleToolCall(request.params.name, request.params.arguments ?? {}, transportManager);
};
const toolHandlers = new Map([
[ListToolsRequestSchema, listToolsHandler],
[CallToolRequestSchema, callToolHandler]
]);
// Set up session cleanup interval
const cleanupInterval = setInterval(async () => {
await transportManager.cleanupExpiredSessions();
}, 5 * 60 * 1000); // Clean up every 5 minutes
if (transportMode === 'http') {
// Streamable HTTP endpoint
app.all('/mcp', async (req, res) => {
try {
// Set CORS headers
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Authorization, fireflies-api-key');
// Get Authorization header or fireflies-api-key header
const authHeader = req.headers['authorization'];
const apiKeyHeader = req.headers['fireflies-api-key'];
let firefliesApiKey;
// Extract token from Authorization header (Bearer token)
if (authHeader && authHeader.startsWith('Bearer ')) {
firefliesApiKey = authHeader.substring(7);
}
else if (apiKeyHeader) {
firefliesApiKey = apiKeyHeader;
}
const sessionId = req.headers['mcp-session-id'];
const isInitRequest = req.method === 'POST' && isInitializeRequest(req.body);
// Handle POST requests without session ID for non-initialize requests
if (req.method === 'POST' && !sessionId && !isInitRequest) {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: Session ID required for non-initialize requests',
},
id: req.body?.id || null,
});
return;
}
// Handle GET/DELETE requests without session ID
if ((req.method === 'GET' || req.method === 'DELETE') && !sessionId) {
res.status(400).send('Invalid or missing session ID');
return;
}
// Validate API key for new connections only
if (!sessionId && isInitRequest && firefliesApiKey) {
console.log(`🔐 Validating Fireflies API key for new connection...`);
const validation = await validateFirefliesToken(firefliesApiKey);
if (!validation.isValid) {
console.log(`❌ Fireflies connection rejected - invalid token: ${validation.error}`);
res.status(401).json({
jsonrpc: '2.0',
error: {
code: -32001,
message: 'Authentication Failed',
data: {
reason: 'Invalid Fireflies API key',
details: validation.error,
action: 'Please provide a valid Fireflies API Token in the Authorization header as "Bearer <token>"'
}
},
id: req.body?.id || null,
});
return;
}
console.log(`✅ Fireflies API key validated successfully`);
}
// Get or create session with complete isolation
const { sessionData, authSessionId } = await transportManager.getOrCreateSession(sessionId, req, res, isInitRequest, baseServerConfig, serverCapabilities, toolHandlers, firefliesApiKey);
// Handle request with session-aware context preservation
await transportManager.handleSessionRequest(sessionData, req, res, req.body);
}
catch (error) {
console.error('❌ Error handling Fireflies HTTP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
}
if (transportMode === 'sse') {
// SSE endpoint
app.get('/sse', async (req, res) => {
try {
const authHeader = req.headers['authorization'];
const apiKeyHeader = req.headers['fireflies-api-key'];
let firefliesApiKey;
if (authHeader && authHeader.startsWith('Bearer ')) {
firefliesApiKey = authHeader.substring(7);
}
else if (apiKeyHeader) {
firefliesApiKey = apiKeyHeader;
}
const transport = new SSEServerTransport('/messages', res);
res.on("close", () => {
console.log(`🔌 SSE session closed: ${transport.sessionId}`);
});
// Create FirefliesServer instance (SSE mode still uses direct API key)
if (!firefliesApiKey) {
res.status(400).send('API key required for SSE mode');
return;
}
const server = new FirefliesServer(firefliesApiKey);
await server.server.connect(transport);
console.log(`📡 SSE session started: ${transport.sessionId}`);
}
catch (error) {
console.error('❌ Error starting SSE transport:', error);
res.status(500).send('Failed to start SSE transport');
}
});
// Message endpoint for SSE clients
app.post('/messages', async (req, res) => {
try {
const sessionId = req.query.sessionId;
const authHeader = req.headers['authorization'];
const apiKeyHeader = req.headers['fireflies-api-key'];
let firefliesApiKey;
if (authHeader && authHeader.startsWith('Bearer ')) {
firefliesApiKey = authHeader.substring(7);
}
else if (apiKeyHeader) {
firefliesApiKey = apiKeyHeader;
}
res.status(200).json({ message: 'SSE message handled' });
}
catch (error) {
console.error('❌ Error handling SSE message:', error);
res.status(500).send('Error processing message');
}
});
}
// Handle CORS preflight for all endpoints
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Authorization, fireflies-api-key');
res.sendStatus(200);
});
// Enhanced health check endpoint with session statistics
app.get('/health', (req, res) => {
const sessionStats = transportManager.getSessionStats();
res.json({
status: 'ok',
server: 'Fireflies MCP Server (Session-Aware)',
transport: transportMode,
timestamp: new Date().toISOString(),
version: '1.0.0',
sessionIsolation: true,
activeSessions: sessionStats.totalSessions,
sessions: sessionStats.sessions
});
});
// Session management endpoint
app.get('/sessions', (req, res) => {
const sessionStats = transportManager.getSessionStats();
res.json({
server: 'Fireflies MCP Server',
sessionManagement: 'Session-Aware Transport',
timestamp: new Date().toISOString(),
...sessionStats
});
});
// API documentation endpoint
app.get('/', (req, res) => {
res.json({
name: 'Fireflies MCP Server (Session-Aware)',
version: '1.