#
tokens: 49394/50000 32/34 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
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 | [![Project Logo]([email protected])](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 | 
```
Page 1/2FirstPrevNextLast