Skip to main content
Glama
Angad-2002

Attendee MCP Server

by Angad-2002
index.ts23.8 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; // Types interface BotResponse { bot_id: string; meeting_url: string; state: string; transcription_state: string; name?: string; } interface BotStatus { bot_id: string; state: string; transcription_state: string; meeting_url: string; active: boolean; transcript_ready: boolean; } interface TranscriptResponse { transcript?: string; transcription_state: string; ready: boolean; error?: string; } interface CreateBotRequest { meeting_url: string; bot_name?: string; } interface ChatMessage { id: string; message: string; sender_name: string; created_at: string; } interface Recording { url: string; file_size?: number; duration_ms?: number; } // Configuration const API_BASE_URL = process.env.MEETING_BOT_API_URL || "http://localhost:8000"; const API_KEY = process.env.MEETING_BOT_API_KEY; class MeetingBotMCP { private server: Server; constructor() { this.server = new Server( { name: "meeting-bot-mcp", version: "1.1.0", }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } private async makeApiRequest( endpoint: string, method: string = "GET", body?: any ): Promise<any> { const url = `${API_BASE_URL}${endpoint}`; const headers: Record<string, string> = { "Content-Type": "application/json", }; if (API_KEY) { headers["Authorization"] = `Token ${API_KEY}`; } try { const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API error ${response.status}: ${errorText}`); } return await response.json(); } catch (error) { if (error instanceof Error) { throw new Error(`Network error: ${error.message}`); } throw error; } } private formatBotStatus(data: any): string { const stateIcon = (data.state === 'joining' || data.state === 'joined' || data.state === 'joined_recording') ? "✅" : "❌"; const transcriptIcon = data.transcription_state === 'complete' ? "✅" : "⏳"; return [ `🤖 Bot Status for ${data.id}:`, "", `📊 State: ${data.state} ${stateIcon}`, `📝 Transcription State: ${data.transcription_state} ${transcriptIcon}`, `🔗 Meeting URL: ${data.meeting_url}`, "", `${stateIcon} Bot is ${(data.state === 'joining' || data.state === 'joined' || data.state === 'joined_recording') ? "active and recording" : "not active"}`, `${transcriptIcon} Transcript is ${data.transcription_state === 'complete' ? "ready" : "not ready yet"}`, ].join("\n"); } private formatBotCreated(data: any): string { return [ "✅ Successfully created meeting bot!", "", `🤖 Bot ID: ${data.id}`, `🔗 Meeting URL: ${data.meeting_url}`, `📊 State: ${data.state}`, `📝 Transcription State: ${data.transcription_state}`, "", `💡 You can check the bot status using bot ID: ${data.id}`, ].join("\n"); } private formatTranscriptResponse(data: any, botId: string): string { // If data is an array, it means we got the transcript entries directly if (Array.isArray(data)) { if (data.length === 0) { return `❌ No transcript available for bot ${botId}`; } let transcript = `📝 Meeting Transcript for bot ${botId}:\n\n`; transcript += "─".repeat(50) + "\n"; data.forEach((entry: any) => { const timestamp = entry.timestamp_ms / 1000; const minutes = Math.floor(timestamp / 60); const seconds = Math.floor(timestamp % 60); const timeStr = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; transcript += `[${timeStr}] ${entry.speaker_name}:\n${entry.transcription}\n\n`; }); transcript += "─".repeat(50) + `\n📊 Total entries: ${data.length}`; return transcript; } // Handle object response (legacy format) if (data.ready && data.transcript) { return [ `📝 Meeting Transcript for bot ${botId}:`, "", "─".repeat(50), data.transcript, "─".repeat(50), ].join("\n"); } else { const stateIcon = data.transcription_state === "in_progress" ? "🔄" : "⏳"; return [ `${stateIcon} Transcript not ready for bot ${botId}`, `Current transcription state: ${data.transcription_state}`, "", "💡 The transcript will be available after the meeting ends and processing completes.", ].join("\n"); } } private formatChatMessages(data: any, botId: string): string { if (!Array.isArray(data) || data.length === 0) { return `💬 No chat messages found for bot ${botId}`; } let chatOutput = `💬 Chat Messages for bot ${botId}:\n\n`; chatOutput += "─".repeat(50) + "\n"; data.forEach((message: any) => { const timestamp = new Date(message.created_at).toLocaleTimeString(); chatOutput += `[${timestamp}] ${message.sender_name}:\n${message.message}\n\n`; }); chatOutput += "─".repeat(50) + `\n📊 Total messages: ${data.length}`; return chatOutput; } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "create_meeting_bot", description: "Create a bot to join a meeting and record/transcribe it", inputSchema: { type: "object", properties: { meeting_url: { type: "string", description: "URL of the meeting (Zoom, Google Meet, or Teams)", }, bot_name: { type: "string", description: "Name for the bot (optional, defaults to 'Go Bot')", default: "Go Bot", }, }, required: ["meeting_url"], }, }, { name: "get_bot_status", description: "Get the current status of a meeting bot", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot to check", }, }, required: ["bot_id"], }, }, { name: "get_meeting_transcript", description: "Get the transcript from a meeting bot", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot whose transcript to retrieve", }, }, required: ["bot_id"], }, }, { name: "list_meeting_bots", description: "List all active meeting bots", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "remove_meeting_bot", description: "Remove a bot from a meeting", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot to remove", }, }, required: ["bot_id"], }, }, { name: "make_bot_speak", description: "Make a bot speak text during a meeting using text-to-speech", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot that should speak", }, text: { type: "string", description: "Text for the bot to speak", }, voice_language_code: { type: "string", description: "Voice language code (optional, defaults to 'en-US')", default: "en-US", }, voice_name: { type: "string", description: "Voice name (optional, defaults to 'en-US-Casual-K')", default: "en-US-Casual-K", }, }, required: ["bot_id", "text"], }, }, { name: "send_chat_message", description: "Send a chat message from the bot to the meeting", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot that should send the message", }, message: { type: "string", description: "Message text to send", }, }, required: ["bot_id", "message"], }, }, { name: "get_chat_messages", description: "Get chat messages from the meeting", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot to get chat messages for", }, }, required: ["bot_id"], }, }, { name: "get_recording", description: "Get the recording URL for a bot", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot to get recording for", }, }, required: ["bot_id"], }, }, { name: "send_image_to_meeting", description: "Send an image to the meeting through the bot (Google Meet only)", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot that should display the image", }, image_url: { type: "string", description: "HTTPS URL of the image to display", }, }, required: ["bot_id", "image_url"], }, }, { name: "send_video_to_meeting", description: "Send a video to the meeting through the bot (Google Meet only)", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot that should play the video", }, video_url: { type: "string", description: "HTTPS URL of the MP4 video to play", }, }, required: ["bot_id", "video_url"], }, }, { name: "delete_bot_data", description: "Delete all data associated with a bot (recordings, transcripts, etc.)", inputSchema: { type: "object", properties: { bot_id: { type: "string", description: "ID of the bot to delete data for", }, }, required: ["bot_id"], }, }, ] satisfies Tool[], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const args = request.params.arguments || {}; switch (request.params.name) { case "create_meeting_bot": return await this.createMeetingBot(args); case "get_bot_status": return await this.getBotStatus(args); case "get_meeting_transcript": return await this.getMeetingTranscript(args); case "list_meeting_bots": return await this.listMeetingBots(); case "remove_meeting_bot": return await this.removeMeetingBot(args); case "make_bot_speak": return await this.makeBotSpeak(args); case "send_chat_message": return await this.sendChatMessage(args); case "get_chat_messages": return await this.getChatMessages(args); case "get_recording": return await this.getRecording(args); case "send_image_to_meeting": return await this.sendImageToMeeting(args); case "send_video_to_meeting": return await this.sendVideoToMeeting(args); case "delete_bot_data": return await this.deleteBotData(args); default: throw new Error(`Unknown tool: ${request.params.name}`); } } catch (error) { return { content: [ { type: "text", text: `❌ Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); } private async createMeetingBot(args: Record<string, unknown>) { const meeting_url = args.meeting_url as string; const bot_name = (args.bot_name as string) || "Claude Bot"; if (!meeting_url || typeof meeting_url !== 'string') { throw new Error("Missing or invalid required parameter: meeting_url"); } const data = await this.makeApiRequest("/api/v1/bots", "POST", { meeting_url, bot_name, }); return { content: [ { type: "text", text: this.formatBotCreated(data), }, ], }; } private async getBotStatus(args: Record<string, unknown>) { const bot_id = args.bot_id as string; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } const data = await this.makeApiRequest(`/api/v1/bots/${bot_id}`); return { content: [ { type: "text", text: this.formatBotStatus(data), }, ], }; } private async getMeetingTranscript(args: Record<string, unknown>) { const bot_id = args.bot_id as string; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } const data = await this.makeApiRequest(`/api/v1/bots/${bot_id}/transcript`); return { content: [ { type: "text", text: this.formatTranscriptResponse(data, bot_id), }, ], }; } private async listMeetingBots() { const data = await this.makeApiRequest("/api/v1/bots"); // Handle both array response and object with bots property const bots = Array.isArray(data) ? data : (data.bots || []); if (bots.length === 0) { return { content: [ { type: "text", text: "📋 No active meeting bots found.", }, ], }; } const botList = bots .map((bot: any, index: number) => { const stateIcon = (bot.state === 'joining' || bot.state === 'joined' || bot.state === 'joined_recording') ? "✅" : "❌"; const transcriptIcon = bot.transcription_state === 'complete' ? "✅" : "⏳"; return `${index + 1}. Bot ID: ${bot.id}\n 📊 State: ${bot.state} ${stateIcon}\n 📝 Transcription: ${bot.transcription_state} ${transcriptIcon}\n 🔗 Meeting: ${bot.meeting_url.substring(0, 50)}...`; }) .join("\n\n"); return { content: [ { type: "text", text: `📋 Active Meeting Bots (${bots.length}):\n\n${botList}`, }, ], }; } private async removeMeetingBot(args: Record<string, unknown>) { const bot_id = args.bot_id as string; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } const data = await this.makeApiRequest(`/api/v1/bots/${bot_id}/leave`, "POST", {}); return { content: [ { type: "text", text: `✅ Successfully requested bot ${bot_id} to leave the meeting.\n\n📊 Updated Status:\n${this.formatBotStatus(data)}`, }, ], }; } private async makeBotSpeak(args: Record<string, unknown>) { const bot_id = args.bot_id as string; const text = args.text as string; const voice_language_code = (args.voice_language_code as string) || "en-US"; const voice_name = (args.voice_name as string) || "en-US-Casual-K"; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } if (!text || typeof text !== 'string') { throw new Error("Missing or invalid required parameter: text"); } const speechData = { text, text_to_speech_settings: { google: { voice_language_code, voice_name } } }; await this.makeApiRequest(`/api/v1/bots/${bot_id}/speech`, "POST", speechData); return { content: [ { type: "text", text: `✅ Bot ${bot_id} will speak: "${text}"\n\n🔊 Voice: ${voice_name} (${voice_language_code})\n💡 The bot should now be speaking in the meeting!`, }, ], }; } private async sendChatMessage(args: Record<string, unknown>) { const bot_id = args.bot_id as string; const message = args.message as string; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } if (!message || typeof message !== 'string') { throw new Error("Missing or invalid required parameter: message"); } await this.makeApiRequest(`/api/v1/bots/${bot_id}/send_chat_message`, "POST", { message }); return { content: [ { type: "text", text: `✅ Chat message sent from bot ${bot_id}: "${message}"`, }, ], }; } private async getChatMessages(args: Record<string, unknown>) { const bot_id = args.bot_id as string; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } const data = await this.makeApiRequest(`/api/v1/bots/${bot_id}/chat_messages`); return { content: [ { type: "text", text: this.formatChatMessages(data, bot_id), }, ], }; } private async getRecording(args: Record<string, unknown>) { const bot_id = args.bot_id as string; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } const data = await this.makeApiRequest(`/api/v1/bots/${bot_id}/recording`); const formatFileSize = (bytes?: number) => { if (!bytes) return "Unknown size"; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; }; const formatDuration = (ms?: number) => { if (!ms) return "Unknown duration"; const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } }; return { content: [ { type: "text", text: [ `🎥 Recording for bot ${bot_id}:`, "", `📁 URL: ${data.url}`, `📊 Size: ${formatFileSize(data.file_size)}`, `⏱️ Duration: ${formatDuration(data.duration_ms)}`, "", "💡 This is a short-lived URL that expires after a certain time.", ].join("\n"), }, ], }; } private async sendImageToMeeting(args: Record<string, unknown>) { const bot_id = args.bot_id as string; const image_url = args.image_url as string; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } if (!image_url || typeof image_url !== 'string') { throw new Error("Missing or invalid required parameter: image_url"); } if (!image_url.startsWith('https://')) { throw new Error("Image URL must start with https://"); } await this.makeApiRequest(`/api/v1/bots/${bot_id}/output_image`, "POST", { url: image_url }); return { content: [ { type: "text", text: `✅ Image sent to meeting from bot ${bot_id}\n📷 Image URL: ${image_url}\n\n💡 The image should now be displayed in the meeting (Google Meet only)!`, }, ], }; } private async sendVideoToMeeting(args: Record<string, unknown>) { const bot_id = args.bot_id as string; const video_url = args.video_url as string; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } if (!video_url || typeof video_url !== 'string') { throw new Error("Missing or invalid required parameter: video_url"); } if (!video_url.startsWith('https://')) { throw new Error("Video URL must start with https://"); } if (!video_url.endsWith('.mp4')) { throw new Error("Video URL must end with .mp4"); } await this.makeApiRequest(`/api/v1/bots/${bot_id}/output_video`, "POST", { url: video_url }); return { content: [ { type: "text", text: `✅ Video sent to meeting from bot ${bot_id}\n🎬 Video URL: ${video_url}\n\n💡 The video should now be playing in the meeting (Google Meet only)!`, }, ], }; } private async deleteBotData(args: Record<string, unknown>) { const bot_id = args.bot_id as string; if (!bot_id || typeof bot_id !== 'string') { throw new Error("Missing or invalid required parameter: bot_id"); } const data = await this.makeApiRequest(`/api/v1/bots/${bot_id}/delete_data`, "POST"); return { content: [ { type: "text", text: [ `✅ Successfully deleted all data for bot ${bot_id}`, "", "🗑️ The following data has been permanently deleted:", "• Recording files", "• Transcript data", "• Chat messages", "• Participant information", "", "⚠️ This action cannot be undone.", "💡 Bot metadata is preserved for audit purposes.", ].join("\n"), }, ], }; } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Meeting Bot MCP Server running on stdio"); } } // Start the server if (import.meta.url === `file://${process.argv[1]}`) { const server = new MeetingBotMCP(); server.run().catch((error) => { console.error("Server error:", error); process.exit(1); }); } export default MeetingBotMCP;

Implementation Reference

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/Angad-2002/attendee-mcp'

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