@gongrzhe/server-fireflies-mcp
Version:
MCP server for using the Fireflies.ai API with session-aware multi-user support
857 lines (852 loc) • 38.2 kB
JavaScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
import axios from 'axios';
import express from 'express';
import { AsyncLocalStorage } from 'node:async_hooks';
const asyncLocalStorage = new AsyncLocalStorage();
// 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 {
console.log(`Executing GraphQL query with variables: ${JSON.stringify(variables)}`);
const response = await axios.post(this.baseUrl, { query, variables }, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
timeout: 60000
});
if (response.data.errors) {
console.log(`GraphQL errors: ${JSON.stringify(response.data.errors)}`);
throw new Error(`GraphQL error: ${response.data.errors[0].message}`);
}
return response.data.data;
}
catch (error) {
if (axios.isAxiosError(error)) {
console.log(`API request failed: ${error.message}`);
if (error.response) {
console.log(`Response status: ${error.response.status}`);
console.log(`Response data: ${JSON.stringify(error.response.data)}`);
}
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) {
const actualLimit = limit || 20;
console.log(`Getting transcripts with limit: ${actualLimit}, fromDate: ${fromDate || 'not specified'}, toDate: ${toDate || 'not specified'}, minimal: ${minimal}`);
let query;
if (minimal) {
query = `
query Transcripts(
$limit: Int
$skip: Int
$fromDate: DateTime
$toDate: DateTime
) {
transcripts(
limit: $limit
skip: $skip
fromDate: $fromDate
toDate: $toDate
) {
id
title
date
}
}
`;
}
else {
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
}
}
}
`;
}
const variables = {
limit: actualLimit,
skip: 0
};
if (fromDate) {
variables.fromDate = fromDate;
console.log(`Using fromDate: ${fromDate}`);
}
if (toDate) {
variables.toDate = toDate;
console.log(`Using toDate: ${toDate}`);
}
console.log(`Executing getTranscripts query with variables: ${JSON.stringify(variables)}`);
const startTime = Date.now();
try {
const data = await this.executeQuery(query, variables);
const endTime = Date.now();
console.log(`getTranscripts query completed in ${endTime - startTime}ms`);
const transcripts = data.transcripts || [];
console.log(`Retrieved ${transcripts.length} transcripts`);
if (transcripts.length <= 1) {
console.log(`WARNING: Only ${transcripts.length} transcript(s) returned. This might be due to:`);
console.log(`1. Limited data in your Fireflies account`);
console.log(`2. Date filters restricting results`);
console.log(`3. API permissions or visibility settings`);
}
return transcripts;
}
catch (error) {
console.log(`Error in getTranscripts: ${error instanceof Error ? error.message : String(error)}`);
if (!minimal && error instanceof Error &&
(error.message.includes('timeout') || error.message.includes('ECONNABORTED'))) {
console.log(`Retrying with minimal fields...`);
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
}
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
meeting_link
}
}
`;
const variables = {
transcriptId: transcriptId
};
try {
console.log(`Getting transcript details for ID: ${transcriptId}`);
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 && transcript.sentences && transcript.sentences.length > 0) {
const formattedText = transcript.sentences.map((sentence) => {
return `${sentence.speaker_name}: ${sentence.text}`;
}).join('\n');
transcript.formatted_text = formattedText;
transcript.sentences = transcript.sentences.map((sentence) => ({
index: sentence.index,
speaker_name: sentence.speaker_name,
text: sentence.text
}));
}
return transcript;
}
catch (error) {
console.log(`Error getting transcript details: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
async searchTranscripts(searchQuery, limit, fromDate, toDate) {
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
}
}
}
`;
const actualLimit = limit || 20;
console.log(`Searching transcripts with query: "${searchQuery}", limit: ${actualLimit}, fromDate: ${fromDate || 'not specified'}, toDate: ${toDate || 'not specified'}`);
const variables = {
title: searchQuery,
limit: actualLimit,
skip: 0
};
if (fromDate) {
variables.fromDate = fromDate;
console.log(`Using fromDate: ${fromDate}`);
}
if (toDate) {
variables.toDate = toDate;
console.log(`Using toDate: ${toDate}`);
}
try {
console.log(`Executing searchTranscripts query...`);
const startTime = Date.now();
const data = await this.executeQuery(query, variables);
const endTime = Date.now();
console.log(`searchTranscripts query completed in ${endTime - startTime}ms`);
const transcripts = data.transcripts || [];
console.log(`Found ${transcripts.length} matching transcripts`);
return transcripts;
}
catch (error) {
console.log(`Error in searchTranscripts: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
async generateTranscriptSummary(transcriptId, format = 'bullet_points') {
const query = `
query Transcript($transcriptId: String!) {
transcript(id: $transcriptId) {
id
title
summary {
keywords
action_items
overview
topics_discussed
}
}
}
`;
const variables = {
transcriptId: transcriptId
};
try {
console.log(`Generating summary for transcript ID: ${transcriptId}`);
const data = await this.executeQuery(query, variables);
const transcript = data.transcript;
if (!transcript || !transcript.summary) {
throw new McpError(ErrorCode.InvalidParams, 'Summary not available for this transcript');
}
console.log(`Summary structure: ${JSON.stringify(transcript.summary)}`);
const isArray = (field) => Array.isArray(field);
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') {
const bullets = [];
if (transcript.summary.overview) {
bullets.push(`Overview: ${transcript.summary.overview}`);
}
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}`);
}
}
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}`);
}
}
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 {
let summary = '';
if (transcript.summary.overview) {
summary += transcript.summary.overview + ' ';
}
if (transcript.summary.topics_discussed) {
summary += 'Topics discussed include: ' + safeJoin(transcript.summary.topics_discussed, '; ') + '. ';
}
if (transcript.summary.action_items) {
summary += 'Action items include: ' + safeJoin(transcript.summary.action_items, '; ') + '. ';
}
if (transcript.summary.keywords) {
summary += 'Key topics: ' + safeJoin(transcript.summary.keywords, ', ') + '.';
}
return summary;
}
}
catch (error) {
console.log(`Error generating summary: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
}
// Factory function to create fresh server instances
function createStatelessServer() {
const server = new Server({
name: "fireflies-mcp-server",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: TOOLS,
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const context = asyncLocalStorage.getStore();
const userAuthToken = context?.userToken;
return await asyncLocalStorage.run({ token: null, userToken: userAuthToken }, async () => {
try {
if (!request.params.arguments) {
throw new Error("Arguments are required");
}
const args = request.params.arguments;
// Use token from Authorization header
const token = userAuthToken;
if (!token) {
return {
content: [{
type: "text",
text: JSON.stringify({
error: "Authentication required",
message: "Please provide Authorization: Bearer <token> header",
required_header: "Authorization"
}, null, 2)
}],
};
}
const apiClient = new FirefliesApiClient(token);
switch (request.params.name) {
case "fireflies_get_transcripts": {
const { limit, from_date, to_date } = args;
console.log(`Handling fireflies_get_transcripts with args: ${JSON.stringify(args)}`);
try {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timed out after 90 seconds')), 90000);
});
const transcripts = await Promise.race([
apiClient.getTranscripts(typeof limit === 'number' ? limit : undefined, typeof from_date === 'string' ? from_date : undefined, typeof to_date === 'string' ? to_date : undefined),
timeoutPromise
]);
console.log(`Successfully retrieved ${transcripts.length} transcripts`);
let resultText = JSON.stringify(transcripts, null, 2);
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 {
content: [{
type: "text",
text: resultText
}]
};
}
catch (error) {
console.log(`Error in fireflies_get_transcripts: ${error instanceof Error ? error.message : String(error)}`);
if (error instanceof Error && error.message.includes('timeout')) {
console.log(`Trying with minimal fields due to timeout...`);
const minimalTranscripts = await apiClient.getTranscripts(typeof limit === 'number' ? limit : undefined, typeof from_date === 'string' ? from_date : undefined, typeof to_date === 'string' ? to_date : undefined, true);
let resultText = JSON.stringify(minimalTranscripts, null, 2);
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 {
content: [{
type: "text",
text: resultText
}]
};
}
throw error;
}
}
case "fireflies_get_transcript_details": {
const { transcript_id } = args;
if (!transcript_id) {
throw new McpError(ErrorCode.InvalidParams, 'transcript_id parameter is required');
}
console.log(`Getting transcript details for ID: ${transcript_id}`);
try {
const transcript = await apiClient.getTranscriptDetails(typeof transcript_id === 'string' ? transcript_id : '', true);
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`;
if (transcript.formatted_text) {
resultText += transcript.formatted_text;
}
else {
resultText += transcript.sentences.map((sentence) => `${sentence.speaker_name}: ${sentence.text}`).join('\n');
}
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 {
content: [{
type: "text",
text: resultText
}]
};
}
catch (error) {
console.log(`Error in fireflies_get_transcript_details: ${error instanceof Error ? error.message : String(error)}`);
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');
}
console.log(`Searching transcripts with query: "${query}", limit: ${limit || 'default'}, from_date: ${from_date || 'not specified'}, to_date: ${to_date || 'not specified'}`);
try {
const transcripts = await apiClient.searchTranscripts(typeof query === 'string' ? query : '', typeof limit === 'number' ? limit : undefined, typeof from_date === 'string' ? from_date : undefined, typeof to_date === 'string' ? to_date : undefined);
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 {
content: [{
type: "text",
text: resultText
}]
};
}
catch (error) {
console.log(`Error in fireflies_search_transcripts: ${error instanceof Error ? error.message : String(error)}`);
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');
}
console.log(`Generating summary for transcript ID: ${transcript_id} with format: ${format}`);
try {
const summary = await apiClient.generateTranscriptSummary(typeof transcript_id === 'string' ? transcript_id : '', typeof format === 'string' ? format : 'bullet_points');
return {
content: [{
type: "text",
text: summary
}]
};
}
catch (error) {
console.log(`Error generating summary: ${error instanceof Error ? error.message : String(error)}`);
if (error instanceof McpError &&
error.message.includes('Summary not available')) {
return {
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`
}]
};
}
throw error;
}
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
}
catch (error) {
console.log(`Error in tool call for ${request.params.name}: ${error instanceof Error ? error.message : String(error)}`);
if (error instanceof McpError) {
throw error;
}
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `Error processing request: ${error.message}`);
}
else {
throw new McpError(ErrorCode.InternalError, `Unknown error occurred`);
}
}
});
});
return server;
}
// Stateless HTTP server
async function startStatelessServer() {
const app = express();
app.use(express.json());
console.log('🚀 Starting Fireflies MCP Server (Stateless Mode)...');
app.post('/mcp', async (req, res) => {
try {
// Extract auth token from Authorization header
const authHeader = req.headers.authorization;
const userAuthToken = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : undefined;
// Create fresh instances for complete isolation
const server = createStatelessServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Key: stateless mode
});
// Clean up on request close
res.on('close', () => {
transport.close();
server.close();
});
// Set request context with auth token
await asyncLocalStorage.run({ token: null, userToken: userAuthToken }, async () => {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
}
catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: { code: -32603, message: 'Internal server error' },
id: null
});
}
}
});
// Disable GET/DELETE for stateless mode
app.get('/mcp', (req, res) => {
res.status(405).json({
jsonrpc: "2.0",
error: { code: -32000, message: "Method not allowed in stateless mode" },
id: null
});
});
app.delete('/mcp', (req, res) => {
res.status(405).json({
jsonrpc: "2.0",
error: { code: -32000, message: "Method not allowed in stateless mode" },
id: null
});
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
server: 'Fireflies MCP Server (Stateless)',
timestamp: new Date().toISOString(),
version: '1.0.0',
sessionIsolation: false,
stateless: true
});
});
// API documentation endpoint
app.get('/', (req, res) => {
res.json({
name: 'Fireflies MCP Server (Stateless)',
version: '1.0.0',
protocol: 'Streamable HTTP (2025-03-26)',
stateless: true,
authentication: {
method: 'Bearer Token',
headers: ['Authorization: Bearer <fireflies-api-key>'],
description: 'Provide your Fireflies API key in the Authorization header'
},
endpoints: {
mcp: 'POST /mcp - Stateless MCP endpoint',
health: 'GET /health - Health check',
docs: 'GET / - This documentation'
},
tools: [
'fireflies_get_transcripts',
'fireflies_get_transcript_details',
'fireflies_search_transcripts',
'fireflies_generate_summary'
],
documentation: 'https://docs.fireflies.ai/'
});
});
const port = process.env.PORT || 30000;
app.listen(port, () => {
console.log(`✅ Stateless Fireflies MCP Server listening on port ${port}`);
console.log(`📡 Endpoint: http://localhost:${port}/mcp`);
console.log(`🔓 Session management: DISABLED (stateless)`);
console.log(`🔐 Auth: Use 'Authorization: Bearer <fireflies-api-key>' header`);
console.log(`💚 Health check: http://localhost:${port}/health`);
console.log(`📚 Documentation: http://localhost:${port}/`);
});
}
startStatelessServer().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});