Skip to main content
Glama
meeting.ts13.6 kB
/** * Meeting tool implementation */ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { z } from 'zod'; import { apiRequest, MeetingBaasClient, SessionAuth } from '../api/client.js'; import { AUDIO_FREQUENCIES, BOT_CONFIG, RECORDING_MODES, SPEECH_TO_TEXT_PROVIDERS, } from '../config.js'; import { createValidSession } from '../utils/auth.js'; import { createTool, MeetingBaaSTool } from '../utils/tool-types.js'; // Define the parameters schemas const joinMeetingParams = z.object({ meetingUrl: z.string().url().describe('URL of the meeting to join'), botName: z .string() .optional() .describe( 'Name to display for the bot in the meeting (OPTIONAL: if omitted, will use name from configuration)', ), botImage: z .string() .nullable() .optional() .describe( "URL to an image to use for the bot's avatar (OPTIONAL: if omitted, will use image from configuration)", ), entryMessage: z .string() .optional() .describe( 'Message the bot will send upon joining the meeting (OPTIONAL: if omitted, will use message from configuration)', ), deduplicationKey: z .string() .optional() .describe( 'Unique key to override the 5-minute restriction on joining the same meeting with the same API key', ), nooneJoinedTimeout: z .number() .int() .optional() .describe( 'Timeout in seconds for the bot to wait for participants to join before leaving (default: 600)', ), waitingRoomTimeout: z .number() .int() .optional() .describe( 'Timeout in seconds for the bot to wait in the waiting room before leaving (default: 600)', ), speechToTextProvider: z .enum(SPEECH_TO_TEXT_PROVIDERS) .optional() .describe('Speech-to-text provider to use for transcription (default: Default)'), speechToTextApiKey: z .string() .optional() .describe('API key for the speech-to-text provider (if required)'), streamingInputUrl: z .string() .optional() .describe('WebSocket URL to stream audio input to the bot'), streamingOutputUrl: z .string() .optional() .describe('WebSocket URL to stream audio output from the bot'), streamingAudioFrequency: z .enum(AUDIO_FREQUENCIES) .optional() .describe('Audio frequency for streaming (default: 16khz)'), reserved: z .boolean() .default(false) .describe( 'Whether to use a bot from the pool of bots or a new one (new ones are created on the fly and instances can take up to 4 minutes to boot', ), startTime: z .string() .optional() .describe( 'ISO datetime string. If provided, the bot will join at this time instead of immediately', ), recordingMode: z.enum(RECORDING_MODES).default('speaker_view').describe('Recording mode'), extra: z .record(z.string(), z.any()) .optional() .describe( 'Additional metadata for the meeting (e.g., meeting type, custom summary prompt, search keywords)', ), }); /** * Parameters for getting meeting data */ const getMeetingDetailsParams = z.object({ meetingId: z.string().describe('ID of the meeting to get data for'), }); /** * Parameters for getting meeting data with direct credentials */ const getMeetingDetailsWithCredentialsParams = z.object({ meetingId: z.string().describe('ID of the meeting to get data for'), apiKey: z.string().describe('API key for authentication'), }); const stopRecordingParams = z.object({ botId: z.string().uuid().describe('ID of the bot that recorded the meeting'), }); // Define our return types export type JoinMeetingParams = z.infer<typeof joinMeetingParams>; /** * Function to directly read the Claude Desktop config */ function readClaudeDesktopConfig(log: any) { try { // Define the expected config path const configPath = path.join( os.homedir(), 'Library/Application Support/Claude/claude_desktop_config.json', ); if (fs.existsSync(configPath)) { const configContent = fs.readFileSync(configPath, 'utf8'); const configJson = JSON.parse(configContent); // Check for meetingbaas server config if (configJson.mcpServers && configJson.mcpServers.meetingbaas) { const serverConfig = configJson.mcpServers.meetingbaas; // Check for bot configuration if (serverConfig.botConfig) { return serverConfig.botConfig; } } } return null; } catch (error) { log.error(`Error reading Claude Desktop config: ${error}`); return null; } } /** * Join a meeting */ export const joinMeetingTool: MeetingBaaSTool<typeof joinMeetingParams> = createTool( 'joinMeeting', 'Have a bot join a meeting now or schedule it for the future. Bot name, image, and entry message will use system defaults if not specified.', joinMeetingParams, async (args, context) => { const { session, log } = context; // Directly load Claude Desktop config const claudeConfig = readClaudeDesktopConfig(log); // Get bot name (user input, config, or prompt to provide) let botName = args.botName; // If no user-provided name, try Claude config, then BOT_CONFIG if (!botName) { if (claudeConfig && claudeConfig.name) { botName = claudeConfig.name; } else if (BOT_CONFIG.defaultBotName) { botName = BOT_CONFIG.defaultBotName; } } // Get bot image from various sources let botImage: string | null | undefined = args.botImage; if (botImage === undefined) { if (claudeConfig && claudeConfig.image) { botImage = claudeConfig.image; } else { botImage = BOT_CONFIG.defaultBotImage; } } // Get entry message from various sources let entryMessage = args.entryMessage; if (!entryMessage) { if (claudeConfig && claudeConfig.entryMessage) { entryMessage = claudeConfig.entryMessage; } else if (BOT_CONFIG.defaultEntryMessage) { entryMessage = BOT_CONFIG.defaultEntryMessage; } } // Get extra fields from various sources let extra = args.extra; if (!extra) { if (claudeConfig && claudeConfig.extra) { extra = claudeConfig.extra; } else if (BOT_CONFIG.defaultExtra) { extra = BOT_CONFIG.defaultExtra; } } // Only prompt for a name if no name is available from any source if (!botName) { log.info('No bot name available from any source'); return { content: [ { type: 'text' as const, text: 'Please provide a name for the bot that will join the meeting.', }, ], isError: true, }; } // Basic logging log.info('Joining meeting', { url: args.meetingUrl, botName }); // Use our utility function to get a valid session with API key const validSession = createValidSession(session, log); // Verify we have an API key if (!validSession) { log.error('Authentication failed - no API key available'); return { content: [ { type: 'text' as const, text: 'Authentication failed. Please configure your API key in Claude Desktop settings.', }, ], isError: true, }; } // Prepare API request with the meeting details const payload = { meeting_url: args.meetingUrl, bot_name: botName, bot_image: botImage, entry_message: entryMessage, deduplication_key: args.deduplicationKey, reserved: args.reserved, recording_mode: args.recordingMode, start_time: args.startTime, automatic_leave: args.nooneJoinedTimeout || args.waitingRoomTimeout ? { noone_joined_timeout: args.nooneJoinedTimeout, waiting_room_timeout: args.waitingRoomTimeout, } : undefined, speech_to_text: args.speechToTextProvider ? { provider: args.speechToTextProvider, api_key: args.speechToTextApiKey, } : undefined, streaming: args.streamingInputUrl || args.streamingOutputUrl || args.streamingAudioFrequency ? { input: args.streamingInputUrl, output: args.streamingOutputUrl, audio_frequency: args.streamingAudioFrequency, } : undefined, extra: extra, }; try { // Use the client to join the meeting with the API key from our valid session const client = new MeetingBaasClient(validSession.apiKey); const result = await client.joinMeeting(payload); // Prepare response message with details let responseMessage = `Bot named "${botName}" joined meeting successfully. Bot ID: ${result.bot_id}`; if (botImage) responseMessage += '\nCustom bot image is being used.'; if (entryMessage) responseMessage += '\nThe bot will send an entry message.'; if (args.startTime) { responseMessage += '\nThe bot is scheduled to join at the specified start time.'; } return responseMessage; } catch (error) { log.error('Failed to join meeting', { error: String(error) }); return { content: [ { type: 'text' as const, text: `Failed to join meeting: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); /** * Leave a meeting */ export const leaveMeetingTool: MeetingBaaSTool<typeof stopRecordingParams> = createTool( 'leaveMeeting', 'Have a bot leave an ongoing meeting', stopRecordingParams, async (args, context) => { const { session, log } = context; log.info('Leaving meeting', { botId: args.botId }); // Create a valid session with fallbacks for API key const validSession = createValidSession(session, log); // Check if we have a valid session with API key if (!validSession) { return { content: [ { type: 'text' as const, text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.', }, ], isError: true, }; } const response = await apiRequest(validSession, 'delete', `/bots/${args.botId}`); if (response.ok) { return 'Bot left the meeting successfully'; } else { return `Failed to make bot leave: ${response.error || 'Unknown error'}`; } }, ); /** * Get meeting metadata and recording information (without transcript details) */ export const getMeetingDataTool: MeetingBaaSTool<typeof getMeetingDetailsParams> = createTool( 'getMeetingData', 'Get recording metadata and video URL from a meeting', getMeetingDetailsParams, async (args, context) => { const { session, log } = context; log.info('Getting meeting metadata', { meetingId: args.meetingId }); // Create a valid session with fallbacks for API key const validSession = createValidSession(session, log); // Check if we have a valid session with API key if (!validSession) { return { content: [ { type: 'text' as const, text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.', }, ], isError: true, }; } const response = await apiRequest( validSession, 'get', `/bots/meeting_data?bot_id=${args.meetingId}`, ); // Format the meeting duration const duration = response.duration; const minutes = Math.floor(duration / 60); const seconds = duration % 60; // Extract the meeting name and URL const meetingName = response.bot_data.bot?.bot_name || 'Unknown Meeting'; const meetingUrl = response.bot_data.bot?.meeting_url || 'Unknown URL'; return { content: [ { type: 'text' as const, text: `Meeting: "${meetingName}"\nURL: ${meetingUrl}\nDuration: ${minutes}m ${seconds}s\n\nRecording is available at: ${response.mp4}`, }, ], isError: false, }; }, ); /** * Get meeting metadata and recording information with direct credentials */ export const getMeetingDataWithCredentialsTool: MeetingBaaSTool< typeof getMeetingDetailsWithCredentialsParams > = createTool( 'getMeetingDataWithCredentials', 'Get recording metadata and video URL from a meeting using direct API credentials', getMeetingDetailsWithCredentialsParams, async (args, context) => { const { log } = context; log.info('Getting meeting metadata with direct credentials', { meetingId: args.meetingId }); // Create a session object with the provided API key const session: SessionAuth = { apiKey: args.apiKey }; const response = await apiRequest( session, 'get', `/bots/meeting_data?bot_id=${args.meetingId}`, ); // Format the meeting duration const duration = response.duration; const minutes = Math.floor(duration / 60); const seconds = duration % 60; // Extract the meeting name and URL const meetingName = response.bot_data.bot?.bot_name || 'Unknown Meeting'; const meetingUrl = response.bot_data.bot?.meeting_url || 'Unknown URL'; return { content: [ { type: 'text' as const, text: `Meeting: "${meetingName}"\nURL: ${meetingUrl}\nDuration: ${minutes}m ${seconds}s\n\nRecording is available at: ${response.mp4}`, }, ], isError: false, }; }, );

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/Meeting-BaaS/meeting-mcp'

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