This is page 1 of 2. Use http://codebase.md/meeting-baas/meeting-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .cursor
│ └── rules
│ ├── fastmcp.mdc
│ ├── joke.mdc
│ ├── mcp.mdc
│ ├── meetingbaas.mdc
│ └── nextjs.mdc
├── .editorconfig
├── .gitignore
├── .husky
│ └── pre-commit
├── .prettierignore
├── .prettierrc
├── FORMATTING.md
├── LICENSE
├── [email protected]
├── openapi.json
├── package-lock.json
├── package.json
├── README.md
├── scripts
│ ├── cleanup_cursor_logs.sh
│ └── install-hooks.js
├── set_up_for_other_mcp.md
├── src
│ ├── api
│ │ └── client.ts
│ ├── config.ts
│ ├── index.ts
│ ├── resources
│ │ ├── index.ts
│ │ └── transcript.ts
│ ├── tools
│ │ ├── calendar.ts
│ │ ├── deleteData.ts
│ │ ├── environment.ts
│ │ ├── index.ts
│ │ ├── links.ts
│ │ ├── listBots.ts
│ │ ├── meeting.ts
│ │ ├── qrcode.ts
│ │ ├── retranscribe.ts
│ │ └── search.ts
│ ├── types
│ │ └── index.ts
│ └── utils
│ ├── auth.ts
│ ├── formatters.ts
│ ├── linkFormatter.ts
│ ├── logging.ts
│ ├── tinyDb.ts
│ └── tool-types.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | dist/
3 | coverage/
4 | .vscode/
5 | .husky/
6 | .github/
7 | *.md
8 | *.log
9 | *.min.js
```
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
```
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
```
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "trailingComma": "all",
7 | "singleQuote": true,
8 | "quoteProps": "as-needed",
9 | "bracketSpacing": true,
10 | "arrowParens": "always",
11 | "endOfLine": "lf",
12 | "bracketSameLine": false
13 | }
14 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 |
7 | # Build output
8 | dist/
9 | build/
10 |
11 | # Editor directories and files
12 | .idea/
13 | .vscode/
14 | *.suo
15 | *.ntvs*
16 | *.njsproj
17 | *.sln
18 | *.sw?
19 |
20 | # Environment variables
21 | .env
22 | .env.local
23 | .env.development.local
24 | .env.test.local
25 | .env.production.local
26 |
27 | # OS specific
28 | .DS_Store
29 | .cursor/
30 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Meeting BaaS MCP Server
2 | [](https://meetingBaaS.com)
3 |
4 | <p align="center"><a href="https://discord.com/invite/dsvFgDTr6c"><img height="60px" src="https://user-images.githubusercontent.com/31022056/158916278-4504b838-7ecb-4ab9-a900-7dc002aade78.png" alt="Join our Discord!"></a></p>
5 |
6 | A Model Context Protocol (MCP) server that provides tools for managing meeting data, including transcripts, recordings, calendar events, and search functionality.
7 |
8 | ## QUICK START: Claude Desktop Integration
9 |
10 | To use Meeting BaaS with Claude Desktop:
11 |
12 | 1. Edit the Claude Desktop configuration file:
13 | ```bash
14 | vim ~/Library/Application\ Support/Claude/claude_desktop_config.json
15 | ```
16 |
17 | 2. Add the Meeting BaaS configuration:
18 | ```json
19 | "meetingbaas": {
20 | "command": "/bin/bash",
21 | "args": [
22 | "-c",
23 | "cd /path/to/meeting-mcp && (npm run build 1>&2) && export MCP_FROM_CLAUDE=true && node dist/index.js"
24 | ],
25 | "headers": {
26 | "x-api-key": "YOUR_API_KEY"
27 | }
28 | }
29 | ```
30 |
31 | 3. For calendar integration, you can add the `calendarOAuth` section to your `botConfig`:
32 | ```json
33 | "botConfig": {
34 | "calendarOAuth": {
35 | "platform": "Google", // or "Microsoft"
36 | "clientId": "YOUR_OAUTH_CLIENT_ID",
37 | "clientSecret": "YOUR_OAUTH_CLIENT_SECRET",
38 | "refreshToken": "YOUR_REFRESH_TOKEN",
39 | "rawCalendarId": "[email protected]" // Optional
40 | }
41 | }
42 | ```
43 |
44 | 4. Save the file and restart Claude Desktop.
45 |
46 | > **Note:** Calendar integration is optional. Meeting BaaS can be used without connecting a calendar by simply omitting the `calendarOAuth` section.
47 |
48 | ## Overview
49 |
50 | This project implements a Model Context Protocol (MCP) server that allows AI assistants like Claude and Cursor to access and manipulate meeting data. It exposes a set of tools and resources that can be used to:
51 |
52 | - **Invite Meeting Bots**: Create and invite bots to your video conferences that automatically record and transcribe meetings
53 |
54 | ```
55 | "Create a new meeting bot for my Zoom call tomorrow"
56 | ```
57 |
58 | - **Query Meeting Data**: Search through meeting transcripts and find specific information without watching entire recordings
59 |
60 | ```
61 | "Search my recent meetings for discussions about the quarterly budget"
62 | "Find all mentions of Project Apollo in yesterday's team meeting"
63 | "Show me parts of the meeting where Jane was speaking"
64 | ```
65 |
66 | - **Manage Calendar Events**: View and organize calendar entries and upcoming meetings
67 |
68 | - **Access Recording Information**: Get metadata about meeting recordings and their status
69 |
70 | ## Prerequisites
71 |
72 | - Node.js (v16 or later)
73 | - npm
74 | - **MeetingBaaS Account**: You need access to a MeetingBaaS account using your corporate email address
75 | - All logs, bots, and shared links are available to colleagues with the same corporate domain (not personal emails like gmail.com)
76 | - This enables seamless collaboration where all team members can access meeting recordings and transcripts created by anyone in your organization
77 |
78 | ## Installation
79 |
80 | 1. Clone the repository:
81 |
82 | ```bash
83 | git clone <repository-url>
84 | cd mcp-baas
85 | ```
86 |
87 | 2. Install dependencies:
88 |
89 | ```bash
90 | npm install
91 | ```
92 |
93 | 3. Build the project:
94 | ```bash
95 | npm run build
96 | ```
97 |
98 | ## Usage
99 |
100 | Start the server:
101 |
102 | ```bash
103 | npm run start
104 | ```
105 |
106 | By default, the server runs on port 7017 and exposes the MCP endpoint at `http://localhost:7017/mcp`.
107 |
108 | ## Available Tools
109 |
110 | The server exposes several tools through the MCP protocol:
111 |
112 | ### Calendar Tools
113 |
114 | - `oauthGuidance`: Get detailed step-by-step instructions on setting up OAuth for Google or Microsoft calendars
115 | - No parameters required
116 | - Returns comprehensive instructions for obtaining OAuth credentials and setting up calendar integration
117 |
118 | - `listRawCalendars`: Lists available calendars from Google or Microsoft before integration
119 | - Parameters: `platform` ("Google" or "Microsoft"), `clientId`, `clientSecret`, `refreshToken`
120 | - Returns a list of available calendars with their IDs and primary status
121 |
122 | - `setupCalendarOAuth`: Integrates a calendar using OAuth credentials
123 | - Parameters: `platform` ("Google" or "Microsoft"), `clientId`, `clientSecret`, `refreshToken`, `rawCalendarId` (optional)
124 | - Returns confirmation of successful integration with calendar details
125 |
126 | - `listCalendars`: Lists all integrated calendars
127 | - No parameters required
128 | - Returns a list of all calendars with their names, email addresses, and UUIDs
129 |
130 | - `getCalendar`: Gets detailed information about a specific calendar integration
131 | - Parameters: `calendarId` (UUID of the calendar)
132 | - Returns comprehensive calendar details
133 |
134 | - `deleteCalendar`: Permanently removes a calendar integration
135 | - Parameters: `calendarId` (UUID of the calendar)
136 | - Returns confirmation of successful deletion
137 |
138 | - `resyncAllCalendars`: Forces a refresh of all connected calendars
139 | - No parameters required
140 | - Returns the status of the sync operation
141 |
142 | - `listUpcomingMeetings`: Lists upcoming meetings from a calendar
143 | - Parameters: `calendarId`, `status` (optional: "upcoming", "past", "all"), `limit` (optional)
144 | - Returns a list of meetings with their names, times, and recording status
145 |
146 | - `listEvents`: Lists calendar events with comprehensive filtering options
147 | - Parameters: `calendarId`, plus optional filters like `startDateGte`, `startDateLte`, `attendeeEmail`, etc.
148 | - Returns detailed event listings with rich information
149 |
150 | - `listEventsWithCredentials`: Lists calendar events with credentials provided directly in the query
151 | - Parameters: `calendarId`, `apiKey`, plus same optional filters as `listEvents`
152 | - Returns the same detailed information as `listEvents` but with direct authentication
153 |
154 | - `getEvent`: Gets detailed information about a specific calendar event
155 | - Parameters: `eventId` (UUID of the event)
156 | - Returns comprehensive event details including attendees and recording status
157 |
158 | - `scheduleRecording`: Schedules a bot to record an upcoming meeting
159 | - Parameters: `eventId`, `botName`, plus optional settings like `botImage`, `recordingMode`, etc.
160 | - Returns confirmation of successful scheduling
161 |
162 | - `scheduleRecordingWithCredentials`: Schedules recording with credentials provided directly in the query
163 | - Parameters: `eventId`, `apiKey`, `botName`, plus same optional settings as `scheduleRecording`
164 | - Returns confirmation of successful scheduling
165 |
166 | - `cancelRecording`: Cancels a previously scheduled recording
167 | - Parameters: `eventId`, `allOccurrences` (optional, for recurring events)
168 | - Returns confirmation of successful cancellation
169 |
170 | - `cancelRecordingWithCredentials`: Cancels recording with credentials provided directly in the query
171 | - Parameters: `eventId`, `apiKey`, `allOccurrences` (optional)
172 | - Returns confirmation of successful cancellation
173 |
174 | - `checkCalendarIntegration`: Checks and diagnoses calendar integration status
175 | - No parameters required
176 | - Returns a comprehensive status report and troubleshooting tips
177 |
178 | ### Meeting Tools
179 |
180 | - `createBot`: Creates a meeting bot that can join video conferences to record and transcribe meetings
181 | - Parameters:
182 | - `meeting_url` (URL of the meeting to join)
183 | - `name` (optional bot name)
184 | - `botImage` (optional URL to an image for the bot's avatar)
185 | - `entryMessage` (optional message the bot will send when joining)
186 | - `deduplicationKey` (optional key to override the 5-minute restriction on joining the same meeting)
187 | - `nooneJoinedTimeout` (optional timeout in seconds for bot to leave if no one joins)
188 | - `waitingRoomTimeout` (optional timeout in seconds for bot to leave if stuck in waiting room)
189 | - `speechToTextProvider` (optional provider for transcription: "Gladia", "Runpod", or "Default")
190 | - `speechToTextApiKey` (optional API key for the speech-to-text provider)
191 | - `streamingInputUrl` (optional WebSocket URL to stream audio input)
192 | - `streamingOutputUrl` (optional WebSocket URL to stream audio output)
193 | - `streamingAudioFrequency` (optional frequency for streaming: "16khz" or "24khz")
194 | - `extra` (optional object with additional metadata about the meeting, such as meeting type, custom summary prompt, search keywords)
195 | - Returns: Bot details including ID and join status
196 | - `getBots`: Lists all bots and their associated meetings
197 | - `getBotsByMeeting`: Gets bots for a specific meeting URL
198 | - `getRecording`: Retrieves recording information for a specific bot/meeting
199 | - `getRecordingStatus`: Checks the status of a recording in progress
200 | - `getMeetingData`: Gets transcript and recording data for a specific meeting
201 | - Parameters: `meetingId` (ID of the meeting to get data for)
202 | - Returns: Information about the meeting recording including duration and transcript segment count
203 | - `getMeetingDataWithCredentials`: Gets transcript and recording data using direct API credentials
204 | - Parameters: `meetingId` (ID of the meeting), `apiKey` (API key for authentication)
205 | - Returns: Same information as `getMeetingData` but with direct authentication
206 |
207 | ### Transcript Tools
208 |
209 | - `getMeetingTranscript`: Gets a meeting transcript with speaker names and content grouped by speaker
210 | - Parameters: `botId` (the bot that recorded the meeting)
211 | - Returns: Complete transcript with speaker information, formatted as paragraphs grouped by speaker
212 | - Example output:
213 | ```
214 | Meeting: "Weekly Team Meeting"
215 | Duration: 45m 30s
216 | Transcript:
217 |
218 | John Smith: Hello everyone, thanks for joining today's call. We have a lot to cover regarding the Q3 roadmap and our current progress on the platform redesign.
219 |
220 | Sarah Johnson: Thanks John. I've prepared some slides about the user testing results we got back yesterday. The feedback was generally positive but there are a few areas we need to address.
221 | ```
222 |
223 | - `findKeyMoments`: Automatically identifies and shares links to important moments in a meeting
224 | - Parameters: `botId`, optional `meetingTitle`, optional list of `topics` to look for, and optional `maxMoments`
225 | - Returns: Markdown-formatted list of key moments with links, automatically detected based on transcript
226 | - Uses AI-powered analysis to find significant moments without requiring manual timestamp selection
227 |
228 | ### QR Code Tools
229 |
230 | - `generateQRCode`: Creates an AI-generated QR code image that can be used as a bot avatar
231 | - Parameters:
232 | - `type`: Type of QR code (url, email, phone, sms, text)
233 | - `to`: Destination for the QR code (URL, email, phone number, or text)
234 | - `prompt`: AI prompt to customize the QR code (max 1000 characters). You can include your API key directly in the prompt text by typing "API key: qrc_your_key" or similar phrases.
235 | - `style`: Style of the QR code (style_default, style_dots, style_rounded, style_crystal)
236 | - `useAsBotImage`: Whether to use the generated QR code as the bot avatar (default: true)
237 | - `template`: Template ID for the QR code (optional)
238 | - `apiKey`: Your QR Code AI API key (optional, will use default if not provided)
239 | - Returns: URL to the generated QR code image that can be used directly with the joinMeeting tool
240 | - Example usage:
241 | ```
242 | "Generate a QR code with my email [email protected] that looks like a Tiger in crystal style"
243 | ```
244 | - Example with API key in the prompt:
245 | ```
246 | "Generate a QR code for my website https://example.com that looks like a mountain landscape. Use API key: qrc_my-personal-api-key-123456"
247 | ```
248 | - Example with formal parameter:
249 | ```
250 | "Generate a QR code with the following parameters:
251 | - Type: email
252 | - To: [email protected]
253 | - Prompt: Create a QR code that looks like a mountain landscape
254 | - Style: style_rounded
255 | - API Key: qrc_my-personal-api-key-123456"
256 | ```
257 |
258 | ### Link Sharing Tools
259 |
260 | - `shareableMeetingLink`: Generates a nicely formatted, shareable link to a meeting recording
261 | - Parameters: `botId`, plus optional `timestamp`, `title`, `speakerName`, and `description`
262 | - Returns: Markdown-formatted link with metadata that can be shared directly in chat
263 | - Example:
264 | ```
265 | 📽️ **Meeting Recording: Weekly Team Sync**
266 | ⏱️ Timestamp: 00:12:35
267 | 🎤 Speaker: Sarah Johnson
268 | 📝 Discussing the new product roadmap
269 |
270 | 🔗 [View Recording](https://meetingbaas.com/viewer/abc123?t=755)
271 | ```
272 |
273 | - `shareMeetingSegments`: Creates a list of links to multiple important moments in a meeting
274 | - Parameters: `botId` and an array of `segments` with timestamps, speakers, and descriptions
275 | - Returns: Markdown-formatted list of segments with direct links to each moment
276 | - Useful for creating a table of contents for a long meeting
277 |
278 | ## Example Workflows
279 |
280 | ### Recording a Meeting
281 |
282 | 1. Create a bot for your upcoming meeting:
283 |
284 | ```
285 | "Create a bot for my Zoom meeting at https://zoom.us/j/123456789"
286 | ```
287 |
288 | 2. The bot joins the meeting automatically and begins recording.
289 |
290 | 3. Check recording status:
291 | ```
292 | "What's the status of my meeting recording for the Zoom call I started earlier?"
293 | ```
294 |
295 | ### Calendar Integration and Automatic Recording
296 |
297 | 1. Get guidance on obtaining OAuth credentials:
298 |
299 | ```
300 | "I want to integrate my Google Calendar. How do I get OAuth credentials?"
301 | ```
302 |
303 | 2. List your available calendars before integration:
304 |
305 | ```
306 | "List my available Google calendars. Here are my OAuth credentials:
307 | - Client ID: my-client-id-123456789.apps.googleusercontent.com
308 | - Client Secret: my-client-secret-ABCDEF123456
309 | - Refresh Token: my-refresh-token-ABCDEF123456789"
310 | ```
311 |
312 | 3. Set up calendar integration with a specific calendar:
313 |
314 | ```
315 | "Integrate my Google Calendar using these credentials:
316 | - Platform: Google
317 | - Client ID: my-client-id-123456789.apps.googleusercontent.com
318 | - Client Secret: my-client-secret-ABCDEF123456
319 | - Refresh Token: my-refresh-token-ABCDEF123456789
320 | - Raw Calendar ID: [email protected]"
321 | ```
322 |
323 | 4. View your upcoming meetings:
324 |
325 | ```
326 | "Show me my upcoming meetings from calendar 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
327 | ```
328 |
329 | 5. Schedule recording for an upcoming meeting:
330 |
331 | ```
332 | "Schedule a recording for my team meeting with event ID 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d.
333 | Configure the bot with:
334 | - Name: Team Meeting Bot
335 | - Recording Mode: gallery_view
336 | - Entry Message: Hello everyone, I'm here to record the meeting"
337 | ```
338 |
339 | 6. Check all recordings scheduled in your calendar:
340 |
341 | ```
342 | "Show me all meetings in my calendar that have recordings scheduled"
343 | ```
344 |
345 | 7. Cancel a previously scheduled recording:
346 |
347 | ```
348 | "Cancel the recording for event 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
349 | ```
350 |
351 | 8. Refresh calendar data if meetings are missing:
352 |
353 | ```
354 | "Force a resync of all my connected calendars"
355 | ```
356 |
357 | ### Analyzing Meeting Content
358 |
359 | 1. Get the full transcript of a meeting:
360 |
361 | ```
362 | "Get the transcript from my team meeting with bot ID abc-123"
363 | ```
364 |
365 | 2. Find key moments in a meeting:
366 |
367 | ```
368 | "Identify key moments from yesterday's product planning meeting with bot ID xyz-456"
369 | ```
370 |
371 | 3. Share a specific moment from a meeting:
372 |
373 | ```
374 | "Create a shareable link to the part of meeting abc-123 at timestamp 12:45 where John was talking about the budget"
375 | ```
376 |
377 | ### Using Direct Credential Tools
378 |
379 | You can provide API credentials directly in your queries:
380 |
381 | 1. List events with direct credentials:
382 |
383 | ```
384 | "List events from calendar 5c99f8a4-f498-40d0-88f0-29f698c53c51 using API key tesban where attendee is [email protected]"
385 | ```
386 |
387 | 2. Schedule a recording with direct credentials:
388 |
389 | ```
390 | "Schedule a recording for event 78d06b42-794f-4efe-8195-62db1f0052d5 using API key tesban with bot name 'Weekly Meeting Bot'"
391 | ```
392 |
393 | 3. Cancel a recording with direct credentials:
394 |
395 | ```
396 | "Cancel the recording for event 97cd62f0-ea9b-42b3-add5-7a607ce6d80f using API key tesban"
397 | ```
398 |
399 | 4. Get meeting data with direct credentials:
400 |
401 | ```
402 | "Get meeting data for meeting 47de9462-bea7-406c-b79a-fd6b82c3de76 using API key tesban"
403 | ```
404 |
405 | ### Using AI-Generated QR Codes as Bot Avatars
406 |
407 | 1. Generate a QR code with your contact information and a custom design:
408 |
409 | ```
410 | "Generate a QR code with the following parameters:
411 | - Type: email
412 | - To: [email protected]
413 | - Prompt: Create a professional-looking QR code with abstract blue patterns that resemble a corporate logo
414 | - Style: style_crystal"
415 | ```
416 |
417 | 2. Use the generated QR code as a bot avatar in a meeting:
418 |
419 | ```
420 | "Join my Zoom meeting at https://zoom.us/j/123456789 with the following parameters:
421 | - Bot name: QR Code Assistant
422 | - Bot image: [URL from the generated QR code]
423 | - Entry message: Hello everyone, I'm here to record the meeting. You can scan my avatar to get my contact information."
424 | ```
425 |
426 | 3. Generate a QR code with a meeting link for easy sharing:
427 |
428 | ```
429 | "Generate a QR code with the following parameters:
430 | - Type: url
431 | - To: https://zoom.us/j/123456789
432 | - Prompt: Create a colorful QR code with a calendar icon in the center
433 | - Style: style_rounded"
434 | ```
435 |
436 | ### Accessing Meeting Recordings
437 |
438 | Meeting recordings can be accessed directly through the Meeting BaaS viewer using the bot ID:
439 |
440 | ```
441 | https://meetingbaas.com/viewer/{BOT_ID}
442 | ```
443 |
444 | For example:
445 | ```
446 | https://meetingbaas.com/viewer/67738f48-2360-4f9e-a999-275a74208ff5
447 | ```
448 |
449 | This viewer provides:
450 | - The meeting video recording
451 | - Synchronized transcript with speaker identification
452 | - Navigation by speaker or topic
453 | - Direct link sharing with teammates
454 |
455 | When using the `createBot`, `getBots`, or search tools, you'll receive bot IDs that can be used to construct these viewer URLs for easy access to recordings.
456 |
457 | > **Important**: All meeting recordings and links are automatically shared with colleagues who have the same corporate email domain (e.g., @yourcompany.com). This allows your entire team to access recordings without requiring individual permissions, creating a collaborative environment where meeting knowledge is accessible to everyone in your organization.
458 |
459 | ## Configuration
460 |
461 | The server can be configured through environment variables or by editing the `src/config.ts` file.
462 |
463 | Key configuration options:
464 |
465 | - `PORT`: The port the server listens on (default: 7017)
466 | - `API_BASE_URL`: The base URL for the Meeting BaaS API
467 | - `DEFAULT_API_KEY`: Default API key for testing
468 |
469 | ## Integration with Cursor
470 |
471 | To integrate with Cursor:
472 |
473 | 1. Open Cursor
474 | 2. Go to Settings
475 | 3. Navigate to "Model Context Protocol"
476 | 4. Add a new server with:
477 | - Name: "Meeting BaaS MCP"
478 | - Type: "sse"
479 | - Server URL: "http://localhost:7017/mcp"
480 | - Optionally add headers if authentication is required
481 |
482 | ## Development
483 |
484 | ### Build
485 |
486 | ```bash
487 | npm run build
488 | ```
489 |
490 | ### Test with MCP Inspector
491 |
492 | ```bash
493 | npm run inspect
494 | ```
495 |
496 | ### Development mode (with auto-reload)
497 |
498 | ```bash
499 | npm run dev
500 | ```
501 |
502 | ### Log Management
503 |
504 | The server includes optimized logging with:
505 |
506 | ```bash
507 | npm run cleanup
508 | ```
509 |
510 | This command:
511 | - Cleans up unnecessary log files and cached data
512 | - Filters out repetitive ping messages from logs
513 | - Reduces disk usage while preserving important log information
514 | - Maintains a smaller log footprint for long-running servers
515 |
516 | ## Project Structure
517 |
518 | - `src/index.ts`: Main entry point
519 | - `src/tools/`: Tool implementations
520 | - `src/resources/`: Resource definitions
521 | - `src/api/`: API client for the Meeting BaaS backend
522 | - `src/types/`: TypeScript type definitions
523 | - `src/config.ts`: Server configuration
524 | - `src/utils/`: Utility functions
525 | - `logging.ts`: Log filtering and management
526 | - `tinyDb.ts`: Persistent bot tracking database
527 |
528 | ## Authentication
529 |
530 | The server expects an API key in the `x-api-key` header for authentication. You can configure the default API key in the configuration.
531 |
532 | Direct authentication is also supported in many tools (named with "WithCredentials") where you can provide the API key directly as a parameter rather than in headers.
533 |
534 | ## License
535 |
536 | [MIT](LICENSE)
537 |
538 | ## QR Code API Key Configuration
539 |
540 | The QR code generator tool requires an API key from QR Code AI API. There are several ways to provide this:
541 |
542 | 1. **Directly in the prompt**: Include your API key directly in the prompt text when using the `generateQRCode` tool, e.g., "Generate a QR code for my website https://example.com with API key: qrc_your_key"
543 |
544 | 2. **As a parameter**: Provide your API key as the `apiKey` parameter when using the `generateQRCode` tool
545 |
546 | 3. **Environment variable**: Set the `QRCODE_API_KEY` environment variable
547 |
548 | 4. **Claude Desktop config**: Add the API key to your Claude Desktop configuration file located at:
549 | - Mac/Linux: `~/Library/Application Support/Claude/claude_desktop_config.json`
550 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
551 |
552 | Example configuration:
553 | ```json
554 | {
555 | "headers": {
556 | "x-api-key": "qrc_your_key_here"
557 | }
558 | }
559 | ```
560 |
561 | The tool will check for the API key in the order listed above. If no API key is provided, the default API key will be used if available.
562 |
563 | You can obtain an API key by signing up at [QR Code AI API](https://qrcode-ai.com).
564 |
565 |
566 |
```
--------------------------------------------------------------------------------
/src/resources/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Export all MCP resources
3 | */
4 |
5 | export * from "./transcript.js";
6 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "esModuleInterop": true,
7 | "outDir": "./dist",
8 | "strict": true
9 | },
10 | "include": ["src/**/*"]
11 | }
12 |
```
--------------------------------------------------------------------------------
/src/utils/formatters.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Formatting utility functions
3 | */
4 |
5 | /**
6 | * Format seconds to MM:SS format
7 | */
8 | export function formatTime(seconds: number): string {
9 | const minutes = Math.floor(seconds / 60);
10 | const remainingSeconds = Math.floor(seconds % 60);
11 | return `${minutes.toString().padStart(2, "0")}:${remainingSeconds
12 | .toString()
13 | .padStart(2, "0")}`;
14 | }
15 |
16 | /**
17 | * Format seconds to human-readable duration
18 | */
19 | export function formatDuration(seconds: number): string {
20 | const minutes = Math.floor(seconds / 60);
21 | const remainingSeconds = Math.floor(seconds % 60);
22 | return `${minutes}m ${remainingSeconds}s`;
23 | }
24 |
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Export all MCP tools
3 | */
4 |
5 | // Meeting tools
6 | export * from './meeting.js';
7 | export { getMeetingDataWithCredentialsTool } from './meeting.js';
8 |
9 | // Simplified transcript tool
10 | export { getTranscriptTool } from './search.js';
11 |
12 | // Calendar tools
13 | export * from './calendar.js';
14 |
15 | // Link sharing tools
16 | export * from './links.js';
17 |
18 | // Bot management tools
19 | export * from './deleteData.js';
20 | export * from './listBots.js';
21 | export { retranscribeTool } from './retranscribe.js';
22 |
23 | // Environment tools
24 | export * from './environment.js';
25 |
26 | // QR code generation tool
27 | export { generateQRCodeTool } from './qrcode.js';
28 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "meetingbaas-mcp",
3 | "version": "1.0.0",
4 | "description": "MCP server for Meeting BaaS API",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "scripts": {
8 | "build": "tsc",
9 | "start": "npm run build && node dist/index.js",
10 | "dev": "ts-node --esm src/index.ts",
11 | "inspect": "fastmcp inspect src/index.ts",
12 | "cleanup": "bash scripts/cleanup_cursor_logs.sh",
13 | "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
14 | "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
15 | "prepare": "husky install"
16 | },
17 | "dependencies": {
18 | "axios": "^1.6.0",
19 | "fastmcp": "^1.20.2",
20 | "zod": "^3.22.4"
21 | },
22 | "devDependencies": {
23 | "husky": "^8.0.3",
24 | "lint-staged": "^15.2.0",
25 | "prettier": "^3.1.0",
26 | "ts-node": "^10.9.1",
27 | "typescript": "^5.2.2"
28 | },
29 | "lint-staged": {
30 | "**/*.{ts,tsx,js,jsx,json,md}": "prettier --write"
31 | }
32 | }
33 |
```
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Type definitions for the MeetingBaaS MCP server
3 | */
4 |
5 | // Session data type
6 | export interface SessionData {
7 | apiKey: string;
8 | }
9 |
10 | // Transcript type
11 | export interface Transcript {
12 | speaker: string;
13 | start_time: number;
14 | words: { text: string }[];
15 | }
16 |
17 | // Meeting bot type
18 | export interface Bot {
19 | bot_id: string;
20 | bot_name: string;
21 | meeting_url: string;
22 | created_at: string;
23 | ended_at: string | null;
24 | }
25 |
26 | // Calendar event type
27 | export interface CalendarEvent {
28 | uuid: string;
29 | name: string;
30 | start_time: string;
31 | end_time: string;
32 | deleted: boolean;
33 | bot_param: unknown;
34 | meeting_url?: string;
35 | attendees?: Array<{
36 | name?: string;
37 | email: string;
38 | }>;
39 | calendar_uuid: string;
40 | google_id: string;
41 | is_organizer: boolean;
42 | is_recurring: boolean;
43 | last_updated_at: string;
44 | raw: Record<string, any>;
45 | recurring_event_id?: string | null;
46 | }
47 |
48 | // Calendar type
49 | export interface Calendar {
50 | uuid: string;
51 | name: string;
52 | email: string;
53 | }
54 |
```
--------------------------------------------------------------------------------
/src/utils/tool-types.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Type definitions for FastMCP Tools that properly handle session auth
3 | */
4 |
5 | import { z } from "zod";
6 | import type { Context, TextContent, ImageContent, ContentResult } from "fastmcp";
7 | import type { SessionAuth } from "../api/client.js";
8 |
9 | /**
10 | * Proper tool type definition that satisfies FastMCP's constraints
11 | *
12 | * This creates a type-safe wrapper for tools that ensures they're compatible
13 | * with SessionAuth while still allowing them to use their own parameter schemas
14 | */
15 | export interface MeetingBaaSTool<P extends z.ZodType> {
16 | name: string;
17 | description: string;
18 | parameters: P;
19 | execute: (
20 | args: z.infer<P>,
21 | context: Context<SessionAuth>
22 | ) => Promise<string | ContentResult | TextContent | ImageContent>;
23 | }
24 |
25 | /**
26 | * Helper function to create a properly typed tool that works with FastMCP and SessionAuth
27 | *
28 | * @param name Tool name
29 | * @param description Tool description
30 | * @param parameters Zod schema for tool parameters
31 | * @param execute Function that executes the tool
32 | * @returns A properly typed tool compatible with FastMCP SessionAuth
33 | */
34 | export function createTool<P extends z.ZodType>(
35 | name: string,
36 | description: string,
37 | parameters: P,
38 | execute: (
39 | args: z.infer<P>,
40 | context: Context<SessionAuth>
41 | ) => Promise<string | ContentResult | TextContent | ImageContent>
42 | ): MeetingBaaSTool<P> {
43 | return {
44 | name,
45 | description,
46 | parameters,
47 | execute
48 | };
49 | }
```
--------------------------------------------------------------------------------
/src/utils/logging.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Logging utilities for the MCP server
3 | */
4 |
5 | // Store the original console methods
6 | const originalConsoleError = console.error;
7 |
8 | // Define our ping filter
9 | function isPingMessage(message: string): boolean {
10 | // Skip ping/pong messages to reduce log noise
11 | return (
12 | (typeof message === 'string') && (
13 | message.includes('"method":"ping"') ||
14 | (message.includes('"result":{}') && message.includes('"jsonrpc":"2.0"') && message.includes('"id":'))
15 | )
16 | );
17 | }
18 |
19 | /**
20 | * Patches the console to filter out ping messages
21 | */
22 | export function setupPingFiltering(): void {
23 | // Replace console.error with our filtered version
24 | console.error = function(...args: any[]) {
25 | // Check if this is a ping message we want to filter
26 | const firstArg = args[0];
27 |
28 | if (typeof firstArg === 'string' &&
29 | (firstArg.includes('[meetingbaas]') || firstArg.includes('[MCP Server]'))) {
30 | // This is a log message from our server
31 | const messageContent = args.join(' ');
32 |
33 | // Skip ping messages to reduce log size
34 | if (isPingMessage(messageContent)) {
35 | return; // Don't log ping messages
36 | }
37 | }
38 |
39 | // For all other messages, pass through to the original
40 | originalConsoleError.apply(console, args);
41 | };
42 | }
43 |
44 | /**
45 | * Create standard server logger
46 | */
47 | export function createServerLogger(prefix: string): (message: string) => void {
48 | return (message: string) => {
49 | console.error(`[${prefix}] ${message}`);
50 | };
51 | }
```
--------------------------------------------------------------------------------
/FORMATTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Code Formatting Guidelines
2 |
3 | This project uses Prettier for automatic code formatting. This ensures consistent code style across the codebase and prevents code review discussions about formatting.
4 |
5 | ## Setup
6 |
7 | The formatting tools are automatically installed when you run `npm install`. Pre-commit hooks are also set up to format code before committing.
8 |
9 | ## VS Code Integration
10 |
11 | If you're using VS Code, the repository includes settings to automatically format code on save. You'll need to install the Prettier extension:
12 |
13 | 1. Open VS Code
14 | 2. Go to Extensions (Ctrl+Shift+X or Cmd+Shift+X)
15 | 3. Search for "Prettier - Code formatter"
16 | 4. Install the extension by Prettier
17 |
18 | The workspace settings will automatically enable format-on-save.
19 |
20 | ## Manual Formatting
21 |
22 | You can manually format code using these npm scripts:
23 |
24 | - `npm run format` - Format all code files
25 | - `npm run format:check` - Check if all files are formatted correctly (useful for CI)
26 |
27 | ## Formatting Configuration
28 |
29 | The formatting rules are defined in `.prettierrc` at the root of the project. Here are the key settings:
30 |
31 | - Double quotes for strings
32 | - 2 spaces for indentation
33 | - Maximum line length of 100 characters
34 | - Trailing commas in objects and arrays (ES5 compatible)
35 | - No semicolons at the end of statements
36 |
37 | ## Pre-commit Hook
38 |
39 | A pre-commit hook automatically formats your code before committing. If for some reason this doesn't work, you can reinstall the hooks with:
40 |
41 | ```
42 | npm run setup-hooks
43 | ```
44 |
45 | ## Ignoring Files
46 |
47 | If you need to exclude specific files from formatting, add them to `.prettierignore`.
```
--------------------------------------------------------------------------------
/scripts/install-hooks.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Script to install pre-commit hooks for code formatting
5 | *
6 | * This sets up a Git hook to automatically format your code
7 | * before committing using Prettier.
8 | */
9 |
10 | import { exec } from 'child_process';
11 | import { promises as fs } from 'fs';
12 | import path from 'path';
13 | import { fileURLToPath } from 'url';
14 |
15 | // Get the directory of the current script
16 | const __filename = fileURLToPath(import.meta.url);
17 | const __dirname = path.dirname(__filename);
18 |
19 | // Path to Git hooks directory
20 | const gitHooksPath = path.resolve(__dirname, '../.git/hooks');
21 | const preCommitPath = path.join(gitHooksPath, 'pre-commit');
22 |
23 | // Pre-commit hook script content
24 | const preCommitScript = `#!/bin/sh
25 | # Pre-commit hook to format code with Prettier
26 |
27 | # Get all staged files
28 | FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E "\\.(js|ts|jsx|tsx|json)$")
29 |
30 | if [ -n "$FILES" ]; then
31 | echo "🔍 Formatting staged files with Prettier..."
32 | npx prettier --write $FILES
33 | # Add the formatted files back to staging
34 | git add $FILES
35 | echo "✅ Formatting complete"
36 | fi
37 | `;
38 |
39 | async function installHooks() {
40 | try {
41 | // Check if .git directory exists
42 | try {
43 | await fs.access(path.resolve(__dirname, '../.git'));
44 | } catch (error) {
45 | console.error('❌ No .git directory found. Are you in a Git repository?');
46 | process.exit(1);
47 | }
48 |
49 | // Ensure hooks directory exists
50 | try {
51 | await fs.access(gitHooksPath);
52 | } catch (error) {
53 | await fs.mkdir(gitHooksPath, { recursive: true });
54 | console.log(`📁 Created hooks directory: ${gitHooksPath}`);
55 | }
56 |
57 | // Write pre-commit hook
58 | await fs.writeFile(preCommitPath, preCommitScript, 'utf8');
59 |
60 | // Make it executable
61 | await new Promise((resolve, reject) => {
62 | exec(`chmod +x ${preCommitPath}`, (error) => {
63 | if (error) {
64 | reject(error);
65 | return;
66 | }
67 | resolve();
68 | });
69 | });
70 |
71 | console.log('✅ Pre-commit hook installed successfully');
72 | } catch (error) {
73 | console.error('❌ Error installing hooks:', error);
74 | process.exit(1);
75 | }
76 | }
77 |
78 | // Execute the function
79 | installHooks().catch(console.error);
80 |
```
--------------------------------------------------------------------------------
/src/tools/environment.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Environment selection tool
3 | *
4 | * Allows switching between different API environments (gmeetbot, preprod, prod)
5 | */
6 |
7 | import { z } from 'zod';
8 | import { Environment, getApiBaseUrl, setEnvironment } from '../config.js';
9 | import { createValidSession } from '../utils/auth.js';
10 | import { createServerLogger } from '../utils/logging.js';
11 | import { createTool } from '../utils/tool-types.js';
12 |
13 | const logger = createServerLogger('Environment Tool');
14 |
15 | // Define the schema for the environment selection tool parameters
16 | const environmentSelectionSchema = z.object({
17 | environment: z
18 | .enum(['gmeetbot', 'preprod', 'prod', 'local'])
19 | .describe('The environment to use (gmeetbot, preprod, prod, or local)'),
20 | });
21 |
22 | // Create the environment selection tool using the helper function
23 | export const selectEnvironmentTool = createTool(
24 | 'select_environment',
25 | 'Select which environment (API endpoint) to use for all MeetingBaaS operations',
26 | environmentSelectionSchema,
27 | async (input, context) => {
28 | const { session, log } = context;
29 |
30 | try {
31 | // Create a valid session with fallbacks for API key
32 | const validSession = createValidSession(session, log);
33 |
34 | // Check if we have a valid session with API key
35 | if (!validSession) {
36 | return {
37 | content: [
38 | {
39 | type: 'text' as const,
40 | text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
41 | },
42 | ],
43 | isError: true,
44 | };
45 | }
46 |
47 | // Set the environment
48 | setEnvironment(input.environment as Environment);
49 |
50 | // Get the current API base URL to include in the response
51 | const apiBaseUrl = getApiBaseUrl();
52 |
53 | logger(`Environment switched to: ${input.environment} (${apiBaseUrl})`);
54 |
55 | return `Environment set to ${input.environment} (${apiBaseUrl})`;
56 | } catch (error) {
57 | logger(`Error setting environment: ${error}`);
58 | return {
59 | content: [
60 | {
61 | type: 'text' as const,
62 | text: `Failed to set environment: ${error instanceof Error ? error.message : String(error)}`,
63 | },
64 | ],
65 | isError: true,
66 | };
67 | }
68 | },
69 | );
70 |
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Configuration and constants for the MeetingBaaS MCP server
3 | */
4 |
5 | // Environment configuration
6 | export type Environment = 'gmeetbot' | 'preprod' | 'prod' | 'local';
7 |
8 | // Current active environment (default to prod)
9 | let currentEnvironment: Environment = 'prod';
10 |
11 | // API base URLs for different environments
12 | const API_URLS = {
13 | gmeetbot: 'https://api.gmeetbot.com',
14 | preprod: 'https://api.pre-prod-meetingbaas.com',
15 | prod: 'https://api.meetingbaas.com',
16 | local: 'http://localhost:3001',
17 | };
18 |
19 | // Get current API base URL based on the active environment
20 | export const getApiBaseUrl = (): string => {
21 | return API_URLS[currentEnvironment];
22 | };
23 |
24 | // Set the active environment
25 | export const setEnvironment = (env: Environment): void => {
26 | currentEnvironment = env;
27 | console.error(`[MCP Server] Environment switched to: ${env} (${API_URLS[env]})`);
28 | };
29 |
30 | // For backward compatibility and direct access
31 | export const API_BASE_URL = API_URLS[currentEnvironment];
32 |
33 | // Server configuration
34 | export const SERVER_CONFIG = {
35 | name: 'Meeting BaaS MCP',
36 | version: '1.0.0',
37 | port: 7017,
38 | endpoint: '/mcp',
39 | };
40 |
41 | // Bot configuration from environment variables (set in index.ts when loading Claude Desktop config)
42 | export const BOT_CONFIG = {
43 | // Default bot name displayed in meetings
44 | defaultBotName: process.env.MEETING_BOT_NAME || null,
45 | // Default bot image URL
46 | defaultBotImage: process.env.MEETING_BOT_IMAGE || null,
47 | // Default bot entry message
48 | defaultEntryMessage: process.env.MEETING_BOT_ENTRY_MESSAGE || null,
49 | // Default extra metadata
50 | defaultExtra: process.env.MEETING_BOT_EXTRA ? JSON.parse(process.env.MEETING_BOT_EXTRA) : null,
51 | };
52 |
53 | // Log bot configuration at startup
54 | if (
55 | BOT_CONFIG.defaultBotName ||
56 | BOT_CONFIG.defaultBotImage ||
57 | BOT_CONFIG.defaultEntryMessage ||
58 | BOT_CONFIG.defaultExtra
59 | ) {
60 | console.error(
61 | '[MCP Server] Bot configuration loaded:',
62 | BOT_CONFIG.defaultBotName ? `name="${BOT_CONFIG.defaultBotName}"` : '',
63 | BOT_CONFIG.defaultBotImage ? 'image=✓' : '',
64 | BOT_CONFIG.defaultEntryMessage ? 'message=✓' : '',
65 | BOT_CONFIG.defaultExtra ? 'extra=✓' : '',
66 | );
67 | }
68 |
69 | // Recording modes
70 | export const RECORDING_MODES = ['speaker_view', 'gallery_view', 'audio_only'] as const;
71 | export type RecordingMode = (typeof RECORDING_MODES)[number];
72 |
73 | // Speech-to-text providers
74 | export const SPEECH_TO_TEXT_PROVIDERS = ['Gladia', 'Runpod', 'Default'] as const;
75 | export type SpeechToTextProvider = (typeof SPEECH_TO_TEXT_PROVIDERS)[number];
76 |
77 | // Audio frequencies
78 | export const AUDIO_FREQUENCIES = ['16khz', '24khz'] as const;
79 | export type AudioFrequency = (typeof AUDIO_FREQUENCIES)[number];
80 |
```
--------------------------------------------------------------------------------
/src/utils/auth.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Authentication utilities for handling API keys and sessions
3 | */
4 |
5 | import * as fs from 'fs';
6 | import * as path from 'path';
7 | import * as os from 'os';
8 | import type { SessionAuth } from '../api/client.js';
9 |
10 | // Define a minimal logger interface rather than importing from fastmcp
11 | interface Logger {
12 | error: (message: string, ...args: any[]) => void;
13 | warn: (message: string, ...args: any[]) => void;
14 | info: (message: string, ...args: any[]) => void;
15 | debug: (message: string, ...args: any[]) => void;
16 | }
17 |
18 | /**
19 | * Get an API key with robust fallback mechanisms.
20 | * Tries, in order:
21 | * 1. Session object
22 | * 2. Environment variable
23 | * 3. Claude Desktop config file
24 | *
25 | * @param session The session object, which may contain an API key
26 | * @param log Optional logger for debugging
27 | * @returns An object with { apiKey, source } or null if no API key was found
28 | */
29 | export function getApiKeyWithFallbacks(
30 | session: any | undefined,
31 | log?: Logger
32 | ): { apiKey: string; source: string } | null {
33 | // Try to get API key from session
34 | if (session?.apiKey) {
35 | log?.debug("Using API key from session");
36 | return { apiKey: session.apiKey, source: 'session' };
37 | }
38 |
39 | // Try to get API key from environment variable
40 | if (process.env.MEETING_BAAS_API_KEY) {
41 | log?.debug("Using API key from environment variable");
42 | return { apiKey: process.env.MEETING_BAAS_API_KEY, source: 'environment' };
43 | }
44 |
45 | // Try to get API key from Claude Desktop config
46 | try {
47 | const claudeDesktopConfigPath = path.join(os.homedir(), 'Library/Application Support/Claude/claude_desktop_config.json');
48 | if (fs.existsSync(claudeDesktopConfigPath)) {
49 | const configContent = fs.readFileSync(claudeDesktopConfigPath, 'utf8');
50 | const configJson = JSON.parse(configContent);
51 |
52 | if (configJson.mcpServers?.meetingbaas?.headers?.['x-api-key']) {
53 | const apiKey = configJson.mcpServers.meetingbaas.headers['x-api-key'];
54 | log?.debug("Using API key from Claude Desktop config");
55 | return { apiKey, source: 'claude_config' };
56 | }
57 | }
58 | } catch (error) {
59 | log?.error("Error reading Claude Desktop config", { error });
60 | }
61 |
62 | // No API key found
63 | log?.error("No API key found in session, environment, or Claude Desktop config");
64 | return null;
65 | }
66 |
67 | /**
68 | * Creates a valid session object with an API key
69 | *
70 | * @param session The original session, which may be incomplete
71 | * @param log Optional logger for debugging
72 | * @returns A valid session object or null if no API key could be found
73 | */
74 | export function createValidSession(
75 | session: any | undefined,
76 | log?: Logger
77 | ): SessionAuth | null {
78 | const apiKeyInfo = getApiKeyWithFallbacks(session, log);
79 |
80 | if (!apiKeyInfo) {
81 | return null;
82 | }
83 |
84 | return { apiKey: apiKeyInfo.apiKey };
85 | }
```
--------------------------------------------------------------------------------
/scripts/cleanup_cursor_logs.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 | # cleanup_cursor_logs.sh
3 | # Script to clean up Cursor IDE log files to prevent excessive disk usage
4 |
5 | # Location of project
6 | PROJECT_DIR="/Users/lazmini/code/meeting-mcp"
7 |
8 | # Check if project directory exists
9 | if [ ! -d "$PROJECT_DIR" ]; then
10 | echo "Error: Project directory not found at $PROJECT_DIR"
11 | exit 1
12 | fi
13 |
14 | # Check if .cursor directory exists
15 | CURSOR_DIR="$PROJECT_DIR/.cursor"
16 | if [ ! -d "$CURSOR_DIR" ]; then
17 | echo "No .cursor directory found. Nothing to clean up."
18 | exit 0
19 | fi
20 |
21 | # Current size before cleanup
22 | BEFORE_SIZE=$(du -sh "$CURSOR_DIR" | awk '{print $1}')
23 | echo "Current .cursor directory size: $BEFORE_SIZE"
24 |
25 | # Backup important rules files that are not logs
26 | BACKUP_DIR="$PROJECT_DIR/.cursor_backup"
27 | mkdir -p "$BACKUP_DIR"
28 |
29 | # Save the rule definitions (not the log content)
30 | if [ -d "$CURSOR_DIR/rules" ]; then
31 | for file in "$CURSOR_DIR/rules"/*.mdc; do
32 | if [ -f "$file" ]; then
33 | # Extract just the first few lines which contain rule definitions
34 | head -n 10 "$file" > "$BACKUP_DIR/$(basename "$file")"
35 | fi
36 | done
37 | echo "Backed up rule definitions to $BACKUP_DIR"
38 | fi
39 |
40 | # Remove or truncate large log files
41 | find "$CURSOR_DIR" -type f -name "*.log" -exec truncate -s 0 {} \;
42 | echo "Truncated log files"
43 |
44 | # Check for log files in the parent directory
45 | LOG_DIR="/Users/lazmini/Library/Logs/Claude"
46 | if [ -d "$LOG_DIR" ]; then
47 | echo "Checking Claude logs directory..."
48 |
49 | # Find MCP server logs
50 | MCP_LOGS=$(find "$LOG_DIR" -name "mcp-server-meetingbaas.log*")
51 |
52 | for log_file in $MCP_LOGS; do
53 | if [ -f "$log_file" ]; then
54 | echo "Processing log file: $log_file"
55 |
56 | # Get file size before
57 | BEFORE_LOG_SIZE=$(du -h "$log_file" | awk '{print $1}')
58 |
59 | # Create a temporary file
60 | TEMP_FILE=$(mktemp)
61 |
62 | # Filter out ping/pong messages and keep other important logs
63 | grep -v '"method":"ping"' "$log_file" | grep -v '"result":{},"jsonrpc":"2.0","id":[0-9]\+' > "$TEMP_FILE"
64 |
65 | # Replace the original file with the filtered content
66 | mv "$TEMP_FILE" "$log_file"
67 |
68 | # Get file size after
69 | AFTER_LOG_SIZE=$(du -h "$log_file" | awk '{print $1}')
70 |
71 | echo " Removed ping/pong messages: $BEFORE_LOG_SIZE -> $AFTER_LOG_SIZE"
72 | fi
73 | done
74 | fi
75 |
76 | # Optional: Completely remove the mdc files which contain the full API specs
77 | # Uncomment if you want to remove these completely
78 | # find "$CURSOR_DIR/rules" -type f -name "*.mdc" -delete
79 | # echo "Removed rule definition files"
80 |
81 | # Or alternatively, truncate them to just include the essential metadata
82 | for file in "$CURSOR_DIR/rules"/*.mdc; do
83 | if [ -f "$file" ]; then
84 | # Keep only the first few lines with metadata and truncate the rest
85 | head -n 10 "$file" > "$file.tmp" && mv "$file.tmp" "$file"
86 | fi
87 | done
88 | echo "Truncated rule definition files to essential metadata"
89 |
90 | # After cleanup size
91 | AFTER_SIZE=$(du -sh "$CURSOR_DIR" | awk '{print $1}')
92 | echo "New .cursor directory size: $AFTER_SIZE"
93 |
94 | echo "Cleanup complete!"
```
--------------------------------------------------------------------------------
/src/tools/retranscribe.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tool for retranscribing a bot's audio
3 | */
4 |
5 | import { z } from 'zod';
6 | import { apiRequest } from '../api/client.js';
7 | import { createValidSession } from '../utils/auth.js';
8 | import { createTool } from '../utils/tool-types.js';
9 |
10 | // Schema for the retranscribe tool parameters
11 | const retranscribeParams = z.object({
12 | botId: z.string().describe('UUID of the bot to retranscribe'),
13 | speechToTextProvider: z
14 | .enum(['Gladia', 'Runpod', 'Default'])
15 | .optional()
16 | .describe('Speech-to-text provider to use for transcription (optional)'),
17 | speechToTextApiKey: z
18 | .string()
19 | .optional()
20 | .describe('API key for the speech-to-text provider if required (optional)'),
21 | webhookUrl: z
22 | .string()
23 | .url()
24 | .optional()
25 | .describe('Webhook URL to receive notification when transcription is complete (optional)'),
26 | });
27 |
28 | /**
29 | * Retranscribes a bot's audio using the specified speech-to-text provider.
30 | * This is useful when you want to:
31 | * 1. Use a different speech-to-text provider than originally used
32 | * 2. Retry a failed transcription
33 | * 3. Get a new transcription with different settings
34 | */
35 | export const retranscribeTool = createTool(
36 | 'retranscribe_bot',
37 | "Retranscribe a bot's audio using the Default or your provided Speech to Text Provider",
38 | retranscribeParams,
39 | async (args, context) => {
40 | const { session, log } = context;
41 |
42 | log.info('Retranscribing bot', {
43 | botId: args.botId,
44 | provider: args.speechToTextProvider,
45 | hasApiKey: !!args.speechToTextApiKey,
46 | hasWebhook: !!args.webhookUrl,
47 | });
48 |
49 | try {
50 | // Create a valid session with fallbacks for API key
51 | const validSession = createValidSession(session, log);
52 |
53 | // Check if we have a valid session with API key
54 | if (!validSession) {
55 | return {
56 | content: [
57 | {
58 | type: 'text' as const,
59 | text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
60 | },
61 | ],
62 | isError: true,
63 | };
64 | }
65 |
66 | // Prepare the request body
67 | const requestBody = {
68 | bot_uuid: args.botId,
69 | speech_to_text: args.speechToTextProvider
70 | ? {
71 | provider: args.speechToTextProvider,
72 | api_key: args.speechToTextApiKey,
73 | }
74 | : undefined,
75 | webhook_url: args.webhookUrl,
76 | };
77 |
78 | // Make the API request
79 | const response = await apiRequest(validSession, 'post', '/bots/retranscribe', requestBody);
80 |
81 | // Handle different response status codes
82 | if (response.status === 200) {
83 | return 'Retranscription request accepted. The transcription will be processed asynchronously.';
84 | } else if (response.status === 202) {
85 | return 'Retranscription request accepted and is being processed.';
86 | } else {
87 | return `Unexpected response status: ${response.status}`;
88 | }
89 | } catch (error) {
90 | log.error('Error retranscribing bot', { error: String(error), botId: args.botId });
91 | return `Error retranscribing bot: ${error instanceof Error ? error.message : String(error)}`;
92 | }
93 | },
94 | );
95 |
```
--------------------------------------------------------------------------------
/src/utils/linkFormatter.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Utilities for formatting and presenting meeting recording links
3 | */
4 |
5 | /**
6 | * Base URL for the Meeting BaaS viewer
7 | */
8 | export const VIEWER_BASE_URL = "https://meetingbaas.com/viewer";
9 |
10 | /**
11 | * Formats a meeting link to the recording viewer
12 | */
13 | export function formatMeetingLink(botId: string, timestamp?: number): string {
14 | if (!botId) {
15 | return "";
16 | }
17 |
18 | const baseLink = `${VIEWER_BASE_URL}/${botId}`;
19 |
20 | if (timestamp !== undefined && timestamp !== null) {
21 | return `${baseLink}?t=${Math.floor(timestamp)}`;
22 | }
23 |
24 | return baseLink;
25 | }
26 |
27 | /**
28 | * Creates a rich meeting link display, ready for sharing in chat
29 | */
30 | export function createShareableLink(
31 | botId: string,
32 | options: {
33 | title?: string;
34 | timestamp?: number;
35 | speakerName?: string;
36 | description?: string;
37 | } = {}
38 | ): string {
39 | const { title, timestamp, speakerName, description } = options;
40 |
41 | const link = formatMeetingLink(botId, timestamp);
42 | if (!link) {
43 | return "⚠️ No meeting link could be generated. Please provide a valid bot ID.";
44 | }
45 |
46 | // Construct the display text
47 | let displayText = "📽️ **Meeting Recording";
48 |
49 | if (title) {
50 | displayText += `: ${title}**`;
51 | } else {
52 | displayText += "**";
53 | }
54 |
55 | // Add timestamp info if provided
56 | if (timestamp !== undefined) {
57 | const timestampFormatted = formatTimestamp(timestamp);
58 | displayText += `\n⏱️ Timestamp: ${timestampFormatted}`;
59 | }
60 |
61 | // Add speaker info if provided
62 | if (speakerName) {
63 | displayText += `\n🎤 Speaker: ${speakerName}`;
64 | }
65 |
66 | // Add description if provided
67 | if (description) {
68 | displayText += `\n📝 ${description}`;
69 | }
70 |
71 | // Add the actual link
72 | displayText += `\n\n🔗 [View Recording](${link})`;
73 |
74 | return displayText;
75 | }
76 |
77 | /**
78 | * Format a timestamp in seconds to a human-readable format (HH:MM:SS)
79 | */
80 | function formatTimestamp(seconds: number): string {
81 | if (seconds === undefined || seconds === null) {
82 | return "00:00:00";
83 | }
84 |
85 | const hours = Math.floor(seconds / 3600);
86 | const minutes = Math.floor((seconds % 3600) / 60);
87 | const secs = Math.floor(seconds % 60);
88 |
89 | return [
90 | hours.toString().padStart(2, "0"),
91 | minutes.toString().padStart(2, "0"),
92 | secs.toString().padStart(2, "0"),
93 | ].join(":");
94 | }
95 |
96 | /**
97 | * Generates a shareable segment for multiple moments in a meeting
98 | */
99 | export function createMeetingSegmentsList(
100 | botId: string,
101 | segments: Array<{
102 | timestamp: number;
103 | speaker?: string;
104 | description: string;
105 | }>
106 | ): string {
107 | if (!segments || segments.length === 0) {
108 | return createShareableLink(botId, { title: "Full Recording" });
109 | }
110 |
111 | let result = "## 📽️ Meeting Segments\n\n";
112 |
113 | segments.forEach((segment, index) => {
114 | const link = formatMeetingLink(botId, segment.timestamp);
115 | const timestampFormatted = formatTimestamp(segment.timestamp);
116 |
117 | result += `### Segment ${index + 1}: ${timestampFormatted}\n`;
118 | if (segment.speaker) {
119 | result += `**Speaker**: ${segment.speaker}\n`;
120 | }
121 | result += `**Description**: ${segment.description}\n`;
122 | result += `🔗 [Jump to this moment](${link})\n\n`;
123 | });
124 |
125 | result += `\n🔗 [View Full Recording](${formatMeetingLink(botId)})`;
126 |
127 | return result;
128 | }
129 |
130 | /**
131 | * Creates a compact single-line meeting link for inline sharing
132 | */
133 | export function createInlineMeetingLink(botId: string, timestamp?: number, label?: string): string {
134 | const link = formatMeetingLink(botId, timestamp);
135 | const displayLabel = label || "View Recording";
136 |
137 | return `[${displayLabel}](${link})`;
138 | }
```
--------------------------------------------------------------------------------
/src/api/client.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * API client for MeetingBaaS API
3 | */
4 |
5 | import axios, { AxiosError, Method } from 'axios';
6 | import { UserError } from 'fastmcp';
7 | import { getApiBaseUrl } from '../config.js';
8 |
9 | /**
10 | * Session type definition
11 | */
12 | export type SessionAuth = { apiKey: string };
13 |
14 | /**
15 | * Makes a request to the MeetingBaaS API
16 | */
17 | export async function apiRequest(
18 | session: SessionAuth | undefined,
19 | method: Method,
20 | endpoint: string,
21 | data: Record<string, unknown> | null = null,
22 | ) {
23 | // Validate session
24 | if (!session) {
25 | console.error(`[API Client] No session provided`);
26 | throw new UserError('Authentication required: No session provided');
27 | }
28 |
29 | // Extract and validate API key
30 | const apiKey = session.apiKey;
31 | if (!apiKey) {
32 | console.error(`[API Client] No API key in session object`);
33 | throw new UserError('Authentication required: No API key provided');
34 | }
35 |
36 | // Normalize API key to string
37 | const apiKeyString = Array.isArray(apiKey) ? apiKey[0] : apiKey;
38 |
39 | // Make sure we have a valid string API key
40 | if (typeof apiKeyString !== 'string' || apiKeyString.length === 0) {
41 | console.error(`[API Client] Invalid API key format`);
42 | throw new UserError('Authentication error: Invalid API key format');
43 | }
44 |
45 | try {
46 | // Set up headers with API key
47 | const headers = {
48 | 'x-meeting-baas-api-key': apiKeyString,
49 | 'Content-Type': 'application/json',
50 | };
51 |
52 | // Get the current API base URL
53 | const apiBaseUrl = getApiBaseUrl();
54 |
55 | // Make the API request
56 | const response = await axios({
57 | method,
58 | url: `${apiBaseUrl}${endpoint}`,
59 | headers,
60 | data,
61 | });
62 |
63 | return response.data;
64 | } catch (error) {
65 | // Handle Axios errors
66 | if (axios.isAxiosError(error)) {
67 | const axiosError = error as AxiosError;
68 | console.error(`[API Client] Request failed: ${axiosError.message}`);
69 |
70 | // Handle specific error codes
71 | if (axiosError.response?.status === 401 || axiosError.response?.status === 403) {
72 | throw new UserError(`Authentication failed: Invalid API key or insufficient permissions`);
73 | }
74 |
75 | // Extract error details from response data if available
76 | if (axiosError.response?.data) {
77 | throw new UserError(`API Error: ${JSON.stringify(axiosError.response.data)}`);
78 | }
79 |
80 | throw new UserError(`API Error: ${axiosError.message}`);
81 | }
82 |
83 | // Handle non-Axios errors
84 | const err = error instanceof Error ? error : new Error(String(error));
85 | console.error(`[API Client] Request error: ${err.message}`);
86 | throw new UserError(`Request error: ${err.message}`);
87 | }
88 | }
89 |
90 | /**
91 | * Client for the MeetingBaaS API
92 | */
93 | export class MeetingBaasClient {
94 | private apiKey: string;
95 |
96 | constructor(apiKey: string) {
97 | this.apiKey = apiKey;
98 | }
99 |
100 | /**
101 | * Join a meeting with a bot
102 | */
103 | async joinMeeting(params: {
104 | meeting_url: string;
105 | bot_name: string | null;
106 | bot_image?: string | null;
107 | entry_message?: string | null;
108 | deduplication_key?: string | null;
109 | automatic_leave?: {
110 | noone_joined_timeout?: number | null;
111 | waiting_room_timeout?: number | null;
112 | } | null;
113 | speech_to_text?: {
114 | provider: string;
115 | api_key?: string | null;
116 | } | null;
117 | streaming?: {
118 | input?: string | null;
119 | output?: string | null;
120 | audio_frequency?: string | null;
121 | } | null;
122 | reserved?: boolean;
123 | recording_mode?: string;
124 | start_time?: string;
125 | extra?: Record<string, unknown>;
126 | }) {
127 | // Create a session with our API key
128 | const session: SessionAuth = { apiKey: this.apiKey };
129 |
130 | // Use the existing apiRequest function
131 | return apiRequest(session, 'post', '/bots', params);
132 | }
133 | }
134 |
```
--------------------------------------------------------------------------------
/src/resources/transcript.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Resources for meeting transcripts and metadata
3 | */
4 |
5 | import type { ResourceResult, ResourceTemplate } from "fastmcp";
6 | import { apiRequest } from "../api/client.js";
7 | import { formatDuration, formatTime } from "../utils/formatters.js";
8 |
9 | // Explicitly define transcript interface instead of importing
10 | interface Transcript {
11 | speaker: string;
12 | start_time: number;
13 | words: { text: string }[];
14 | }
15 |
16 | // Define our session auth type
17 | type SessionAuth = { apiKey: string };
18 |
19 | /**
20 | * Meeting transcript resource
21 | */
22 | export const meetingTranscriptResource: ResourceTemplate<
23 | [
24 | {
25 | name: string;
26 | description: string;
27 | required: boolean;
28 | }
29 | ]
30 | > = {
31 | uriTemplate: "meeting:transcript/{botId}",
32 | name: "Meeting Transcript",
33 | mimeType: "text/plain",
34 | arguments: [
35 | {
36 | name: "botId",
37 | description: "ID of the bot that recorded the meeting",
38 | required: true,
39 | },
40 | ],
41 | load: async function (args: Record<string, string>): Promise<ResourceResult> {
42 | const { botId } = args;
43 |
44 | try {
45 | const session = { apiKey: "session-key" }; // This will be provided by the context
46 |
47 | const response = await apiRequest(
48 | session,
49 | "get",
50 | `/bots/meeting_data?bot_id=${botId}`
51 | );
52 |
53 | const transcripts: Transcript[] = response.bot_data.transcripts;
54 |
55 | // Format all transcripts
56 | const formattedTranscripts = transcripts
57 | .map((transcript: Transcript) => {
58 | const text = transcript.words
59 | .map((word: { text: string }) => word.text)
60 | .join(" ");
61 | const startTime = formatTime(transcript.start_time);
62 | const speaker = transcript.speaker;
63 |
64 | return `[${startTime}] ${speaker}: ${text}`;
65 | })
66 | .join("\n\n");
67 |
68 | return {
69 | text:
70 | formattedTranscripts || "No transcript available for this meeting.",
71 | };
72 | } catch (error: unknown) {
73 | const errorMessage =
74 | error instanceof Error ? error.message : String(error);
75 | return {
76 | text: `Error retrieving transcript: ${errorMessage}`,
77 | };
78 | }
79 | },
80 | };
81 |
82 | /**
83 | * Meeting metadata resource
84 | */
85 | export const meetingMetadataResource: ResourceTemplate<
86 | [
87 | {
88 | name: string;
89 | description: string;
90 | required: boolean;
91 | }
92 | ]
93 | > = {
94 | uriTemplate: "meeting:metadata/{botId}",
95 | name: "Meeting Metadata",
96 | mimeType: "application/json",
97 | arguments: [
98 | {
99 | name: "botId",
100 | description: "ID of the bot that recorded the meeting",
101 | required: true,
102 | },
103 | ],
104 | load: async function (args: Record<string, string>): Promise<ResourceResult> {
105 | const { botId } = args;
106 |
107 | try {
108 | const session = { apiKey: "session-key" }; // This will be provided by the context
109 |
110 | const response = await apiRequest(
111 | session,
112 | "get",
113 | `/bots/meeting_data?bot_id=${botId}`
114 | );
115 |
116 | // Extract and format metadata for easier consumption
117 | const metadata = {
118 | duration: response.duration,
119 | formattedDuration: formatDuration(response.duration),
120 | videoUrl: response.mp4,
121 | bot: {
122 | name: response.bot_data.bot.bot_name,
123 | meetingUrl: response.bot_data.bot.meeting_url,
124 | createdAt: response.bot_data.bot.created_at,
125 | endedAt: response.bot_data.bot.ended_at,
126 | },
127 | transcriptSegments: response.bot_data.transcripts.length,
128 | };
129 |
130 | return {
131 | text: JSON.stringify(metadata, null, 2),
132 | };
133 | } catch (error: unknown) {
134 | const errorMessage =
135 | error instanceof Error ? error.message : String(error);
136 | return {
137 | text: `Error retrieving metadata: ${errorMessage}`,
138 | };
139 | }
140 | },
141 | };
142 |
```
--------------------------------------------------------------------------------
/src/tools/deleteData.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tool for deleting data associated with a bot
3 | */
4 |
5 | import { z } from 'zod';
6 | import { apiRequest } from '../api/client.js';
7 | import { createValidSession } from '../utils/auth.js';
8 | import { createTool } from '../utils/tool-types.js';
9 |
10 | // Schema for the delete data tool parameters
11 | const deleteDataParams = z.object({
12 | botId: z.string().describe('UUID of the bot to delete data for'),
13 | });
14 |
15 | // Delete status types from the API
16 | type DeleteStatus = 'deleted' | 'partiallyDeleted' | 'alreadyDeleted' | 'noDataFound';
17 |
18 | // DeleteResponse interface matching the API spec
19 | interface DeleteResponse {
20 | ok: boolean;
21 | status: DeleteStatus;
22 | }
23 |
24 | /**
25 | * Deletes the transcription, log files, and video recording, along with all data
26 | * associated with a bot from Meeting Baas servers.
27 | *
28 | * The following public-facing fields will be retained:
29 | * - meeting_url
30 | * - created_at
31 | * - reserved
32 | * - errors
33 | * - ended_at
34 | * - mp4_s3_path (null as the file is deleted)
35 | * - uuid
36 | * - bot_param_id
37 | * - event_id
38 | * - scheduled_bot_id
39 | * - api_key_id
40 | *
41 | * Note: This endpoint is rate-limited to 5 requests per minute per API key.
42 | */
43 | export const deleteDataTool = createTool(
44 | 'delete_meeting_data',
45 | 'Delete transcription, log files, and video recording, along with all data associated with a bot',
46 | deleteDataParams,
47 | async (args, context) => {
48 | const { session, log } = context;
49 |
50 | log.info('Deleting data for bot', { botId: args.botId });
51 |
52 | try {
53 | // Create a valid session with fallbacks for API key
54 | const validSession = createValidSession(session, log);
55 |
56 | // Check if we have a valid session with API key
57 | if (!validSession) {
58 | return {
59 | content: [
60 | {
61 | type: 'text' as const,
62 | text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
63 | },
64 | ],
65 | isError: true,
66 | };
67 | }
68 |
69 | // Make the API request to delete data
70 | const response = await apiRequest(
71 | validSession,
72 | 'post',
73 | `/bots/${args.botId}/delete_data`,
74 | null,
75 | );
76 |
77 | // Format response based on status
78 | if (response.ok) {
79 | // Handle the DeleteResponse format according to the API spec
80 | const statusMessages: Record<DeleteStatus, string> = {
81 | deleted:
82 | 'Successfully deleted all data. The meeting metadata (URL, timestamps, etc.) has been preserved, but all content (recordings, transcriptions, and logs) has been deleted.',
83 | partiallyDeleted:
84 | 'Partially deleted data. Some content could not be removed, but most data has been deleted. The meeting metadata has been preserved.',
85 | alreadyDeleted: 'Data was already deleted. No further action was needed.',
86 | noDataFound: 'No data was found for the specified bot ID.',
87 | };
88 |
89 | // Extract the DeleteResponse object from the API response
90 | const data = response.data as unknown as DeleteResponse;
91 |
92 | // Verify the operation was successful according to the API
93 | if (data && data.ok) {
94 | // Return the appropriate message based on the status
95 | return (
96 | statusMessages[data.status] || `Successfully processed with status: ${data.status}`
97 | );
98 | } else if (data && data.status) {
99 | return `Operation returned ok: false. Status: ${data.status}`;
100 | } else {
101 | // Fallback for unexpected response format
102 | return `Data deleted successfully, but the response format was unexpected: ${JSON.stringify(response.data)}`;
103 | }
104 | } else {
105 | // Handle error responses
106 | if (response.status === 401) {
107 | return 'Unauthorized: Missing or invalid API key.';
108 | } else if (response.status === 403) {
109 | return "Forbidden: You don't have permission to delete this bot's data.";
110 | } else if (response.status === 404) {
111 | return 'Not found: The specified bot ID does not exist.';
112 | } else if (response.status === 429) {
113 | return 'Rate limit exceeded: This endpoint is limited to 5 requests per minute per API key. Please try again later.';
114 | } else {
115 | return `Failed to delete data: ${JSON.stringify(response)}`;
116 | }
117 | }
118 | } catch (error) {
119 | log.error('Error deleting data', { error: String(error), botId: args.botId });
120 |
121 | // Check for rate limit error
122 | if (error instanceof Error && error.message.includes('429')) {
123 | return 'Rate limit exceeded: This endpoint is limited to 5 requests per minute per API key. Please try again later.';
124 | }
125 |
126 | return `Error deleting data: ${error instanceof Error ? error.message : String(error)}`;
127 | }
128 | },
129 | );
130 |
```
--------------------------------------------------------------------------------
/src/tools/search.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Simple MCP tool for retrieving meeting transcripts
3 | */
4 |
5 | import { z } from 'zod';
6 | import { apiRequest } from '../api/client.js';
7 | import { createValidSession } from '../utils/auth.js';
8 | import { createTool } from '../utils/tool-types.js';
9 |
10 | // Define transcript-related interfaces that match the actual API response structure
11 | interface TranscriptWord {
12 | text: string;
13 | start_time: number;
14 | end_time: number;
15 | id?: number;
16 | bot_id?: number;
17 | user_id?: number | null;
18 | }
19 |
20 | interface TranscriptSegment {
21 | speaker: string;
22 | start_time: number;
23 | end_time?: number | null;
24 | words: TranscriptWord[];
25 | id?: number;
26 | bot_id?: number;
27 | user_id?: number | null;
28 | lang?: string | null;
29 | }
30 |
31 | interface BotData {
32 | bot: {
33 | bot_name: string;
34 | meeting_url: string;
35 | [key: string]: any;
36 | };
37 | transcripts: TranscriptSegment[];
38 | }
39 |
40 | interface MetadataResponse {
41 | bot_data: BotData;
42 | duration: number;
43 | mp4: string;
44 | }
45 |
46 | // Define the simple parameters schema
47 | const getTranscriptParams = z.object({
48 | botId: z.string().describe('ID of the bot/meeting to retrieve transcript for'),
49 | });
50 |
51 | /**
52 | * Tool to get meeting transcript data
53 | *
54 | * This tool retrieves meeting data and returns the transcript,
55 | * properly handling the API response structure.
56 | */
57 | export const getTranscriptTool = createTool(
58 | 'getMeetingTranscript',
59 | 'Get a meeting transcript with speaker names and content grouped by speaker',
60 | getTranscriptParams,
61 | async (args, context) => {
62 | const { session, log } = context;
63 | log.info('Getting meeting transcript', { botId: args.botId });
64 |
65 | try {
66 | // Create a valid session with fallbacks for API key
67 | const validSession = createValidSession(session, log);
68 |
69 | // Check if we have a valid session with API key
70 | if (!validSession) {
71 | return {
72 | content: [
73 | {
74 | type: 'text' as const,
75 | text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
76 | },
77 | ],
78 | isError: true,
79 | };
80 | }
81 |
82 | // Make the API request to get meeting data
83 | const response = (await apiRequest(
84 | validSession,
85 | 'get',
86 | `/bots/meeting_data?bot_id=${args.botId}`,
87 | )) as MetadataResponse;
88 |
89 | // Check for valid response structure
90 | if (!response || !response.bot_data) {
91 | return 'Error: Invalid response structure from API.';
92 | }
93 |
94 | // Extract meeting information
95 | const meetingInfo = {
96 | name: response.bot_data.bot?.bot_name || 'Unknown Meeting',
97 | url: response.bot_data.bot?.meeting_url || 'Unknown URL',
98 | duration: response.duration || 0,
99 | };
100 |
101 | // Extract transcripts from the response
102 | const transcripts = response.bot_data.transcripts || [];
103 |
104 | // If no transcripts, provide info about the meeting
105 | if (transcripts.length === 0) {
106 | return `Meeting "${meetingInfo.name}" has a recording (${Math.floor(meetingInfo.duration / 60)}m ${meetingInfo.duration % 60}s), but no transcript segments are available.`;
107 | }
108 |
109 | // Group and combine text by speaker
110 | const speakerTexts: Record<string, string[]> = {};
111 |
112 | // First pass: collect all text segments by speaker
113 | transcripts.forEach((segment: TranscriptSegment) => {
114 | const speaker = segment.speaker;
115 | // Check that words array exists and has content
116 | if (!segment.words || !Array.isArray(segment.words)) {
117 | return;
118 | }
119 |
120 | const text = segment.words
121 | .map((word) => word.text || '')
122 | .join(' ')
123 | .trim();
124 | if (!text) return; // Skip empty text
125 |
126 | if (!speakerTexts[speaker]) {
127 | speakerTexts[speaker] = [];
128 | }
129 |
130 | speakerTexts[speaker].push(text);
131 | });
132 |
133 | // If after processing we have no text, provide info
134 | if (Object.keys(speakerTexts).length === 0) {
135 | return `Meeting "${meetingInfo.name}" has a recording (${Math.floor(meetingInfo.duration / 60)}m ${meetingInfo.duration % 60}s), but could not extract readable transcript.`;
136 | }
137 |
138 | // Second pass: combine all text segments per speaker
139 | const combinedBySpeaker = Object.entries(speakerTexts).map(([speaker, texts]) => {
140 | return {
141 | speaker,
142 | text: texts.join(' '),
143 | };
144 | });
145 |
146 | // Format the transcript grouped by speaker
147 | const formattedTranscript = combinedBySpeaker
148 | .map((entry) => `${entry.speaker}: ${entry.text}`)
149 | .join('\n\n');
150 |
151 | // Add meeting info header
152 | const header = `Meeting: "${meetingInfo.name}"\nDuration: ${Math.floor(meetingInfo.duration / 60)}m ${meetingInfo.duration % 60}s\nTranscript:\n\n`;
153 |
154 | return header + formattedTranscript;
155 | } catch (error) {
156 | log.error('Error getting transcript', { error: String(error) });
157 | return `Error getting transcript: ${error instanceof Error ? error.message : String(error)}`;
158 | }
159 | },
160 | );
161 |
```
--------------------------------------------------------------------------------
/src/utils/tinyDb.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * TinyDB - A simple file-based database for tracking bot usage
3 | *
4 | * This module provides a lightweight persistence layer to track bot usage across sessions.
5 | * It stores recently used bots along with their metadata for enhanced search experiences.
6 | */
7 |
8 | import fs from 'fs';
9 | import path from 'path';
10 |
11 | // Interface for bot metadata in our tiny database
12 | export interface BotRecord {
13 | id: string; // Bot UUID
14 | name?: string; // Bot name if available
15 | meetingUrl?: string; // URL of the meeting
16 | meetingType?: string; // Type of meeting (e.g., "sales", "standup")
17 | createdAt?: string; // When the bot/meeting was created
18 | lastAccessedAt: string; // When the bot was last accessed by a user
19 | accessCount: number; // How many times this bot has been accessed
20 | creator?: string; // Who created/requested the bot
21 | participants?: string[]; // Meeting participants if known
22 | topics?: string[]; // Key topics discussed in the meeting
23 | extra?: Record<string, any>; // Additional metadata from the original API
24 | }
25 |
26 | // Main database class
27 | export class TinyDb {
28 | private dbPath: string;
29 | private data: {
30 | recentBots: BotRecord[];
31 | lastUpdated: string;
32 | };
33 |
34 | constructor(dbFilePath?: string) {
35 | // Use provided path or default to the project root
36 | this.dbPath = dbFilePath || path.resolve(process.cwd(), 'bot-history.json');
37 |
38 | // Initialize with empty data
39 | this.data = {
40 | recentBots: [],
41 | lastUpdated: new Date().toISOString()
42 | };
43 |
44 | // Try to load existing data
45 | this.loadFromFile();
46 | }
47 |
48 | // Load database from file
49 | private loadFromFile(): void {
50 | try {
51 | if (fs.existsSync(this.dbPath)) {
52 | const fileContent = fs.readFileSync(this.dbPath, 'utf-8');
53 | this.data = JSON.parse(fileContent);
54 | console.log(`TinyDB: Loaded ${this.data.recentBots.length} bot records from ${this.dbPath}`);
55 | } else {
56 | console.log(`TinyDB: No existing database found at ${this.dbPath}, starting fresh`);
57 | }
58 | } catch (error) {
59 | console.error(`TinyDB: Error loading database:`, error);
60 | // Continue with empty data
61 | }
62 | }
63 |
64 | // Save database to file
65 | private saveToFile(): void {
66 | try {
67 | // Update the lastUpdated timestamp
68 | this.data.lastUpdated = new Date().toISOString();
69 |
70 | // Write to file
71 | fs.writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2), 'utf-8');
72 | console.log(`TinyDB: Saved ${this.data.recentBots.length} bot records to ${this.dbPath}`);
73 | } catch (error) {
74 | console.error(`TinyDB: Error saving database:`, error);
75 | }
76 | }
77 |
78 | // Add or update a bot record
79 | public trackBot(botData: Partial<BotRecord> & { id: string }): BotRecord {
80 | // Find if bot already exists
81 | const existingIndex = this.data.recentBots.findIndex(bot => bot.id === botData.id);
82 |
83 | if (existingIndex >= 0) {
84 | // Update existing record
85 | const existingBot = this.data.recentBots[existingIndex];
86 |
87 | // Preserve existing data while updating with new data
88 | const updatedBot: BotRecord = {
89 | ...existingBot,
90 | ...botData,
91 | // Always update these fields
92 | lastAccessedAt: new Date().toISOString(),
93 | accessCount: (existingBot.accessCount || 0) + 1,
94 | };
95 |
96 | // Remove from current position
97 | this.data.recentBots.splice(existingIndex, 1);
98 |
99 | // Add to the front (most recent)
100 | this.data.recentBots.unshift(updatedBot);
101 |
102 | // Save changes
103 | this.saveToFile();
104 |
105 | return updatedBot;
106 | } else {
107 | // Create new record
108 | const newBot: BotRecord = {
109 | ...botData,
110 | lastAccessedAt: new Date().toISOString(),
111 | accessCount: 1,
112 | };
113 |
114 | // Add to the front (most recent)
115 | this.data.recentBots.unshift(newBot);
116 |
117 | // Trim the list if it gets too long (keeping most recent 50)
118 | if (this.data.recentBots.length > 50) {
119 | this.data.recentBots = this.data.recentBots.slice(0, 50);
120 | }
121 |
122 | // Save changes
123 | this.saveToFile();
124 |
125 | return newBot;
126 | }
127 | }
128 |
129 | // Get most recent bots (defaults to 5)
130 | public getRecentBots(limit: number = 5): BotRecord[] {
131 | return this.data.recentBots.slice(0, limit);
132 | }
133 |
134 | // Get most accessed bots (defaults to 5)
135 | public getMostAccessedBots(limit: number = 5): BotRecord[] {
136 | // Sort by access count (descending) and return top ones
137 | return [...this.data.recentBots]
138 | .sort((a, b) => (b.accessCount || 0) - (a.accessCount || 0))
139 | .slice(0, limit);
140 | }
141 |
142 | // Search bots by meeting type
143 | public getBotsByMeetingType(meetingType: string): BotRecord[] {
144 | return this.data.recentBots.filter(bot =>
145 | bot.meetingType?.toLowerCase() === meetingType.toLowerCase()
146 | );
147 | }
148 |
149 | // Get bot by ID
150 | public getBot(id: string): BotRecord | undefined {
151 | return this.data.recentBots.find(bot => bot.id === id);
152 | }
153 |
154 | // Update session with recent bot IDs
155 | public updateSession(session: any): void {
156 | if (!session) return;
157 |
158 | // Add recentBotIds to session
159 | session.recentBotIds = this.getRecentBots(5).map(bot => bot.id);
160 | }
161 | }
162 |
163 | // Singleton instance
164 | let db: TinyDb | null = null;
165 |
166 | // Get the singleton instance
167 | export function getTinyDb(dbFilePath?: string): TinyDb {
168 | if (!db) {
169 | db = new TinyDb(dbFilePath);
170 | }
171 | return db;
172 | }
```
--------------------------------------------------------------------------------
/src/tools/listBots.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tool for listing bots with metadata
3 | */
4 |
5 | import { z } from 'zod';
6 | import { apiRequest } from '../api/client.js';
7 | import { createValidSession } from '../utils/auth.js';
8 | import { createTool } from '../utils/tool-types.js';
9 |
10 | // Schema for the list bots with metadata tool parameters
11 | const listBotsParams = z.object({
12 | limit: z
13 | .number()
14 | .int()
15 | .min(1)
16 | .max(50)
17 | .optional()
18 | .default(10)
19 | .describe('Maximum number of bots to return (1-50, default: 10)'),
20 | bot_name: z
21 | .string()
22 | .optional()
23 | .describe('Filter bots by name containing this string (case-insensitive)'),
24 | meeting_url: z.string().optional().describe('Filter bots by meeting URL containing this string'),
25 | created_after: z
26 | .string()
27 | .optional()
28 | .describe('Filter bots created after this date (ISO format, e.g., 2023-05-01T00:00:00)'),
29 | created_before: z
30 | .string()
31 | .optional()
32 | .describe('Filter bots created before this date (ISO format, e.g., 2023-05-31T23:59:59)'),
33 | filter_by_extra: z
34 | .string()
35 | .optional()
36 | .describe("Filter by extra JSON fields (format: 'field1:value1,field2:value2')"),
37 | sort_by_extra: z
38 | .string()
39 | .optional()
40 | .describe("Sort by field in extra JSON (format: 'field:asc' or 'field:desc')"),
41 | cursor: z.string().optional().describe('Cursor for pagination from previous response'),
42 | });
43 |
44 | /**
45 | * Retrieves a paginated list of the user's bots with essential metadata,
46 | * including IDs, names, and meeting details. Supports filtering, sorting,
47 | * and advanced querying options.
48 | */
49 | export const listBotsWithMetadataTool = createTool(
50 | 'list_bots_with_metadata',
51 | 'List recent bots with metadata, including IDs, names, meeting details with filtering and sorting options',
52 | listBotsParams,
53 | async (args, context) => {
54 | const { session, log } = context;
55 |
56 | log.info('Listing bots with metadata', {
57 | limit: args.limit,
58 | bot_name: args.bot_name,
59 | meeting_url: args.meeting_url,
60 | created_after: args.created_after,
61 | created_before: args.created_before,
62 | filter_by_extra: args.filter_by_extra,
63 | sort_by_extra: args.sort_by_extra,
64 | cursor: args.cursor,
65 | });
66 |
67 | try {
68 | // Create a valid session with fallbacks for API key
69 | const validSession = createValidSession(session, log);
70 |
71 | // Check if we have a valid session with API key
72 | if (!validSession) {
73 | return {
74 | content: [
75 | {
76 | type: 'text' as const,
77 | text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
78 | },
79 | ],
80 | isError: true,
81 | };
82 | }
83 |
84 | // Construct query parameters
85 | const queryParams = new URLSearchParams();
86 |
87 | if (args.limit !== undefined) queryParams.append('limit', args.limit.toString());
88 | if (args.bot_name) queryParams.append('bot_name', args.bot_name);
89 | if (args.meeting_url) queryParams.append('meeting_url', args.meeting_url);
90 | if (args.created_after) queryParams.append('created_after', args.created_after);
91 | if (args.created_before) queryParams.append('created_before', args.created_before);
92 | if (args.filter_by_extra) queryParams.append('filter_by_extra', args.filter_by_extra);
93 | if (args.sort_by_extra) queryParams.append('sort_by_extra', args.sort_by_extra);
94 | if (args.cursor) queryParams.append('cursor', args.cursor);
95 |
96 | // Make the API request
97 | const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
98 | const response = await apiRequest(
99 | validSession,
100 | 'get',
101 | `/bots/bots_with_metadata${queryString}`,
102 | null,
103 | );
104 |
105 | // Check if response contains bots
106 | if (!response.recentBots || !Array.isArray(response.recentBots)) {
107 | return 'No bots found or unexpected response format.';
108 | }
109 |
110 | const bots = response.recentBots;
111 |
112 | // Format the response
113 | if (bots.length === 0) {
114 | return 'No bots found matching your criteria.';
115 | }
116 |
117 | // Format the bot list
118 | const formattedBots = bots
119 | .map((bot: any, index: number) => {
120 | // Extract creation date and format it
121 | const createdAt = bot.createdAt ? new Date(bot.createdAt).toLocaleString() : 'Unknown';
122 |
123 | // Format duration if available
124 | const duration = bot.duration
125 | ? `${Math.floor(bot.duration / 60)}m ${bot.duration % 60}s`
126 | : 'N/A';
127 |
128 | // Extract any customer ID or meeting info from extra (if available)
129 | const extraInfo = [];
130 | if (bot.extra) {
131 | if (bot.extra.customerId) extraInfo.push(`Customer ID: ${bot.extra.customerId}`);
132 | if (bot.extra.meetingType) extraInfo.push(`Type: ${bot.extra.meetingType}`);
133 | if (bot.extra.description) extraInfo.push(`Description: ${bot.extra.description}`);
134 | }
135 |
136 | return `${index + 1}. Bot: ${bot.name || 'Unnamed'} (ID: ${bot.id})
137 | Created: ${createdAt}
138 | Duration: ${duration}
139 | Meeting URL: ${bot.meetingUrl || 'N/A'}
140 | Status: ${bot.endedAt ? 'Completed' : 'Active'}
141 | ${extraInfo.length > 0 ? `Additional Info: ${extraInfo.join(', ')}` : ''}
142 | `;
143 | })
144 | .join('\n');
145 |
146 | // Add pagination information if available
147 | let response_text = `Found ${bots.length} bots:\n\n${formattedBots}`;
148 |
149 | if (response.nextCursor) {
150 | response_text += `\nMore results available. Use cursor: ${response.nextCursor} to see the next page.`;
151 | }
152 |
153 | return response_text;
154 | } catch (error) {
155 | log.error('Error listing bots', { error: String(error) });
156 | return `Error listing bots: ${error instanceof Error ? error.message : String(error)}`;
157 | }
158 | },
159 | );
160 |
```
--------------------------------------------------------------------------------
/src/tools/qrcode.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * QR Code Generation Tool
3 | *
4 | * This tool generates an AI-powered QR code image that can be used as a bot avatar.
5 | * It calls the odin.qrcode-ai.com API to generate a customized QR code based on AI prompts.
6 | */
7 |
8 | import { z } from 'zod';
9 | import { createTool, MeetingBaaSTool } from '../utils/tool-types.js';
10 |
11 | // API configuration
12 | const QR_API_ENDPOINT = 'https://odin.qrcode-ai.com/api/qrcode';
13 | const DEFAULT_QR_API_KEY = 'qrc_o-Fx3GXW3TC7_cLvatIW1699177588300'; // Default key for demo purposes
14 |
15 | // Define available QR code styles
16 | const QR_STYLES = ['style_default', 'style_dots', 'style_rounded', 'style_crystal'] as const;
17 |
18 | // Define QR code types
19 | const QR_TYPES = ['url', 'email', 'phone', 'sms', 'text'] as const;
20 |
21 | // Define the parameters for the generate QR code tool
22 | const generateQRCodeParams = z.object({
23 | type: z.enum(QR_TYPES).describe('Type of QR code (url, email, phone, sms, text)'),
24 | to: z.string().describe('Destination for the QR code (URL, email, phone number, or text)'),
25 | prompt: z
26 | .string()
27 | .max(1000)
28 | .describe(
29 | 'AI prompt to customize the QR code (max 1000 characters). You can also include your API key in the prompt using format "API key: qrc_your_key"',
30 | ),
31 | style: z.enum(QR_STYLES).default('style_default').describe('Style of the QR code'),
32 | useAsBotImage: z
33 | .boolean()
34 | .default(true)
35 | .describe('Whether to use the generated QR code as the bot avatar'),
36 | template: z.string().optional().describe('Template ID for the QR code (optional)'),
37 | apiKey: z
38 | .string()
39 | .optional()
40 | .describe('Your QR Code AI API key (optional, will use default if not provided)'),
41 | });
42 |
43 | /**
44 | * Extracts a QR Code API key from the prompt text
45 | *
46 | * @param prompt The prompt text that might contain an API key
47 | * @returns The extracted API key or null if not found
48 | */
49 | function extractApiKeyFromPrompt(prompt: string): string | null {
50 | const patterns = [
51 | /api\s*key\s*[=:]\s*(qrc_[a-zA-Z0-9_-]+)/i,
52 | /using\s*api\s*key\s*[=:]\s*(qrc_[a-zA-Z0-9_-]+)/i,
53 | /with\s*api\s*key\s*[=:]\s*(qrc_[a-zA-Z0-9_-]+)/i,
54 | /api\s*key\s*is\s*(qrc_[a-zA-Z0-9_-]+)/i,
55 | /api\s*key\s*(qrc_[a-zA-Z0-9_-]+)/i,
56 | /(qrc_[a-zA-Z0-9_-]+)/i, // Last resort to just look for the key format
57 | ];
58 |
59 | for (const pattern of patterns) {
60 | const match = prompt.match(pattern);
61 | if (match && match[1]) {
62 | return match[1];
63 | }
64 | }
65 |
66 | return null;
67 | }
68 |
69 | /**
70 | * Cleans the prompt by removing any API key mentions
71 | *
72 | * @param prompt The original prompt text
73 | * @returns The cleaned prompt without API key mentions
74 | */
75 | function cleanPrompt(prompt: string): string {
76 | // Remove API key phrases
77 | let cleaned = prompt.replace(/(\s*api\s*key\s*[=:]\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
78 | cleaned = cleaned.replace(/(\s*using\s*api\s*key\s*[=:]\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
79 | cleaned = cleaned.replace(/(\s*with\s*api\s*key\s*[=:]\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
80 | cleaned = cleaned.replace(/(\s*api\s*key\s*is\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
81 | cleaned = cleaned.replace(/(\s*api\s*key\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
82 |
83 | // Remove just the key if it exists independently
84 | cleaned = cleaned.replace(/(\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
85 |
86 | // Trim and clean up double spaces
87 | cleaned = cleaned.trim().replace(/\s+/g, ' ');
88 |
89 | return cleaned;
90 | }
91 |
92 | /**
93 | * Generate QR Code Tool
94 | *
95 | * This tool generates an AI-powered QR code that can be used as a bot avatar.
96 | */
97 | export const generateQRCodeTool: MeetingBaaSTool<typeof generateQRCodeParams> = createTool(
98 | 'generateQRCode',
99 | 'Generate an AI-powered QR code that can be used as a bot avatar. You can include your API key directly in the prompt by saying "API key: qrc_your_key".',
100 | generateQRCodeParams,
101 | async (args, context) => {
102 | const { log } = context;
103 | log.info('Generating QR code', { type: args.type, prompt: args.prompt });
104 |
105 | // 1. Look for API key in the prompt text
106 | const promptApiKey = extractApiKeyFromPrompt(args.prompt);
107 |
108 | // 2. Clean the prompt by removing API key mentions if found
109 | const cleanedPrompt = cleanPrompt(args.prompt);
110 |
111 | // 3. Determine which API key to use (priority: 1. Param API key, 2. Prompt API key, 3. Environment variable)
112 | // Check for QRCODE_API_KEY in process.env or get from config if available
113 | const defaultApiKey = process.env.QRCODE_API_KEY || DEFAULT_QR_API_KEY || '';
114 | const effectiveApiKey = args.apiKey || promptApiKey || defaultApiKey;
115 |
116 | // Log which key is being used (without revealing the actual key)
117 | log.info(
118 | `Using QR Code API key from: ${args.apiKey ? 'parameter' : promptApiKey ? 'prompt' : defaultApiKey === DEFAULT_QR_API_KEY ? 'default' : 'environment'}`,
119 | );
120 |
121 | try {
122 | const response = await fetch(QR_API_ENDPOINT, {
123 | method: 'POST',
124 | headers: {
125 | 'Content-Type': 'application/json',
126 | 'x-api-key': effectiveApiKey,
127 | },
128 | body: JSON.stringify({
129 | type: args.type,
130 | to: args.to,
131 | prompt: cleanedPrompt, // Use the cleaned prompt without API key
132 | style: args.style,
133 | template: args.template || '67d30dd4d22a25b77317f407', // Default template
134 | }),
135 | });
136 |
137 | if (!response.ok) {
138 | const errorText = await response.text();
139 | let errorData;
140 | try {
141 | errorData = JSON.parse(errorText);
142 | } catch (e) {
143 | errorData = { message: errorText };
144 | }
145 |
146 | log.error('QR Code API error:', { status: response.status, error: errorData });
147 | return {
148 | content: [
149 | {
150 | type: 'text' as const,
151 | text: `QR code generation failed: ${response.status} ${errorData.message || errorText}`,
152 | },
153 | ],
154 | isError: true,
155 | };
156 | }
157 |
158 | const data = await response.json();
159 |
160 | if (!data.qrcode?.url) {
161 | log.error('QR code URL not found in response', { data });
162 | return {
163 | content: [
164 | {
165 | type: 'text' as const,
166 | text: 'QR code URL not found in response',
167 | },
168 | ],
169 | isError: true,
170 | };
171 | }
172 |
173 | // Return the QR code URL
174 | const qrCodeUrl = data.qrcode.url;
175 | const responseText =
176 | `QR code generated successfully!\n\n` +
177 | `URL: ${qrCodeUrl}\n\n` +
178 | `This image ${args.useAsBotImage ? 'can be used' : 'will not be used'} as a bot avatar.\n\n` +
179 | `To create a bot with this QR code image, use the joinMeeting tool with botImage: "${qrCodeUrl}"`;
180 |
181 | return {
182 | content: [
183 | {
184 | type: 'text' as const,
185 | text: responseText,
186 | },
187 | ],
188 | isError: false,
189 | metadata: {
190 | qrCodeUrl: qrCodeUrl,
191 | useAsBotImage: args.useAsBotImage,
192 | },
193 | };
194 | } catch (error: unknown) {
195 | log.error('Error generating QR code', { error: String(error) });
196 | return {
197 | content: [
198 | {
199 | type: 'text' as const,
200 | text: `Error generating QR code: ${error instanceof Error ? error.message : String(error)}`,
201 | },
202 | ],
203 | isError: true,
204 | };
205 | }
206 | },
207 | );
208 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Meeting BaaS MCP Server
3 | *
4 | * Connects Claude and other AI assistants to Meeting BaaS API,
5 | * allowing them to manage recordings, transcripts, and calendar data.
6 | */
7 |
8 | import { FastMCP } from 'fastmcp';
9 | import { promises as fs } from 'fs';
10 | import * as os from 'os';
11 | import * as path from 'path';
12 | import { z } from 'zod';
13 | import { SERVER_CONFIG } from './config.js';
14 |
15 | // Import tools from the consolidated export
16 | import {
17 | cancelRecordingTool,
18 | cancelRecordingWithCredentialsTool,
19 | checkCalendarIntegrationTool,
20 | deleteCalendarTool,
21 | deleteDataTool,
22 | findKeyMomentsTool,
23 | generateQRCodeTool,
24 | getCalendarTool,
25 | getEventTool,
26 | getMeetingDataTool,
27 | getMeetingDataWithCredentialsTool,
28 | getTranscriptTool,
29 | // Meeting tools
30 | joinMeetingTool,
31 | leaveMeetingTool,
32 | listBotsWithMetadataTool,
33 | listCalendarsTool,
34 | listEventsTool,
35 | listEventsWithCredentialsTool,
36 | listRawCalendarsTool,
37 | listUpcomingMeetingsTool,
38 | // Calendar tools
39 | oauthGuidanceTool,
40 | resyncAllCalendarsTool,
41 | retranscribeTool,
42 | scheduleRecordingTool,
43 | scheduleRecordingWithCredentialsTool,
44 | // Environment tools
45 | selectEnvironmentTool,
46 | setupCalendarOAuthTool,
47 | // Link tools
48 | shareableMeetingLinkTool,
49 | shareMeetingSegmentsTool,
50 | } from './tools/index.js';
51 |
52 | // Import resources
53 | import { meetingMetadataResource, meetingTranscriptResource } from './resources/index.js';
54 |
55 | // Define session auth type
56 | type SessionAuth = { apiKey: string };
57 |
58 | // Set up proper error logging
59 | // This ensures logs go to stderr instead of stdout to avoid interfering with JSON communication
60 | import { createServerLogger, setupPingFiltering } from './utils/logging.js';
61 |
62 | // Set up ping message filtering to reduce log noise
63 | setupPingFiltering();
64 |
65 | const serverLog = createServerLogger('MCP Server');
66 |
67 | // Add global error handlers to prevent crashes
68 | process.on('unhandledRejection', (reason, promise) => {
69 | // Check if this is a connection closed error from the MCP protocol
70 | const error = reason as any;
71 | if (error && error.code === -32000 && error.message?.includes('Connection closed')) {
72 | serverLog(`Connection closed gracefully, ignoring error`);
73 | } else {
74 | serverLog(`Unhandled Rejection: ${error?.message || String(reason)}`);
75 | console.error('[MCP Server] Error details:', reason);
76 | }
77 | });
78 |
79 | process.on('uncaughtException', (error) => {
80 | // Check if this is a connection closed error from the MCP protocol
81 | const err = error as any; // Cast to any to access non-standard properties
82 | if (err && err.code === 'ERR_UNHANDLED_ERROR' && err.context?.error?.code === -32000) {
83 | serverLog(`Connection closed gracefully, ignoring exception`);
84 | } else {
85 | serverLog(`Uncaught Exception: ${error?.message || String(error)}`);
86 | console.error('[MCP Server] Exception details:', error);
87 | }
88 | });
89 |
90 | // Log startup information
91 | serverLog('========== SERVER STARTUP ==========');
92 | serverLog(`Server version: ${SERVER_CONFIG.version}`);
93 | serverLog(`Node version: ${process.version}`);
94 | serverLog(`Running from Claude: ${process.env.MCP_FROM_CLAUDE === 'true' ? 'Yes' : 'No'}`);
95 | serverLog(`Process ID: ${process.pid}`);
96 |
97 | // Function to load and process the Claude Desktop config file
98 | async function loadClaudeDesktopConfig() {
99 | try {
100 | // Define the expected config path
101 | const configPath = path.join(
102 | os.homedir(),
103 | 'Library/Application Support/Claude/claude_desktop_config.json',
104 | );
105 |
106 | const fileExists = await fs
107 | .stat(configPath)
108 | .then(() => true)
109 | .catch(() => false);
110 | if (fileExists) {
111 | serverLog(`Loading config from: ${configPath}`);
112 | try {
113 | const configContent = await fs.readFile(configPath, 'utf8');
114 | const configJson = JSON.parse(configContent);
115 |
116 | // Check for meetingbaas server config
117 | if (configJson.mcpServers && configJson.mcpServers.meetingbaas) {
118 | const serverConfig = configJson.mcpServers.meetingbaas;
119 |
120 | // Check for headers
121 | if (serverConfig.headers) {
122 | // Check for API key header and set it as an environment variable
123 | if (serverConfig.headers['x-api-key']) {
124 | const apiKey = serverConfig.headers['x-api-key'];
125 | process.env.MEETING_BAAS_API_KEY = apiKey;
126 | serverLog(`API key loaded from config`);
127 | }
128 |
129 | // Check for QR code API key in headers
130 | if (serverConfig.headers['x-api-key']) {
131 | const qrCodeApiKey = serverConfig.headers['x-api-key'];
132 | process.env.QRCODE_API_KEY = qrCodeApiKey;
133 | serverLog(`QR code API key loaded from config`);
134 | }
135 | }
136 |
137 | // Check for bot configuration
138 | if (serverConfig.botConfig) {
139 | const botConfig = serverConfig.botConfig;
140 | let configItems = [];
141 |
142 | // Set bot name if available
143 | if (botConfig.name) {
144 | process.env.MEETING_BOT_NAME = botConfig.name;
145 | configItems.push('name');
146 | }
147 |
148 | // Set bot image if available
149 | if (botConfig.image) {
150 | process.env.MEETING_BOT_IMAGE = botConfig.image;
151 | configItems.push('image');
152 | }
153 |
154 | // Set bot entry message if available
155 | if (botConfig.entryMessage) {
156 | process.env.MEETING_BOT_ENTRY_MESSAGE = botConfig.entryMessage;
157 | configItems.push('message');
158 | }
159 |
160 | // Set extra fields if available
161 | if (botConfig.extra) {
162 | process.env.MEETING_BOT_EXTRA = JSON.stringify(botConfig.extra);
163 | configItems.push('extra');
164 | }
165 |
166 | if (configItems.length > 0) {
167 | serverLog(`Bot configuration loaded from config: ${configItems.join(', ')}`);
168 | }
169 | }
170 | }
171 | } catch (parseError) {
172 | serverLog(`Error parsing config file: ${parseError}`);
173 | }
174 | } else {
175 | serverLog(`Config file not found at ${configPath}`);
176 | }
177 | } catch (error) {
178 | serverLog(`Error loading config file: ${error}`);
179 | }
180 | }
181 |
182 | // Load the Claude Desktop config and set up the server
183 | (async () => {
184 | // Load and log the Claude Desktop config
185 | await loadClaudeDesktopConfig();
186 |
187 | // Configure the server
188 | const server = new FastMCP<SessionAuth>({
189 | name: SERVER_CONFIG.name,
190 | version: '1.0.0', // Using explicit semantic version format
191 | authenticate: async (context: any) => {
192 | // Use 'any' for now to avoid type errors
193 | try {
194 | // Get API key from headers, trying multiple possible locations
195 | let apiKey =
196 | // If request object exists (FastMCP newer versions)
197 | (context.request?.headers && context.request.headers['x-api-key']) ||
198 | // Or if headers are directly on the context (older versions)
199 | (context.headers && context.headers['x-api-key']);
200 |
201 | // If API key wasn't found in headers, try environment variable as fallback
202 | if (!apiKey && process.env.MEETING_BAAS_API_KEY) {
203 | apiKey = process.env.MEETING_BAAS_API_KEY;
204 | serverLog(`Using API key from environment variable`);
205 | }
206 |
207 | if (!apiKey) {
208 | serverLog(`Authentication failed: No API key found`);
209 | throw new Response(null, {
210 | status: 401,
211 | statusText:
212 | 'API key required in x-api-key header or as MEETING_BAAS_API_KEY environment variable',
213 | });
214 | }
215 |
216 | // Ensure apiKey is a string
217 | const keyValue = Array.isArray(apiKey) ? apiKey[0] : apiKey;
218 |
219 | // Return a session object that will be accessible in context.session
220 | return { apiKey: keyValue };
221 | } catch (error) {
222 | serverLog(`Authentication error: ${error}`);
223 | throw new Response(null, {
224 | status: 500,
225 | statusText: 'Authentication error',
226 | });
227 | }
228 | },
229 | });
230 |
231 | // Register tools and add debug event listeners
232 | server.on('connect', (event) => {
233 | serverLog(`Client connected`);
234 | });
235 |
236 | server.on('disconnect', (event) => {
237 | serverLog(`Client disconnected`);
238 | });
239 |
240 | // Register tools using our helper function - only register the ones we've updated with MeetingBaaSTool
241 | registerTool(server, joinMeetingTool);
242 | registerTool(server, leaveMeetingTool);
243 | registerTool(server, getMeetingDataTool);
244 | registerTool(server, getMeetingDataWithCredentialsTool);
245 | registerTool(server, retranscribeTool);
246 |
247 | // For the rest, use the original method until we refactor them
248 | server.addTool(getTranscriptTool);
249 | server.addTool(oauthGuidanceTool);
250 | server.addTool(listRawCalendarsTool);
251 | server.addTool(setupCalendarOAuthTool);
252 | server.addTool(listCalendarsTool);
253 | server.addTool(getCalendarTool);
254 | server.addTool(deleteCalendarTool);
255 | server.addTool(resyncAllCalendarsTool);
256 | server.addTool(listUpcomingMeetingsTool);
257 | server.addTool(listEventsTool);
258 | server.addTool(listEventsWithCredentialsTool);
259 | server.addTool(getEventTool);
260 | server.addTool(scheduleRecordingTool);
261 | server.addTool(scheduleRecordingWithCredentialsTool);
262 | server.addTool(cancelRecordingTool);
263 | server.addTool(cancelRecordingWithCredentialsTool);
264 | server.addTool(checkCalendarIntegrationTool);
265 | server.addTool(shareableMeetingLinkTool);
266 | server.addTool(shareMeetingSegmentsTool);
267 | server.addTool(findKeyMomentsTool);
268 | server.addTool(deleteDataTool);
269 | server.addTool(listBotsWithMetadataTool);
270 | server.addTool(selectEnvironmentTool);
271 | server.addTool(generateQRCodeTool);
272 |
273 | // Register resources
274 | server.addResourceTemplate(meetingTranscriptResource);
275 | server.addResourceTemplate(meetingMetadataResource);
276 |
277 | // Determine transport type based on environment
278 | // If run from Claude Desktop, use stdio transport
279 | // Otherwise use SSE transport for web/HTTP interfaces
280 | const isClaudeDesktop = process.env.MCP_FROM_CLAUDE === 'true';
281 |
282 | const transportConfig = isClaudeDesktop
283 | ? {
284 | transportType: 'stdio' as const,
285 | debug: true,
286 | }
287 | : {
288 | transportType: 'sse' as const,
289 | sse: {
290 | endpoint: '/mcp' as `/${string}`,
291 | port: SERVER_CONFIG.port,
292 | },
293 | };
294 |
295 | // Start the server
296 | try {
297 | server.start(transportConfig);
298 |
299 | if (!isClaudeDesktop) {
300 | serverLog(`Meeting BaaS MCP Server started on http://localhost:${SERVER_CONFIG.port}/mcp`);
301 | } else {
302 | serverLog(`Meeting BaaS MCP Server started in stdio mode for Claude Desktop`);
303 | }
304 | } catch (error) {
305 | serverLog(`Error starting server: ${error}`);
306 | }
307 | })();
308 |
309 | import type { Tool } from 'fastmcp';
310 | import type { MeetingBaaSTool } from './utils/tool-types.js';
311 |
312 | /**
313 | * Helper function to safely register tools with the FastMCP server
314 | *
315 | * This function handles the type casting needed to satisfy TypeScript
316 | * while still using our properly designed MeetingBaaSTool interface
317 | */
318 | function registerTool<P extends z.ZodType>(
319 | server: FastMCP<SessionAuth>,
320 | tool: MeetingBaaSTool<P>,
321 | ): void {
322 | // Cast to any to bypass TypeScript's strict type checking
323 | // This is safe because our MeetingBaaSTool interface ensures compatibility
324 | server.addTool(tool as unknown as Tool<SessionAuth, P>);
325 | }
326 |
```
--------------------------------------------------------------------------------
/src/tools/meeting.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Meeting tool implementation
3 | */
4 |
5 | import * as fs from 'fs';
6 | import * as os from 'os';
7 | import * as path from 'path';
8 | import { z } from 'zod';
9 | import { apiRequest, MeetingBaasClient, SessionAuth } from '../api/client.js';
10 | import {
11 | AUDIO_FREQUENCIES,
12 | BOT_CONFIG,
13 | RECORDING_MODES,
14 | SPEECH_TO_TEXT_PROVIDERS,
15 | } from '../config.js';
16 | import { createValidSession } from '../utils/auth.js';
17 | import { createTool, MeetingBaaSTool } from '../utils/tool-types.js';
18 |
19 | // Define the parameters schemas
20 | const joinMeetingParams = z.object({
21 | meetingUrl: z.string().url().describe('URL of the meeting to join'),
22 | botName: z
23 | .string()
24 | .optional()
25 | .describe(
26 | 'Name to display for the bot in the meeting (OPTIONAL: if omitted, will use name from configuration)',
27 | ),
28 | botImage: z
29 | .string()
30 | .nullable()
31 | .optional()
32 | .describe(
33 | "URL to an image to use for the bot's avatar (OPTIONAL: if omitted, will use image from configuration)",
34 | ),
35 | entryMessage: z
36 | .string()
37 | .optional()
38 | .describe(
39 | 'Message the bot will send upon joining the meeting (OPTIONAL: if omitted, will use message from configuration)',
40 | ),
41 | deduplicationKey: z
42 | .string()
43 | .optional()
44 | .describe(
45 | 'Unique key to override the 5-minute restriction on joining the same meeting with the same API key',
46 | ),
47 | nooneJoinedTimeout: z
48 | .number()
49 | .int()
50 | .optional()
51 | .describe(
52 | 'Timeout in seconds for the bot to wait for participants to join before leaving (default: 600)',
53 | ),
54 | waitingRoomTimeout: z
55 | .number()
56 | .int()
57 | .optional()
58 | .describe(
59 | 'Timeout in seconds for the bot to wait in the waiting room before leaving (default: 600)',
60 | ),
61 | speechToTextProvider: z
62 | .enum(SPEECH_TO_TEXT_PROVIDERS)
63 | .optional()
64 | .describe('Speech-to-text provider to use for transcription (default: Default)'),
65 | speechToTextApiKey: z
66 | .string()
67 | .optional()
68 | .describe('API key for the speech-to-text provider (if required)'),
69 | streamingInputUrl: z
70 | .string()
71 | .optional()
72 | .describe('WebSocket URL to stream audio input to the bot'),
73 | streamingOutputUrl: z
74 | .string()
75 | .optional()
76 | .describe('WebSocket URL to stream audio output from the bot'),
77 | streamingAudioFrequency: z
78 | .enum(AUDIO_FREQUENCIES)
79 | .optional()
80 | .describe('Audio frequency for streaming (default: 16khz)'),
81 | reserved: z
82 | .boolean()
83 | .default(false)
84 | .describe(
85 | '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',
86 | ),
87 | startTime: z
88 | .string()
89 | .optional()
90 | .describe(
91 | 'ISO datetime string. If provided, the bot will join at this time instead of immediately',
92 | ),
93 | recordingMode: z.enum(RECORDING_MODES).default('speaker_view').describe('Recording mode'),
94 | extra: z
95 | .record(z.string(), z.any())
96 | .optional()
97 | .describe(
98 | 'Additional metadata for the meeting (e.g., meeting type, custom summary prompt, search keywords)',
99 | ),
100 | });
101 |
102 | /**
103 | * Parameters for getting meeting data
104 | */
105 | const getMeetingDetailsParams = z.object({
106 | meetingId: z.string().describe('ID of the meeting to get data for'),
107 | });
108 |
109 | /**
110 | * Parameters for getting meeting data with direct credentials
111 | */
112 | const getMeetingDetailsWithCredentialsParams = z.object({
113 | meetingId: z.string().describe('ID of the meeting to get data for'),
114 | apiKey: z.string().describe('API key for authentication'),
115 | });
116 |
117 | const stopRecordingParams = z.object({
118 | botId: z.string().uuid().describe('ID of the bot that recorded the meeting'),
119 | });
120 |
121 | // Define our return types
122 | export type JoinMeetingParams = z.infer<typeof joinMeetingParams>;
123 |
124 | /**
125 | * Function to directly read the Claude Desktop config
126 | */
127 | function readClaudeDesktopConfig(log: any) {
128 | try {
129 | // Define the expected config path
130 | const configPath = path.join(
131 | os.homedir(),
132 | 'Library/Application Support/Claude/claude_desktop_config.json',
133 | );
134 |
135 | if (fs.existsSync(configPath)) {
136 | const configContent = fs.readFileSync(configPath, 'utf8');
137 | const configJson = JSON.parse(configContent);
138 |
139 | // Check for meetingbaas server config
140 | if (configJson.mcpServers && configJson.mcpServers.meetingbaas) {
141 | const serverConfig = configJson.mcpServers.meetingbaas;
142 |
143 | // Check for bot configuration
144 | if (serverConfig.botConfig) {
145 | return serverConfig.botConfig;
146 | }
147 | }
148 | }
149 | return null;
150 | } catch (error) {
151 | log.error(`Error reading Claude Desktop config: ${error}`);
152 | return null;
153 | }
154 | }
155 |
156 | /**
157 | * Join a meeting
158 | */
159 | export const joinMeetingTool: MeetingBaaSTool<typeof joinMeetingParams> = createTool(
160 | 'joinMeeting',
161 | '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.',
162 | joinMeetingParams,
163 | async (args, context) => {
164 | const { session, log } = context;
165 |
166 | // Directly load Claude Desktop config
167 | const claudeConfig = readClaudeDesktopConfig(log);
168 |
169 | // Get bot name (user input, config, or prompt to provide)
170 | let botName = args.botName;
171 |
172 | // If no user-provided name, try Claude config, then BOT_CONFIG
173 | if (!botName) {
174 | if (claudeConfig && claudeConfig.name) {
175 | botName = claudeConfig.name;
176 | } else if (BOT_CONFIG.defaultBotName) {
177 | botName = BOT_CONFIG.defaultBotName;
178 | }
179 | }
180 |
181 | // Get bot image from various sources
182 | let botImage: string | null | undefined = args.botImage;
183 | if (botImage === undefined) {
184 | if (claudeConfig && claudeConfig.image) {
185 | botImage = claudeConfig.image;
186 | } else {
187 | botImage = BOT_CONFIG.defaultBotImage;
188 | }
189 | }
190 |
191 | // Get entry message from various sources
192 | let entryMessage = args.entryMessage;
193 | if (!entryMessage) {
194 | if (claudeConfig && claudeConfig.entryMessage) {
195 | entryMessage = claudeConfig.entryMessage;
196 | } else if (BOT_CONFIG.defaultEntryMessage) {
197 | entryMessage = BOT_CONFIG.defaultEntryMessage;
198 | }
199 | }
200 |
201 | // Get extra fields from various sources
202 | let extra = args.extra;
203 | if (!extra) {
204 | if (claudeConfig && claudeConfig.extra) {
205 | extra = claudeConfig.extra;
206 | } else if (BOT_CONFIG.defaultExtra) {
207 | extra = BOT_CONFIG.defaultExtra;
208 | }
209 | }
210 |
211 | // Only prompt for a name if no name is available from any source
212 | if (!botName) {
213 | log.info('No bot name available from any source');
214 | return {
215 | content: [
216 | {
217 | type: 'text' as const,
218 | text: 'Please provide a name for the bot that will join the meeting.',
219 | },
220 | ],
221 | isError: true,
222 | };
223 | }
224 |
225 | // Basic logging
226 | log.info('Joining meeting', { url: args.meetingUrl, botName });
227 |
228 | // Use our utility function to get a valid session with API key
229 | const validSession = createValidSession(session, log);
230 |
231 | // Verify we have an API key
232 | if (!validSession) {
233 | log.error('Authentication failed - no API key available');
234 | return {
235 | content: [
236 | {
237 | type: 'text' as const,
238 | text: 'Authentication failed. Please configure your API key in Claude Desktop settings.',
239 | },
240 | ],
241 | isError: true,
242 | };
243 | }
244 |
245 | // Prepare API request with the meeting details
246 | const payload = {
247 | meeting_url: args.meetingUrl,
248 | bot_name: botName,
249 | bot_image: botImage,
250 | entry_message: entryMessage,
251 | deduplication_key: args.deduplicationKey,
252 | reserved: args.reserved,
253 | recording_mode: args.recordingMode,
254 | start_time: args.startTime,
255 | automatic_leave:
256 | args.nooneJoinedTimeout || args.waitingRoomTimeout
257 | ? {
258 | noone_joined_timeout: args.nooneJoinedTimeout,
259 | waiting_room_timeout: args.waitingRoomTimeout,
260 | }
261 | : undefined,
262 | speech_to_text: args.speechToTextProvider
263 | ? {
264 | provider: args.speechToTextProvider,
265 | api_key: args.speechToTextApiKey,
266 | }
267 | : undefined,
268 | streaming:
269 | args.streamingInputUrl || args.streamingOutputUrl || args.streamingAudioFrequency
270 | ? {
271 | input: args.streamingInputUrl,
272 | output: args.streamingOutputUrl,
273 | audio_frequency: args.streamingAudioFrequency,
274 | }
275 | : undefined,
276 | extra: extra,
277 | };
278 |
279 | try {
280 | // Use the client to join the meeting with the API key from our valid session
281 | const client = new MeetingBaasClient(validSession.apiKey);
282 | const result = await client.joinMeeting(payload);
283 |
284 | // Prepare response message with details
285 | let responseMessage = `Bot named "${botName}" joined meeting successfully. Bot ID: ${result.bot_id}`;
286 | if (botImage) responseMessage += '\nCustom bot image is being used.';
287 | if (entryMessage) responseMessage += '\nThe bot will send an entry message.';
288 | if (args.startTime) {
289 | responseMessage += '\nThe bot is scheduled to join at the specified start time.';
290 | }
291 |
292 | return responseMessage;
293 | } catch (error) {
294 | log.error('Failed to join meeting', { error: String(error) });
295 | return {
296 | content: [
297 | {
298 | type: 'text' as const,
299 | text: `Failed to join meeting: ${error instanceof Error ? error.message : String(error)}`,
300 | },
301 | ],
302 | isError: true,
303 | };
304 | }
305 | },
306 | );
307 |
308 | /**
309 | * Leave a meeting
310 | */
311 | export const leaveMeetingTool: MeetingBaaSTool<typeof stopRecordingParams> = createTool(
312 | 'leaveMeeting',
313 | 'Have a bot leave an ongoing meeting',
314 | stopRecordingParams,
315 | async (args, context) => {
316 | const { session, log } = context;
317 | log.info('Leaving meeting', { botId: args.botId });
318 |
319 | // Create a valid session with fallbacks for API key
320 | const validSession = createValidSession(session, log);
321 |
322 | // Check if we have a valid session with API key
323 | if (!validSession) {
324 | return {
325 | content: [
326 | {
327 | type: 'text' as const,
328 | text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
329 | },
330 | ],
331 | isError: true,
332 | };
333 | }
334 |
335 | const response = await apiRequest(validSession, 'delete', `/bots/${args.botId}`);
336 | if (response.ok) {
337 | return 'Bot left the meeting successfully';
338 | } else {
339 | return `Failed to make bot leave: ${response.error || 'Unknown error'}`;
340 | }
341 | },
342 | );
343 |
344 | /**
345 | * Get meeting metadata and recording information (without transcript details)
346 | */
347 | export const getMeetingDataTool: MeetingBaaSTool<typeof getMeetingDetailsParams> = createTool(
348 | 'getMeetingData',
349 | 'Get recording metadata and video URL from a meeting',
350 | getMeetingDetailsParams,
351 | async (args, context) => {
352 | const { session, log } = context;
353 | log.info('Getting meeting metadata', { meetingId: args.meetingId });
354 |
355 | // Create a valid session with fallbacks for API key
356 | const validSession = createValidSession(session, log);
357 |
358 | // Check if we have a valid session with API key
359 | if (!validSession) {
360 | return {
361 | content: [
362 | {
363 | type: 'text' as const,
364 | text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
365 | },
366 | ],
367 | isError: true,
368 | };
369 | }
370 |
371 | const response = await apiRequest(
372 | validSession,
373 | 'get',
374 | `/bots/meeting_data?bot_id=${args.meetingId}`,
375 | );
376 |
377 | // Format the meeting duration
378 | const duration = response.duration;
379 | const minutes = Math.floor(duration / 60);
380 | const seconds = duration % 60;
381 |
382 | // Extract the meeting name and URL
383 | const meetingName = response.bot_data.bot?.bot_name || 'Unknown Meeting';
384 | const meetingUrl = response.bot_data.bot?.meeting_url || 'Unknown URL';
385 |
386 | return {
387 | content: [
388 | {
389 | type: 'text' as const,
390 | text: `Meeting: "${meetingName}"\nURL: ${meetingUrl}\nDuration: ${minutes}m ${seconds}s\n\nRecording is available at: ${response.mp4}`,
391 | },
392 | ],
393 | isError: false,
394 | };
395 | },
396 | );
397 |
398 | /**
399 | * Get meeting metadata and recording information with direct credentials
400 | */
401 | export const getMeetingDataWithCredentialsTool: MeetingBaaSTool<
402 | typeof getMeetingDetailsWithCredentialsParams
403 | > = createTool(
404 | 'getMeetingDataWithCredentials',
405 | 'Get recording metadata and video URL from a meeting using direct API credentials',
406 | getMeetingDetailsWithCredentialsParams,
407 | async (args, context) => {
408 | const { log } = context;
409 | log.info('Getting meeting metadata with direct credentials', { meetingId: args.meetingId });
410 |
411 | // Create a session object with the provided API key
412 | const session: SessionAuth = { apiKey: args.apiKey };
413 |
414 | const response = await apiRequest(
415 | session,
416 | 'get',
417 | `/bots/meeting_data?bot_id=${args.meetingId}`,
418 | );
419 |
420 | // Format the meeting duration
421 | const duration = response.duration;
422 | const minutes = Math.floor(duration / 60);
423 | const seconds = duration % 60;
424 |
425 | // Extract the meeting name and URL
426 | const meetingName = response.bot_data.bot?.bot_name || 'Unknown Meeting';
427 | const meetingUrl = response.bot_data.bot?.meeting_url || 'Unknown URL';
428 |
429 | return {
430 | content: [
431 | {
432 | type: 'text' as const,
433 | text: `Meeting: "${meetingName}"\nURL: ${meetingUrl}\nDuration: ${minutes}m ${seconds}s\n\nRecording is available at: ${response.mp4}`,
434 | },
435 | ],
436 | isError: false,
437 | };
438 | },
439 | );
440 |
```
--------------------------------------------------------------------------------
/set_up_for_other_mcp.md:
--------------------------------------------------------------------------------
```markdown
1 | # Meeting BaaS MCP Server Setup Guide
2 |
3 | This guide explains how to set up a Meeting BaaS MCP server that connects AI assistants to the Meeting BaaS API for managing recordings, transcripts, and calendar data.
4 |
5 | ## Prerequisites
6 |
7 | - Node.js (v16 or higher)
8 | - npm (v8 or higher)
9 | - Redis (optional, for state management)
10 |
11 | ## Project Setup
12 |
13 | 1. Create a new directory for your project and initialize it:
14 |
15 | ```bash
16 | mkdir my-mcp-server
17 | cd my-mcp-server
18 | npm init -y
19 | ```
20 |
21 | 2. Update your package.json with the necessary dependencies:
22 |
23 | ```json
24 | {
25 | "name": "meetingbaas-mcp",
26 | "version": "1.0.0",
27 | "description": "Meeting BaaS MCP Server",
28 | "type": "module",
29 | "main": "dist/index.js",
30 | "scripts": {
31 | "build": "tsc",
32 | "start": "node dist/index.js",
33 | "dev": "ts-node-esm src/index.ts",
34 | "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
35 | "prepare": "husky install"
36 | },
37 | "dependencies": {
38 | "@meeting-baas/sdk": "^0.2.0",
39 | "axios": "^1.6.0",
40 | "express": "^4.18.2",
41 | "fastmcp": "^1.20.2",
42 | "redis": "^4.6.13",
43 | "zod": "^3.22.4"
44 | },
45 | "devDependencies": {
46 | "@types/express": "^4.17.21",
47 | "@types/node": "^20.11.19",
48 | "@types/redis": "^4.0.11",
49 | "husky": "^8.0.3",
50 | "prettier": "^3.1.0",
51 | "ts-node": "^10.9.2",
52 | "typescript": "^5.3.3"
53 | }
54 | }
55 | ```
56 |
57 | 3. Install the dependencies:
58 |
59 | ```bash
60 | npm install
61 | ```
62 |
63 | 4. Create a TypeScript configuration file (tsconfig.json):
64 |
65 | ```json
66 | {
67 | "compilerOptions": {
68 | "target": "ES2020",
69 | "module": "NodeNext",
70 | "moduleResolution": "NodeNext",
71 | "esModuleInterop": true,
72 | "strict": true,
73 | "outDir": "dist",
74 | "skipLibCheck": true,
75 | "forceConsistentCasingInFileNames": true,
76 | "declaration": true
77 | },
78 | "include": ["src/**/*", "lib/**/*"],
79 | "exclude": ["node_modules", "dist"]
80 | }
81 | ```
82 |
83 | ## File Structure
84 |
85 | Create the following directory structure:
86 |
87 | ```
88 | my-mcp-server/
89 | ├── lib/
90 | │ ├── mcp-api-handler.ts
91 | │ └── tool-adapter.ts
92 | ├── src/
93 | │ ├── api/
94 | │ │ └── client.ts
95 | │ ├── tools/
96 | │ │ ├── index.ts
97 | │ │ ├── calendar.ts
98 | │ │ ├── deleteData.ts
99 | │ │ ├── environment.ts
100 | │ │ ├── links.ts
101 | │ │ ├── listBots.ts
102 | │ │ ├── meeting.ts
103 | │ │ ├── qrcode.ts
104 | │ │ ├── retranscribe.ts
105 | │ │ └── search.ts
106 | │ ├── utils/
107 | │ │ ├── logging.ts
108 | │ │ └── tool-types.ts
109 | │ └── index.ts
110 | └── tsconfig.json
111 | ```
112 |
113 | ## Key Files Implementation
114 |
115 | ### 1. lib/mcp-api-handler.ts
116 |
117 | ```typescript
118 | import type { Request, Response } from 'express';
119 | import type { Context, FastMCP } from 'fastmcp';
120 | import { createClient } from 'redis';
121 |
122 | interface McpApiHandlerOptions {
123 | capabilities?: {
124 | tools?: Record<string, { description: string }>;
125 | };
126 | port?: number;
127 | transportType?: 'stdio' | 'sse';
128 | sse?: {
129 | endpoint: `/${string}`;
130 | };
131 | }
132 |
133 | export function initializeMcpApiHandler(
134 | setupServer: (server: FastMCP) => Promise<void>,
135 | options: McpApiHandlerOptions = {},
136 | ): (req: Request, res: Response) => Promise<void> {
137 | const {
138 | capabilities = {},
139 | port = 3000,
140 | transportType = 'sse',
141 | sse = { endpoint: '/mcp' },
142 | } = options;
143 |
144 | // Initialize Redis client
145 | const redisClient = createClient({
146 | url: process.env.REDIS_URL || 'redis://localhost:6379',
147 | });
148 |
149 | // Initialize FastMCP server
150 | const server = new FastMCP({
151 | name: 'Meeting BaaS MCP Server',
152 | version: '1.0.0' as const,
153 | authenticate: async (context: Context) => {
154 | const apiKey = context.request?.headers?.['x-api-key'];
155 | if (!apiKey) {
156 | throw new Error('API key required');
157 | }
158 | return { apiKey: String(apiKey) };
159 | },
160 | });
161 |
162 | // Set up server capabilities
163 | server.setCapabilities(capabilities);
164 |
165 | // Set up server tools
166 | setupServer(server).catch((error) => {
167 | console.error('Error setting up server:', error);
168 | });
169 |
170 | // Handle SSE transport
171 | if (transportType === 'sse') {
172 | return async (req: Request, res: Response) => {
173 | if (req.path !== sse.endpoint) {
174 | res.status(404).send('Not found');
175 | return;
176 | }
177 |
178 | res.setHeader('Content-Type', 'text/event-stream');
179 | res.setHeader('Cache-Control', 'no-cache');
180 | res.setHeader('Connection', 'keep-alive');
181 |
182 | const transport = server.createTransport('sse', {
183 | request: req,
184 | response: res,
185 | });
186 |
187 | await server.handleTransport(transport);
188 | };
189 | }
190 |
191 | // Handle stdio transport
192 | if (transportType === 'stdio') {
193 | const transport = server.createTransport('stdio', {
194 | stdin: process.stdin,
195 | stdout: process.stdout,
196 | });
197 |
198 | server.handleTransport(transport).catch((error) => {
199 | console.error('Error handling stdio transport:', error);
200 | });
201 | }
202 |
203 | // Return a no-op handler for non-SSE requests
204 | return async (req: Request, res: Response) => {
205 | res.status(404).send('Not found');
206 | };
207 | }
208 | ```
209 |
210 | ### 2. lib/tool-adapter.ts
211 |
212 | ```typescript
213 | import type { Context, Tool } from 'fastmcp';
214 | import type { SessionAuth } from '../src/api/client.js';
215 | import type { MeetingBaaSTool } from '../src/utils/tool-types.js';
216 |
217 | // Adapter function to convert Meeting BaaS tools to FastMCP tools
218 | export function adaptTool<P extends Record<string, any>>(
219 | meetingTool: MeetingBaaSTool<any>
220 | ): Tool<SessionAuth, any> {
221 | return {
222 | name: meetingTool.name,
223 | description: meetingTool.description,
224 | parameters: meetingTool.parameters,
225 | execute: meetingTool.execute,
226 | };
227 | }
228 | ```
229 |
230 | ### 3. src/utils/tool-types.ts
231 |
232 | ```typescript
233 | import type { z } from 'zod';
234 | import type { Context } from 'fastmcp';
235 | import type { SessionAuth } from '../api/client.js';
236 |
237 | /**
238 | * Type for tool execution result
239 | */
240 | export type ToolResult = {
241 | content: { type: 'text'; text: string }[];
242 | };
243 |
244 | /**
245 | * Base type for Meeting BaaS tools
246 | */
247 | export type MeetingBaaSTool<P extends z.ZodType> = {
248 | name: string;
249 | description: string;
250 | parameters: P;
251 | execute: (params: z.infer<P>, context: Context<SessionAuth>) => Promise<ToolResult>;
252 | };
253 |
254 | /**
255 | * Helper function to create a Meeting BaaS tool with the correct type
256 | */
257 | export function createTool<P extends z.ZodType>(
258 | name: string,
259 | description: string,
260 | parameters: P,
261 | execute: (params: z.infer<P>, context: Context<SessionAuth>) => Promise<ToolResult>
262 | ): MeetingBaaSTool<P> {
263 | return {
264 | name,
265 | description,
266 | parameters,
267 | execute,
268 | };
269 | }
270 | ```
271 |
272 | ### 4. src/utils/logging.ts
273 |
274 | ```typescript
275 | /**
276 | * Creates a server logger
277 | *
278 | * @param prefix - Prefix to add to log messages
279 | * @returns Logging function
280 | */
281 | export function createServerLogger(prefix: string) {
282 | return function log(message: string) {
283 | const timestamp = new Date().toISOString();
284 | console.error(`[${timestamp}] [${prefix}] ${message}`);
285 | };
286 | }
287 |
288 | /**
289 | * Sets up filtering for ping messages
290 | *
291 | * This reduces log noise by filtering out the periodic ping messages
292 | */
293 | export function setupPingFiltering() {
294 | const originalStdoutWrite = process.stdout.write.bind(process.stdout);
295 | const originalStderrWrite = process.stderr.write.bind(process.stderr);
296 |
297 | // Replace stdout.write
298 | process.stdout.write = ((
299 | chunk: string | Uint8Array,
300 | encoding?: BufferEncoding,
301 | callback?: (err?: Error) => void
302 | ) => {
303 | // Filter out ping messages
304 | if (typeof chunk === 'string' && chunk.includes('"method":"ping"')) {
305 | return true;
306 | }
307 | return originalStdoutWrite(chunk, encoding, callback);
308 | }) as typeof process.stdout.write;
309 |
310 | // Replace stderr.write
311 | process.stderr.write = ((
312 | chunk: string | Uint8Array,
313 | encoding?: BufferEncoding,
314 | callback?: (err?: Error) => void
315 | ) => {
316 | // Filter out ping messages
317 | if (typeof chunk === 'string' && chunk.includes('"method":"ping"')) {
318 | return true;
319 | }
320 | return originalStderrWrite(chunk, encoding, callback);
321 | }) as typeof process.stderr.write;
322 | }
323 | ```
324 |
325 | ### 5. src/api/client.ts
326 |
327 | ```typescript
328 | import axios from 'axios';
329 |
330 | // Session auth type for authenticating with the Meeting BaaS API
331 | export type SessionAuth = { apiKey: string };
332 |
333 | // Create an API client with the session auth
334 | export function createApiClient(session: SessionAuth) {
335 | const apiUrl = process.env.MEETING_BAAS_API_URL || 'https://api.meetingbaas.com';
336 |
337 | return axios.create({
338 | baseURL: apiUrl,
339 | headers: {
340 | 'Content-Type': 'application/json',
341 | 'x-api-key': session.apiKey,
342 | },
343 | });
344 | }
345 | ```
346 |
347 | ### 6. src/index.ts
348 |
349 | ```typescript
350 | /**
351 | * Meeting BaaS MCP Server
352 | *
353 | * Connects Claude and other AI assistants to Meeting BaaS API,
354 | * allowing them to manage recordings, transcripts, and calendar data.
355 | */
356 |
357 | import { BaasClient, MpcClient } from '@meeting-baas/sdk';
358 | import type { Context } from 'fastmcp';
359 | import { FastMCP } from 'fastmcp';
360 | import { z } from 'zod';
361 | import { promises as fs } from 'fs';
362 | import * as os from 'os';
363 | import * as path from 'path';
364 | import { initializeMcpApiHandler } from '../lib/mcp-api-handler.js';
365 | import { adaptTool } from '../lib/tool-adapter.js';
366 | import type { SessionAuth } from './api/client.js';
367 | import { createTool } from './utils/tool-types.js';
368 |
369 | // Set up proper error logging
370 | import { createServerLogger, setupPingFiltering } from './utils/logging.js';
371 |
372 | // Set up ping message filtering to reduce log noise
373 | setupPingFiltering();
374 |
375 | const serverLog = createServerLogger('MCP Server');
376 |
377 | // Add global error handlers to prevent crashes
378 | process.on('unhandledRejection', (reason, promise) => {
379 | // Check if this is a connection closed error from the MCP protocol
380 | const error = reason as any;
381 | if (error && error.code === -32000 && error.message?.includes('Connection closed')) {
382 | serverLog(`Connection closed gracefully, ignoring error`);
383 | } else {
384 | serverLog(`Unhandled Rejection: ${error?.message || String(reason)}`);
385 | console.error('[MCP Server] Error details:', reason);
386 | }
387 | });
388 |
389 | process.on('uncaughtException', (error) => {
390 | // Check if this is a connection closed error from the MCP protocol
391 | const err = error as any;
392 | if (err && err.code === 'ERR_UNHANDLED_ERROR' && err.context?.error?.code === -32000) {
393 | serverLog(`Connection closed gracefully, ignoring exception`);
394 | } else {
395 | serverLog(`Uncaught Exception: ${error?.message || String(error)}`);
396 | console.error('[MCP Server] Exception details:', error);
397 | }
398 | });
399 |
400 | // Log startup information
401 | serverLog('========== SERVER STARTUP ==========');
402 | serverLog(`Node version: ${process.version}`);
403 | serverLog(`Running from Claude: ${process.env.MCP_FROM_CLAUDE === 'true' ? 'Yes' : 'No'}`);
404 | serverLog(`Process ID: ${process.pid}`);
405 |
406 | // Function to load and process the Claude Desktop config file
407 | async function loadClaudeDesktopConfig() {
408 | try {
409 | // Define the expected config path
410 | const configPath = path.join(
411 | os.homedir(),
412 | 'Library/Application Support/Claude/claude_desktop_config.json',
413 | );
414 |
415 | const fileExists = await fs
416 | .stat(configPath)
417 | .then(() => true)
418 | .catch(() => false);
419 | if (fileExists) {
420 | serverLog(`Loading config from: ${configPath}`);
421 | try {
422 | const configContent = await fs.readFile(configPath, 'utf8');
423 | const configJson = JSON.parse(configContent);
424 |
425 | // Check for meetingbaas server config
426 | if (configJson.mcpServers && configJson.mcpServers.meetingbaas) {
427 | const serverConfig = configJson.mcpServers.meetingbaas;
428 |
429 | // Check for headers
430 | if (serverConfig.headers) {
431 | // Check for API key header and set it as an environment variable
432 | if (serverConfig.headers['x-api-key']) {
433 | const apiKey = serverConfig.headers['x-api-key'];
434 | process.env.MEETING_BAAS_API_KEY = apiKey;
435 | serverLog(`API key loaded from config`);
436 | }
437 |
438 | // Check for QR code API key in headers
439 | if (serverConfig.headers['x-api-key']) {
440 | const qrCodeApiKey = serverConfig.headers['x-api-key'];
441 | process.env.QRCODE_API_KEY = qrCodeApiKey;
442 | serverLog(`QR code API key loaded from config`);
443 | }
444 | }
445 |
446 | // Check for bot configuration
447 | if (serverConfig.botConfig) {
448 | const botConfig = serverConfig.botConfig;
449 | let configItems = [];
450 |
451 | // Set bot name if available
452 | if (botConfig.name) {
453 | process.env.MEETING_BOT_NAME = botConfig.name;
454 | configItems.push('name');
455 | }
456 |
457 | // Set bot image if available
458 | if (botConfig.image) {
459 | process.env.MEETING_BOT_IMAGE = botConfig.image;
460 | configItems.push('image');
461 | }
462 |
463 | // Set bot entry message if available
464 | if (botConfig.entryMessage) {
465 | process.env.MEETING_BOT_ENTRY_MESSAGE = botConfig.entryMessage;
466 | configItems.push('message');
467 | }
468 |
469 | // Set extra fields if available
470 | if (botConfig.extra) {
471 | process.env.MEETING_BOT_EXTRA = JSON.stringify(botConfig.extra);
472 | configItems.push('extra');
473 | }
474 |
475 | if (configItems.length > 0) {
476 | serverLog(`Bot configuration loaded from config: ${configItems.join(', ')}`);
477 | }
478 | }
479 | }
480 | } catch (parseError) {
481 | serverLog(`Error parsing config file: ${parseError}`);
482 | }
483 | } else {
484 | serverLog(`Config file not found at ${configPath}`);
485 | }
486 | } catch (error) {
487 | serverLog(`Error loading config file: ${error}`);
488 | }
489 | }
490 |
491 | // Initialize the BaaS client
492 | const baasClient = new BaasClient({
493 | apiKey: process.env.MEETING_BAAS_API_KEY || '',
494 | });
495 |
496 | // Initialize MPC client for tool registration
497 | const mpcClient = new MpcClient({
498 | serverUrl: process.env.MPC_SERVER_URL || 'http://localhost:7020',
499 | });
500 |
501 | interface ToolParameter {
502 | name: string;
503 | required?: boolean;
504 | schema?: {
505 | type: string;
506 | };
507 | }
508 |
509 | // Helper function to convert MPC parameter definition to Zod schema
510 | function convertToZodSchema(parameters: ToolParameter[]): z.ZodType {
511 | const schema: Record<string, z.ZodType> = {};
512 | for (const param of parameters) {
513 | if (param.required) {
514 | schema[param.name] = z.string(); // Default to string for now, can be expanded based on param.schema.type
515 | } else {
516 | schema[param.name] = z.string().optional();
517 | }
518 | }
519 | return z.object(schema);
520 | }
521 |
522 | const handler = initializeMcpApiHandler(
523 | async (server: FastMCP) => {
524 | // Register all Meeting BaaS tools automatically
525 | const tools = mpcClient.getRegisteredTools();
526 | for (const tool of tools) {
527 | const paramsSchema = convertToZodSchema(tool.parameters || []);
528 | const meetingTool = createTool(
529 | tool.name,
530 | tool.description || 'Meeting BaaS tool',
531 | paramsSchema,
532 | async (params: Record<string, unknown>, context: Context<SessionAuth>) => {
533 | // Handle tool execution here
534 | return {
535 | content: [{ type: 'text', text: `Tool ${tool.name} executed` }],
536 | };
537 | },
538 | );
539 | server.addTool(adaptTool(meetingTool));
540 | }
541 |
542 | // Keep the existing echo tool as an example
543 | const echoTool = createTool(
544 | 'echo',
545 | 'Echo a message',
546 | z.object({ message: z.string() }),
547 | async ({ message }: { message: string }, context: Context<SessionAuth>) => ({
548 | content: [{ type: 'text', text: `Tool echo: ${message}` }],
549 | }),
550 | );
551 | server.addTool(adaptTool(echoTool));
552 | },
553 | {
554 | capabilities: {
555 | tools: {
556 | echo: {
557 | description: 'Echo a message',
558 | },
559 | // Meeting BaaS tools will be automatically added to capabilities
560 | },
561 | },
562 | port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
563 | transportType: process.env.MCP_FROM_CLAUDE === 'true' ? 'stdio' : 'sse',
564 | sse: {
565 | endpoint: '/mcp' as `/${string}`,
566 | },
567 | },
568 | );
569 |
570 | // Load the Claude Desktop config and start the server
571 | (async () => {
572 | // Load and log the Claude Desktop config
573 | await loadClaudeDesktopConfig();
574 |
575 | // Start the server
576 | try {
577 | const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
578 | const isClaudeDesktop = process.env.MCP_FROM_CLAUDE === 'true';
579 |
580 | if (!isClaudeDesktop) {
581 | serverLog(`Meeting BaaS MCP Server starting on http://localhost:${port}/mcp`);
582 | } else {
583 | serverLog(`Meeting BaaS MCP Server starting in stdio mode for Claude Desktop`);
584 | }
585 |
586 | // Start the server using the appropriate method based on environment
587 | if (isClaudeDesktop) {
588 | // For Claude Desktop, we use stdio transport
589 | process.stdin.pipe(process.stdout);
590 | } else {
591 | // For web/HTTP interface, we use the handler directly
592 | const http = require('http');
593 | const server = http.createServer(handler);
594 | server.listen(port, () => {
595 | serverLog(`Server listening on port ${port}`);
596 | });
597 | }
598 | } catch (error) {
599 | serverLog(`Error starting server: ${error}`);
600 | }
601 | })();
602 |
603 | export default handler;
604 | ```
605 |
606 | ## Tool Examples
607 |
608 | Here are examples of tools that you can implement in the tools directory:
609 |
610 | ### src/tools/environment.ts
611 |
612 | ```typescript
613 | import { z } from 'zod';
614 | import { createTool } from '../utils/tool-types.js';
615 |
616 | /**
617 | * Tool to select the environment for API requests
618 | */
619 | export const selectEnvironmentTool = createTool(
620 | 'selectEnvironment',
621 | 'Switch between API environments',
622 | z.object({
623 | environment: z.enum(['production', 'staging', 'development']),
624 | }),
625 | async ({ environment }, context) => {
626 | // Set the environment for subsequent requests
627 | process.env.MEETING_BAAS_API_ENVIRONMENT = environment;
628 |
629 | return {
630 | content: [
631 | { type: 'text', text: `Environment set to ${environment}` },
632 | ],
633 | };
634 | }
635 | );
636 | ```
637 |
638 | ### src/tools/deleteData.ts
639 |
640 | ```typescript
641 | import { z } from 'zod';
642 | import { createApiClient } from '../api/client.js';
643 | import { createTool } from '../utils/tool-types.js';
644 |
645 | /**
646 | * Tool to delete data associated with a bot
647 | */
648 | export const deleteDataTool = createTool(
649 | 'deleteData',
650 | 'Delete data associated with a bot',
651 | z.object({
652 | botId: z.string().uuid(),
653 | }),
654 | async ({ botId }, context) => {
655 | try {
656 | const client = createApiClient(context.session);
657 |
658 | // Call the API to delete bot data
659 | await client.delete(`/bots/${botId}/data`);
660 |
661 | return {
662 | content: [
663 | { type: 'text', text: `Successfully deleted data for bot ${botId}` },
664 | ],
665 | };
666 | } catch (error: any) {
667 | return {
668 | content: [
669 | { type: 'text', text: `Error deleting data: ${error.message}` },
670 | ],
671 | };
672 | }
673 | }
674 | );
675 | ```
676 |
677 | ### src/tools/listBots.ts
678 |
679 | ```typescript
680 | import { z } from 'zod';
681 | import { createApiClient } from '../api/client.js';
682 | import { createTool } from '../utils/tool-types.js';
683 |
684 | /**
685 | * Tool to list bots with metadata
686 | */
687 | export const listBotsWithMetadataTool = createTool(
688 | 'listBotsWithMetadata',
689 | 'List all bots with their metadata',
690 | z.object({}),
691 | async (_, context) => {
692 | try {
693 | const client = createApiClient(context.session);
694 |
695 | // Call the API to get bots
696 | const response = await client.get('/bots');
697 | const bots = response.data;
698 |
699 | return {
700 | content: [
701 | { type: 'text', text: JSON.stringify(bots, null, 2) },
702 | ],
703 | };
704 | } catch (error: any) {
705 | return {
706 | content: [
707 | { type: 'text', text: `Error listing bots: ${error.message}` },
708 | ],
709 | };
710 | }
711 | }
712 | );
713 | ```
714 |
715 | ### src/tools/index.ts
716 |
717 | ```typescript
718 | // Export all tools
719 | export { deleteDataTool } from './deleteData.js';
720 | export { listBotsWithMetadataTool } from './listBots.js';
721 | export { selectEnvironmentTool } from './environment.js';
722 | // Add exports for other tools
723 | ```
724 |
725 | ## Running the Server
726 |
727 | 1. Build the project:
728 |
729 | ```bash
730 | npm run build
731 | ```
732 |
733 | 2. Start the server:
734 |
735 | ```bash
736 | npm start
737 | ```
738 |
739 | ## Environment Variables
740 |
741 | The server supports the following environment variables:
742 |
743 | - `MEETING_BAAS_API_KEY`: API key for authenticating with the Meeting BaaS API
744 | - `MEETING_BAAS_API_URL`: Base URL for the Meeting BaaS API (default: https://api.meetingbaas.com)
745 | - `PORT`: Port to run the server on (default: 3000)
746 | - `MCP_FROM_CLAUDE`: Set to 'true' when running from Claude Desktop
747 | - `REDIS_URL`: URL for Redis connection (optional)
748 | - `MPC_SERVER_URL`: URL for the MPC server (default: http://localhost:7020)
749 | - `MEETING_BOT_NAME`: Default name for bots
750 | - `MEETING_BOT_IMAGE`: Default image URL for bots
751 | - `MEETING_BOT_ENTRY_MESSAGE`: Default entry message for bots
752 | - `MEETING_BOT_EXTRA`: JSON string with extra bot configuration
753 |
754 | ## Additional Resources
755 |
756 | - [FastMCP Documentation](https://github.com/fastmcp/fastmcp)
757 | - [Meeting BaaS SDK Documentation](https://github.com/meeting-baas/sdk)
758 | - [Zod Documentation](https://github.com/colinhacks/zod)
```
--------------------------------------------------------------------------------
/src/tools/links.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Meeting recording link generation and sharing tools
3 | */
4 |
5 | import { z } from "zod";
6 | import type { Context, TextContent } from "fastmcp";
7 | import { apiRequest, SessionAuth } from "../api/client.js";
8 | import {
9 | createShareableLink,
10 | createMeetingSegmentsList,
11 | createInlineMeetingLink
12 | } from "../utils/linkFormatter.js";
13 | import { createValidSession } from "../utils/auth.js";
14 |
15 | /**
16 | * Schema for generating a shareable link to a meeting
17 | */
18 | const shareableMeetingLinkParams = z.object({
19 | botId: z.string().describe("ID of the bot that recorded the meeting"),
20 | timestamp: z.number().optional().describe("Timestamp in seconds to link to a specific moment (optional)"),
21 | title: z.string().optional().describe("Title to display for the meeting (optional)"),
22 | speakerName: z.string().optional().describe("Name of the speaker at this timestamp (optional)"),
23 | description: z.string().optional().describe("Brief description of what's happening at this timestamp (optional)"),
24 | });
25 |
26 | /**
27 | * Tool for generating a shareable meeting link
28 | */
29 | export const shareableMeetingLinkTool = {
30 | name: "shareableMeetingLink",
31 | description: "Generate a shareable link to a specific moment in a meeting recording",
32 | parameters: shareableMeetingLinkParams,
33 | execute: async (args: z.infer<typeof shareableMeetingLinkParams>, context: Context<SessionAuth>) => {
34 | const { session, log } = context;
35 | log.info("Generating shareable meeting link", { botId: args.botId });
36 |
37 | try {
38 | // Create a valid session with fallbacks for API key
39 | const validSession = createValidSession(session, log);
40 |
41 | // Check if we have a valid session with API key
42 | if (!validSession) {
43 | return {
44 | content: [
45 | {
46 | type: "text" as const,
47 | text: "Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly."
48 | }
49 | ],
50 | isError: true
51 | };
52 | }
53 |
54 | // Get the meeting data to verify the bot ID exists
55 | const response = await apiRequest(
56 | validSession,
57 | "get",
58 | `/bots/meeting_data?bot_id=${args.botId}`
59 | );
60 |
61 | // If we got a response, the bot exists, so we can generate a link
62 | const shareableLink = createShareableLink(args.botId, {
63 | timestamp: args.timestamp,
64 | title: args.title,
65 | speakerName: args.speakerName,
66 | description: args.description
67 | });
68 |
69 | return shareableLink;
70 |
71 | } catch (error) {
72 | return `Error generating shareable link: ${error instanceof Error ? error.message : String(error)}. Please check that the bot ID is correct.`;
73 | }
74 | }
75 | };
76 |
77 | /**
78 | * Schema for generating links to multiple timestamps in a meeting
79 | */
80 | const shareMeetingSegmentsParams = z.object({
81 | botId: z.string().describe("ID of the bot that recorded the meeting"),
82 | segments: z.array(
83 | z.object({
84 | timestamp: z.number().describe("Timestamp in seconds"),
85 | speaker: z.string().optional().describe("Name of the speaker at this timestamp (optional)"),
86 | description: z.string().describe("Brief description of what's happening at this timestamp"),
87 | })
88 | ).describe("List of meeting segments to share")
89 | });
90 |
91 | /**
92 | * Tool for sharing multiple segments from a meeting
93 | */
94 | export const shareMeetingSegmentsTool = {
95 | name: "shareMeetingSegments",
96 | description: "Generate a list of links to important moments in a meeting",
97 | parameters: shareMeetingSegmentsParams,
98 | execute: async (args: z.infer<typeof shareMeetingSegmentsParams>, context: Context<SessionAuth>) => {
99 | const { session, log } = context;
100 | log.info("Sharing meeting segments", { botId: args.botId, segments: args.segments });
101 |
102 | try {
103 | // Create a valid session with fallbacks for API key
104 | const validSession = createValidSession(session, log);
105 |
106 | // Check if we have a valid session with API key
107 | if (!validSession) {
108 | return {
109 | content: [
110 | {
111 | type: "text" as const,
112 | text: "Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly."
113 | }
114 | ],
115 | isError: true
116 | };
117 | }
118 |
119 | // Get the meeting data to verify the bot ID exists
120 | const response = await apiRequest(
121 | validSession,
122 | "get",
123 | `/bots/meeting_data?bot_id=${args.botId}`
124 | );
125 |
126 | // If we got a response, the bot exists, so we can generate the segments
127 | const segmentsList = createMeetingSegmentsList(args.botId, args.segments);
128 |
129 | return segmentsList;
130 |
131 | } catch (error) {
132 | return `Error generating meeting segments: ${error instanceof Error ? error.message : String(error)}. Please check that the bot ID is correct.`;
133 | }
134 | }
135 | };
136 |
137 | /**
138 | * Schema for finding key moments in a meeting and sharing them
139 | */
140 | const findKeyMomentsParams = z.object({
141 | botId: z.string().describe("ID of the bot that recorded the meeting - required"),
142 | meetingTitle: z.string().optional().describe("Title of the meeting (optional)"),
143 | topics: z.array(z.string()).optional().describe("List of topics to look for in the meeting (optional)"),
144 | maxMoments: z.number().default(5).describe("Maximum number of key moments to find"),
145 | granularity: z.enum(["high", "medium", "low"]).default("medium")
146 | .describe("Level of detail for topic extraction: 'high' finds many specific topics, 'medium' is balanced, 'low' finds fewer broad topics"),
147 | autoDetectTopics: z.boolean().default(true)
148 | .describe("Automatically detect important topics in the meeting without requiring predefined topics"),
149 | initialChunkSize: z.number().default(1200)
150 | .describe("Initial chunk size in seconds to analyze (default 20 minutes)"),
151 | });
152 |
153 | /**
154 | * Tool for automatically finding and sharing key moments from a meeting
155 | */
156 | export const findKeyMomentsTool = {
157 | name: "findKeyMoments",
158 | description: "Automatically find and share key moments and topics from a meeting recording with configurable granularity",
159 | parameters: findKeyMomentsParams,
160 | execute: async (args: z.infer<typeof findKeyMomentsParams>, context: Context<SessionAuth>) => {
161 | const { session, log } = context;
162 | log.info("Finding key moments in meeting", {
163 | botId: args.botId,
164 | granularity: args.granularity,
165 | maxMoments: args.maxMoments,
166 | initialChunkSize: args.initialChunkSize
167 | });
168 |
169 | try {
170 | // Create a valid session with fallbacks for API key
171 | const validSession = createValidSession(session, log);
172 |
173 | // Check if we have a valid session with API key
174 | if (!validSession) {
175 | return {
176 | content: [
177 | {
178 | type: "text" as const,
179 | text: "Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly."
180 | }
181 | ],
182 | isError: true
183 | };
184 | }
185 |
186 | // Get the meeting data using the explicitly provided botId
187 | const response = await apiRequest(
188 | validSession,
189 | "get",
190 | `/bots/meeting_data?bot_id=${args.botId}`
191 | );
192 |
193 | if (!response?.bot_data?.bot) {
194 | return `Could not find meeting data for the provided bot ID: ${args.botId}`;
195 | }
196 |
197 | const meetingTitle = args.meetingTitle || response.bot_data.bot.bot_name || "Meeting Recording";
198 |
199 | // Get the transcripts
200 | const transcripts = response.bot_data.transcripts || [];
201 |
202 | if (transcripts.length === 0) {
203 | return `No transcript found for meeting "${meetingTitle}". You can still view the recording:\n\n${createShareableLink(args.botId, { title: meetingTitle })}`;
204 | }
205 |
206 | // Sort all transcripts chronologically
207 | const sortedTranscripts = [...transcripts].sort((a, b) => a.start_time - b.start_time);
208 |
209 | // Get meeting duration info
210 | const meetingStart = sortedTranscripts[0].start_time;
211 | const meetingEnd = sortedTranscripts[sortedTranscripts.length - 1].start_time;
212 | const meetingDuration = meetingEnd - meetingStart;
213 |
214 | log.info("Processing meeting transcript", {
215 | segmentCount: sortedTranscripts.length,
216 | durationSeconds: meetingDuration
217 | });
218 |
219 | // STEP 1: Group transcripts into larger contextual chunks
220 | // This preserves context while making processing more manageable
221 | const contextChunks = groupTranscriptsIntoChunks(sortedTranscripts, 300); // 5-minute chunks
222 |
223 | // STEP 2: Identify important segments and topics
224 | let allMeetingTopics: string[] = args.topics || [];
225 | const candidateSegments: any[] = [];
226 |
227 | // First, analyze each chunk to find patterns and topics
228 | for (const chunk of contextChunks) {
229 | // Only do topic detection if requested
230 | if (args.autoDetectTopics) {
231 | const detectedTopics = identifyTopicsWithAI(chunk);
232 | allMeetingTopics = [...allMeetingTopics, ...detectedTopics];
233 | }
234 |
235 | // Find important segments in this chunk
236 | const importantSegments = findImportantSegments(chunk);
237 | candidateSegments.push(...importantSegments);
238 |
239 | // Find conversation segments (multiple speakers)
240 | const conversationSegments = findConversationalExchanges(chunk);
241 | candidateSegments.push(...conversationSegments);
242 | }
243 |
244 | // Deduplicate topics
245 | const uniqueTopics = [...new Set(allMeetingTopics)];
246 |
247 | // STEP 3: Score and rank all candidate segments
248 | const scoredSegments = scoreSegments(candidateSegments);
249 |
250 | // STEP 4: Ensure structural segments (beginning, end) are included
251 | const structuralSegments = getStructuralSegments(sortedTranscripts);
252 | const allSegments = [...scoredSegments, ...structuralSegments];
253 |
254 | // STEP 5: Sort by importance, then deduplicate
255 | allSegments.sort((a, b) => b.importance - a.importance);
256 | const dedupedSegments = deduplicateSegments(allSegments);
257 |
258 | // STEP 6: Resort by chronological order and take top N
259 | const chronologicalSegments = dedupedSegments.sort((a, b) => a.timestamp - b.timestamp);
260 | const finalSegments = chronologicalSegments.slice(0, args.maxMoments);
261 |
262 | // If we have no segments, return a message
263 | if (finalSegments.length === 0) {
264 | return `No key moments found in meeting "${meetingTitle}". You can view the full recording:\n\n${createShareableLink(args.botId, { title: meetingTitle })}`;
265 | }
266 |
267 | // Format the segments for display
268 | const formattedSegments = finalSegments.map(segment => ({
269 | timestamp: segment.timestamp,
270 | speaker: segment.speaker,
271 | description: segment.description
272 | }));
273 |
274 | // Create the segments list with the full title
275 | const segmentsList = createMeetingSegmentsList(args.botId, formattedSegments);
276 |
277 | // Include topics if they were detected
278 | let result = `# Key Moments from ${meetingTitle}\n\n`;
279 |
280 | if (uniqueTopics.length > 0) {
281 | const topicLimit = args.granularity === "high" ? 10 : args.granularity === "medium" ? 7 : 5;
282 | const topTopics = uniqueTopics.slice(0, topicLimit);
283 |
284 | result += `## Main Topics Discussed\n${topTopics.map(topic => `- ${topic}`).join('\n')}\n\n`;
285 | }
286 |
287 | result += segmentsList;
288 |
289 | return result;
290 |
291 | } catch (error) {
292 | return `Error finding key moments: ${error instanceof Error ? error.message : String(error)}. Please check that the bot ID is correct.`;
293 | }
294 | }
295 | };
296 |
297 | /**
298 | * Group transcripts into larger chunks for context preservation
299 | */
300 | function groupTranscriptsIntoChunks(transcripts: any[], maxChunkDuration: number = 300): any[][] {
301 | if (!transcripts || transcripts.length === 0) return [];
302 |
303 | const chunks: any[][] = [];
304 | let currentChunk: any[] = [];
305 | let chunkStartTime = transcripts[0].start_time;
306 |
307 | for (const segment of transcripts) {
308 | if (currentChunk.length === 0 || (segment.start_time - chunkStartTime <= maxChunkDuration)) {
309 | currentChunk.push(segment);
310 | } else {
311 | chunks.push(currentChunk);
312 | currentChunk = [segment];
313 | chunkStartTime = segment.start_time;
314 | }
315 | }
316 |
317 | // Add the last chunk if it has any segments
318 | if (currentChunk.length > 0) {
319 | chunks.push(currentChunk);
320 | }
321 |
322 | return chunks;
323 | }
324 |
325 | /**
326 | * AI-based topic identification that works across any domain or language
327 | * Uses natural language processing patterns to identify important concepts
328 | */
329 | function identifyTopicsWithAI(transcripts: any[]): string[] {
330 | if (!transcripts || transcripts.length === 0) return [];
331 |
332 | // Extract the text from all segments
333 | const allText = transcripts.map(t => {
334 | return t.words ? t.words.map((w: any) => w.text).join(" ") : "";
335 | }).join(" ");
336 |
337 | // Split into sentences for better context
338 | const sentences = allText.split(/[.!?]+/).filter(s => s.trim().length > 0);
339 |
340 | // Identify potential topics through pattern analysis
341 | const topics: Record<string, number> = {};
342 |
343 | // AI-like pattern recognition for topics:
344 | // 1. Look for repeated meaningful phrases
345 | // 2. Look for phrases that appear after introductory patterns
346 | // 3. Look for phrases with specific part-of-speech patterns (noun phrases)
347 |
348 | // Pattern 1: Repeated phrases (frequency-based)
349 | const phraseFrequency = findRepeatedPhrases(allText);
350 | Object.entries(phraseFrequency)
351 | .filter(([_, count]) => count > 1) // Only phrases that appear multiple times
352 | .forEach(([phrase, _]) => {
353 | topics[phrase] = (topics[phrase] || 0) + 2; // Weight by 2
354 | });
355 |
356 | // Pattern 2: Introductory phrases
357 | // Look for phrases like "talking about X", "discussing X", "focused on X"
358 | for (const sentence of sentences) {
359 | const introPatterns = [
360 | {regex: /(?:talk|talking|discuss|discussing|focus|focusing|about|regarding)\s+([a-z0-9\s]{3,30})/i, group: 1},
361 | {regex: /(?:main|key|important)\s+(?:topic|point|issue|concern)\s+(?:is|was|being)\s+([a-z0-9\s]{3,30})/i, group: 1},
362 | {regex: /(?:related to|concerning|with regards to)\s+([a-z0-9\s]{3,30})/i, group: 1},
363 | ];
364 |
365 | for (const pattern of introPatterns) {
366 | const matches = sentence.match(pattern.regex);
367 | if (matches && matches[pattern.group]) {
368 | const topic = matches[pattern.group].trim();
369 | if (topic.length > 3) {
370 | topics[topic] = (topics[topic] || 0) + 3; // Weight by 3
371 | }
372 | }
373 | }
374 | }
375 |
376 | // Pattern 3: Noun phrase detection (simplified)
377 | // Look for phrases with specific patterns like "Noun Noun" or "Adjective Noun"
378 | const nounPhrasePatterns = [
379 | /(?:[A-Z][a-z]+)\s+(?:[a-z]+ing|[a-z]+ment|[a-z]+tion)/g, // E.g., "Data processing", "Risk management"
380 | /(?:[A-Z][a-z]+)\s+(?:[A-Z][a-z]+)/g, // E.g., "Health Insurance", "Business Agreement"
381 | /(?:the|our|your|their)\s+([a-z]+\s+[a-z]+)/gi, // E.g., "the pricing model", "your business needs"
382 | ];
383 |
384 | for (const pattern of nounPhrasePatterns) {
385 | const matches = allText.match(pattern) || [];
386 | for (const match of matches) {
387 | if (match.length > 5) {
388 | topics[match] = (topics[match] || 0) + 1;
389 | }
390 | }
391 | }
392 |
393 | // Sort topics by score and take top N
394 | const sortedTopics = Object.entries(topics)
395 | .sort((a, b) => b[1] - a[1])
396 | .map(([topic]) => topic);
397 |
398 | return sortedTopics.slice(0, 10); // Return top 10 topics
399 | }
400 |
401 | /**
402 | * Find repeated phrases in text that might indicate important topics
403 | */
404 | function findRepeatedPhrases(text: string): Record<string, number> {
405 | const phrases: Record<string, number> = {};
406 |
407 | // Normalize text
408 | const normalizedText = text.toLowerCase().replace(/[^\w\s]/g, '');
409 |
410 | // Split text into words
411 | const words = normalizedText.split(/\s+/).filter(w => w.length > 2);
412 |
413 | // Look for 2-3 word phrases
414 | for (let size = 2; size <= 3; size++) {
415 | if (words.length < size) continue;
416 |
417 | for (let i = 0; i <= words.length - size; i++) {
418 | const phrase = words.slice(i, i + size).join(' ');
419 |
420 | // Filter out phrases that are too short
421 | if (phrase.length > 5) {
422 | phrases[phrase] = (phrases[phrase] || 0) + 1;
423 | }
424 | }
425 | }
426 |
427 | return phrases;
428 | }
429 |
430 | /**
431 | * Find segments that appear to be important based on content analysis
432 | */
433 | function findImportantSegments(transcripts: any[]): any[] {
434 | if (!transcripts || transcripts.length === 0) return [];
435 |
436 | const importantSegments = [];
437 |
438 | // Patterns that indicate importance
439 | const importancePatterns = [
440 | {regex: /(?:important|key|critical|essential|significant|main|major)/i, weight: 3},
441 | {regex: /(?:summarize|summary|summarizing|conclude|conclusion|in conclusion|to sum up)/i, weight: 4},
442 | {regex: /(?:need to|have to|must|should|will|going to|plan to|action item)/i, weight: 2},
443 | {regex: /(?:agree|disagree|consensus|decision|decide|decided|determined)/i, weight: 3},
444 | {regex: /(?:problem|issue|challenge|obstacle|difficulty)/i, weight: 2},
445 | {regex: /(?:solution|resolve|solve|approach|strategy|tactic)/i, weight: 2},
446 | {regex: /(?:next steps|follow up|get back|circle back|future|next time)/i, weight: 3},
447 | ];
448 |
449 | for (const transcript of transcripts) {
450 | if (!transcript.words) continue;
451 |
452 | const text = transcript.words.map((w: any) => w.text).join(" ");
453 |
454 | // Calculate an importance score based on matching patterns
455 | let importanceScore = 0;
456 |
457 | // Check for matches with importance patterns
458 | for (const pattern of importancePatterns) {
459 | if (pattern.regex.test(text)) {
460 | importanceScore += pattern.weight;
461 | }
462 | }
463 |
464 | // Also consider length - longer segments might be more substantive
465 | importanceScore += Math.min(2, Math.floor(text.split(/\s+/).length / 20));
466 |
467 | // If the segment has some importance, add it to results
468 | if (importanceScore > 0) {
469 | importantSegments.push({
470 | timestamp: transcript.start_time,
471 | speaker: transcript.speaker || "Unknown speaker",
472 | text,
473 | importance: importanceScore,
474 | type: 'content',
475 | description: determineDescription(text, importanceScore)
476 | });
477 | }
478 | }
479 |
480 | return importantSegments;
481 | }
482 |
483 | /**
484 | * Determine an appropriate description for a segment based on its content
485 | */
486 | function determineDescription(text: string, importance: number): string {
487 | // Try to find a suitable description based on content patterns
488 |
489 | if (/(?:summarize|summary|summarizing|conclude|conclusion|in conclusion|to sum up)/i.test(text)) {
490 | return "Summary or conclusion";
491 | }
492 |
493 | if (/(?:next steps|follow up|moving forward|future|plan)/i.test(text)) {
494 | return "Discussion about next steps";
495 | }
496 |
497 | if (/(?:agree|disagree|consensus|decision|decide|decided|determined)/i.test(text)) {
498 | return "Decision point";
499 | }
500 |
501 | if (/(?:problem|issue|challenge|obstacle|difficulty)/i.test(text)) {
502 | return "Problem discussion";
503 | }
504 |
505 | if (/(?:solution|resolve|solve|approach|strategy|tactic)/i.test(text)) {
506 | return "Solution discussion";
507 | }
508 |
509 | // Default description based on importance
510 | if (importance > 5) {
511 | return "Highly important discussion";
512 | } else if (importance > 3) {
513 | return "Important point";
514 | } else {
515 | return "Notable discussion";
516 | }
517 | }
518 |
519 | /**
520 | * Find segments with active conversation between multiple speakers
521 | */
522 | function findConversationalExchanges(transcripts: any[]): any[] {
523 | if (!transcripts || transcripts.length < 3) return [];
524 |
525 | const conversationSegments = [];
526 |
527 | // Look for rapid exchanges between different speakers
528 | for (let i = 0; i < transcripts.length - 2; i++) {
529 | const segment1 = transcripts[i];
530 | const segment2 = transcripts[i+1];
531 | const segment3 = transcripts[i+2];
532 |
533 | // Check if there are at least 2 different speakers
534 | const speakers = new Set([
535 | segment1.speaker,
536 | segment2.speaker,
537 | segment3.speaker
538 | ].filter(Boolean));
539 |
540 | if (speakers.size >= 2) {
541 | // Check if the segments are close in time (rapid exchange)
542 | const timeSpan = segment3.start_time - segment1.start_time;
543 |
544 | if (timeSpan < 60) { // Less than 1 minute for 3 segments = pretty active conversation
545 | conversationSegments.push({
546 | timestamp: segment1.start_time,
547 | speaker: segment1.speaker || "Unknown speaker",
548 | text: segment1.words ? segment1.words.map((w: any) => w.text).join(" ") : "",
549 | importance: 2 + speakers.size, // More speakers = more important
550 | type: 'conversation',
551 | description: `Active discussion with ${speakers.size} participants`
552 | });
553 |
554 | // Skip ahead to avoid overlapping conversation segments
555 | i += 2;
556 | }
557 | }
558 | }
559 |
560 | return conversationSegments;
561 | }
562 |
563 | /**
564 | * Get structural segments like start and end of meeting
565 | */
566 | function getStructuralSegments(transcripts: any[]): any[] {
567 | if (!transcripts || transcripts.length === 0) return [];
568 |
569 | const result = [];
570 |
571 | // Add meeting start
572 | const first = transcripts[0];
573 | result.push({
574 | timestamp: first.start_time,
575 | speaker: first.speaker || "Unknown speaker",
576 | text: first.words ? first.words.map((w: any) => w.text).join(" ") : "",
577 | importance: 5, // High importance
578 | type: 'structural',
579 | description: "Meeting start"
580 | });
581 |
582 | // Add meeting end if it's a different segment
583 | if (transcripts.length > 1) {
584 | const last = transcripts[transcripts.length - 1];
585 | if (last.start_time !== first.start_time) {
586 | result.push({
587 | timestamp: last.start_time,
588 | speaker: last.speaker || "Unknown speaker",
589 | text: last.words ? last.words.map((w: any) => w.text).join(" ") : "",
590 | importance: 4, // High importance
591 | type: 'structural',
592 | description: "Meeting conclusion"
593 | });
594 | }
595 | }
596 |
597 | return result;
598 | }
599 |
600 | /**
601 | * Score segments based on various factors to determine overall importance
602 | */
603 | function scoreSegments(segments: any[]): any[] {
604 | if (!segments || segments.length === 0) return [];
605 |
606 | return segments.map(segment => {
607 | // Add any additional scoring factors here
608 | return segment;
609 | });
610 | }
611 |
612 | /**
613 | * Deduplicate segments that are too close to each other
614 | * Keeps the most important segment when duplicates are found
615 | */
616 | function deduplicateSegments(segments: any[]): any[] {
617 | if (segments.length <= 1) return segments;
618 |
619 | const result: any[] = [];
620 | const usedTimeRanges: number[] = [];
621 |
622 | // Process segments in order of importance
623 | for (const segment of segments) {
624 | // Check if this segment is too close to an already included one
625 | const isTooClose = usedTimeRanges.some(range =>
626 | Math.abs(segment.timestamp - range) < 30 // 30 seconds threshold
627 | );
628 |
629 | if (!isTooClose) {
630 | result.push(segment);
631 | usedTimeRanges.push(segment.timestamp);
632 | }
633 | }
634 |
635 | return result;
636 | }
637 |
```