#
tokens: 45962/50000 33/34 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/meeting-baas/meeting-mcp?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:
--------------------------------------------------------------------------------

```
node_modules/
dist/
coverage/
.vscode/
.husky/
.github/
*.md
*.log
*.min.js 
```

--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------

```
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false 
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
{
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "trailingComma": "all",
  "singleQuote": true,
  "quoteProps": "as-needed",
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf",
  "bracketSameLine": false
}

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Build output
dist/
build/

# Editor directories and files
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# OS specific
.DS_Store
.cursor/

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Meeting BaaS MCP Server
[![Project Logo]([email protected])](https://meetingBaaS.com)

<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>

A Model Context Protocol (MCP) server that provides tools for managing meeting data, including transcripts, recordings, calendar events, and search functionality.

## QUICK START: Claude Desktop Integration

To use Meeting BaaS with Claude Desktop:

1. Edit the Claude Desktop configuration file:
   ```bash
   vim ~/Library/Application\ Support/Claude/claude_desktop_config.json
   ```

2. Add the Meeting BaaS configuration:
   ```json
   "meetingbaas": {
     "command": "/bin/bash",
     "args": [
       "-c",
       "cd /path/to/meeting-mcp && (npm run build 1>&2) && export MCP_FROM_CLAUDE=true && node dist/index.js"
     ],
     "headers": {
       "x-api-key": "YOUR_API_KEY"
     }
   }
   ```

3. For calendar integration, you can add the `calendarOAuth` section to your `botConfig`:
   ```json
   "botConfig": {
     "calendarOAuth": {
       "platform": "Google",  // or "Microsoft"
       "clientId": "YOUR_OAUTH_CLIENT_ID",
       "clientSecret": "YOUR_OAUTH_CLIENT_SECRET", 
       "refreshToken": "YOUR_REFRESH_TOKEN",
       "rawCalendarId": "[email protected]"  // Optional
     }
   }
   ```

4. Save the file and restart Claude Desktop.

> **Note:** Calendar integration is optional. Meeting BaaS can be used without connecting a calendar by simply omitting the `calendarOAuth` section.

## Overview

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:

- **Invite Meeting Bots**: Create and invite bots to your video conferences that automatically record and transcribe meetings

  ```
  "Create a new meeting bot for my Zoom call tomorrow"
  ```

- **Query Meeting Data**: Search through meeting transcripts and find specific information without watching entire recordings

  ```
  "Search my recent meetings for discussions about the quarterly budget"
  "Find all mentions of Project Apollo in yesterday's team meeting"
  "Show me parts of the meeting where Jane was speaking"
  ```

- **Manage Calendar Events**: View and organize calendar entries and upcoming meetings

- **Access Recording Information**: Get metadata about meeting recordings and their status

## Prerequisites

- Node.js (v16 or later)
- npm
- **MeetingBaaS Account**: You need access to a MeetingBaaS account using your corporate email address
  - All logs, bots, and shared links are available to colleagues with the same corporate domain (not personal emails like gmail.com)
  - This enables seamless collaboration where all team members can access meeting recordings and transcripts created by anyone in your organization

## Installation

1. Clone the repository:

   ```bash
   git clone <repository-url>
   cd mcp-baas
   ```

2. Install dependencies:

   ```bash
   npm install
   ```

3. Build the project:
   ```bash
   npm run build
   ```

## Usage

Start the server:

```bash
npm run start
```

By default, the server runs on port 7017 and exposes the MCP endpoint at `http://localhost:7017/mcp`.

## Available Tools

The server exposes several tools through the MCP protocol:

### Calendar Tools

- `oauthGuidance`: Get detailed step-by-step instructions on setting up OAuth for Google or Microsoft calendars
  - No parameters required
  - Returns comprehensive instructions for obtaining OAuth credentials and setting up calendar integration

- `listRawCalendars`: Lists available calendars from Google or Microsoft before integration
  - Parameters: `platform` ("Google" or "Microsoft"), `clientId`, `clientSecret`, `refreshToken`
  - Returns a list of available calendars with their IDs and primary status

- `setupCalendarOAuth`: Integrates a calendar using OAuth credentials
  - Parameters: `platform` ("Google" or "Microsoft"), `clientId`, `clientSecret`, `refreshToken`, `rawCalendarId` (optional)
  - Returns confirmation of successful integration with calendar details

- `listCalendars`: Lists all integrated calendars
  - No parameters required
  - Returns a list of all calendars with their names, email addresses, and UUIDs

- `getCalendar`: Gets detailed information about a specific calendar integration
  - Parameters: `calendarId` (UUID of the calendar)
  - Returns comprehensive calendar details

- `deleteCalendar`: Permanently removes a calendar integration
  - Parameters: `calendarId` (UUID of the calendar)
  - Returns confirmation of successful deletion

- `resyncAllCalendars`: Forces a refresh of all connected calendars
  - No parameters required
  - Returns the status of the sync operation

- `listUpcomingMeetings`: Lists upcoming meetings from a calendar
  - Parameters: `calendarId`, `status` (optional: "upcoming", "past", "all"), `limit` (optional)
  - Returns a list of meetings with their names, times, and recording status

- `listEvents`: Lists calendar events with comprehensive filtering options
  - Parameters: `calendarId`, plus optional filters like `startDateGte`, `startDateLte`, `attendeeEmail`, etc.
  - Returns detailed event listings with rich information

- `listEventsWithCredentials`: Lists calendar events with credentials provided directly in the query
  - Parameters: `calendarId`, `apiKey`, plus same optional filters as `listEvents`
  - Returns the same detailed information as `listEvents` but with direct authentication

- `getEvent`: Gets detailed information about a specific calendar event
  - Parameters: `eventId` (UUID of the event)
  - Returns comprehensive event details including attendees and recording status

- `scheduleRecording`: Schedules a bot to record an upcoming meeting
  - Parameters: `eventId`, `botName`, plus optional settings like `botImage`, `recordingMode`, etc.
  - Returns confirmation of successful scheduling

- `scheduleRecordingWithCredentials`: Schedules recording with credentials provided directly in the query
  - Parameters: `eventId`, `apiKey`, `botName`, plus same optional settings as `scheduleRecording`
  - Returns confirmation of successful scheduling

- `cancelRecording`: Cancels a previously scheduled recording
  - Parameters: `eventId`, `allOccurrences` (optional, for recurring events)
  - Returns confirmation of successful cancellation

- `cancelRecordingWithCredentials`: Cancels recording with credentials provided directly in the query
  - Parameters: `eventId`, `apiKey`, `allOccurrences` (optional)
  - Returns confirmation of successful cancellation

- `checkCalendarIntegration`: Checks and diagnoses calendar integration status
  - No parameters required
  - Returns a comprehensive status report and troubleshooting tips

### Meeting Tools

- `createBot`: Creates a meeting bot that can join video conferences to record and transcribe meetings
  - Parameters: 
    - `meeting_url` (URL of the meeting to join)
    - `name` (optional bot name)
    - `botImage` (optional URL to an image for the bot's avatar) 
    - `entryMessage` (optional message the bot will send when joining)
    - `deduplicationKey` (optional key to override the 5-minute restriction on joining the same meeting)
    - `nooneJoinedTimeout` (optional timeout in seconds for bot to leave if no one joins)
    - `waitingRoomTimeout` (optional timeout in seconds for bot to leave if stuck in waiting room)
    - `speechToTextProvider` (optional provider for transcription: "Gladia", "Runpod", or "Default")
    - `speechToTextApiKey` (optional API key for the speech-to-text provider)
    - `streamingInputUrl` (optional WebSocket URL to stream audio input)
    - `streamingOutputUrl` (optional WebSocket URL to stream audio output)
    - `streamingAudioFrequency` (optional frequency for streaming: "16khz" or "24khz")
    - `extra` (optional object with additional metadata about the meeting, such as meeting type, custom summary prompt, search keywords)
  - Returns: Bot details including ID and join status
- `getBots`: Lists all bots and their associated meetings
- `getBotsByMeeting`: Gets bots for a specific meeting URL
- `getRecording`: Retrieves recording information for a specific bot/meeting
- `getRecordingStatus`: Checks the status of a recording in progress
- `getMeetingData`: Gets transcript and recording data for a specific meeting
  - Parameters: `meetingId` (ID of the meeting to get data for)
  - Returns: Information about the meeting recording including duration and transcript segment count
- `getMeetingDataWithCredentials`: Gets transcript and recording data using direct API credentials
  - Parameters: `meetingId` (ID of the meeting), `apiKey` (API key for authentication)
  - Returns: Same information as `getMeetingData` but with direct authentication

### Transcript Tools

- `getMeetingTranscript`: Gets a meeting transcript with speaker names and content grouped by speaker
  - Parameters: `botId` (the bot that recorded the meeting)
  - Returns: Complete transcript with speaker information, formatted as paragraphs grouped by speaker
  - Example output:
    ```
    Meeting: "Weekly Team Meeting"
    Duration: 45m 30s
    Transcript:

    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.

    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.
    ```

- `findKeyMoments`: Automatically identifies and shares links to important moments in a meeting
  - Parameters: `botId`, optional `meetingTitle`, optional list of `topics` to look for, and optional `maxMoments`
  - Returns: Markdown-formatted list of key moments with links, automatically detected based on transcript
  - Uses AI-powered analysis to find significant moments without requiring manual timestamp selection

### QR Code Tools

- `generateQRCode`: Creates an AI-generated QR code image that can be used as a bot avatar
  - Parameters:
    - `type`: Type of QR code (url, email, phone, sms, text)
    - `to`: Destination for the QR code (URL, email, phone number, or text)
    - `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.
    - `style`: Style of the QR code (style_default, style_dots, style_rounded, style_crystal)
    - `useAsBotImage`: Whether to use the generated QR code as the bot avatar (default: true)
    - `template`: Template ID for the QR code (optional)
    - `apiKey`: Your QR Code AI API key (optional, will use default if not provided)
  - Returns: URL to the generated QR code image that can be used directly with the joinMeeting tool
  - Example usage:
    ```
    "Generate a QR code with my email [email protected] that looks like a Tiger in crystal style"
    ```
  - Example with API key in the prompt:
    ```
    "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"
    ```
  - Example with formal parameter:
    ```
    "Generate a QR code with the following parameters:
    - Type: email
    - To: [email protected]
    - Prompt: Create a QR code that looks like a mountain landscape
    - Style: style_rounded
    - API Key: qrc_my-personal-api-key-123456"
    ```

### Link Sharing Tools

- `shareableMeetingLink`: Generates a nicely formatted, shareable link to a meeting recording
  - Parameters: `botId`, plus optional `timestamp`, `title`, `speakerName`, and `description`
  - Returns: Markdown-formatted link with metadata that can be shared directly in chat
  - Example: 
    ```
    📽️ **Meeting Recording: Weekly Team Sync**
    ⏱️ Timestamp: 00:12:35
    🎤 Speaker: Sarah Johnson
    📝 Discussing the new product roadmap

    🔗 [View Recording](https://meetingbaas.com/viewer/abc123?t=755)
    ```

- `shareMeetingSegments`: Creates a list of links to multiple important moments in a meeting
  - Parameters: `botId` and an array of `segments` with timestamps, speakers, and descriptions
  - Returns: Markdown-formatted list of segments with direct links to each moment
  - Useful for creating a table of contents for a long meeting

## Example Workflows

### Recording a Meeting

1. Create a bot for your upcoming meeting:

   ```
   "Create a bot for my Zoom meeting at https://zoom.us/j/123456789"
   ```

2. The bot joins the meeting automatically and begins recording.

3. Check recording status:
   ```
   "What's the status of my meeting recording for the Zoom call I started earlier?"
   ```

### Calendar Integration and Automatic Recording

1. Get guidance on obtaining OAuth credentials:

   ```
   "I want to integrate my Google Calendar. How do I get OAuth credentials?"
   ```

2. List your available calendars before integration:

   ```
   "List my available Google calendars. Here are my OAuth credentials:
   - Client ID: my-client-id-123456789.apps.googleusercontent.com
   - Client Secret: my-client-secret-ABCDEF123456
   - Refresh Token: my-refresh-token-ABCDEF123456789"
   ```

3. Set up calendar integration with a specific calendar:

   ```
   "Integrate my Google Calendar using these credentials:
   - Platform: Google
   - Client ID: my-client-id-123456789.apps.googleusercontent.com
   - Client Secret: my-client-secret-ABCDEF123456
   - Refresh Token: my-refresh-token-ABCDEF123456789
   - Raw Calendar ID: [email protected]"
   ```

4. View your upcoming meetings:

   ```
   "Show me my upcoming meetings from calendar 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
   ```

5. Schedule recording for an upcoming meeting:

   ```
   "Schedule a recording for my team meeting with event ID 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d.
   Configure the bot with:
   - Name: Team Meeting Bot
   - Recording Mode: gallery_view
   - Entry Message: Hello everyone, I'm here to record the meeting"
   ```

6. Check all recordings scheduled in your calendar:

   ```
   "Show me all meetings in my calendar that have recordings scheduled"
   ```

7. Cancel a previously scheduled recording:

   ```
   "Cancel the recording for event 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
   ```

8. Refresh calendar data if meetings are missing:

   ```
   "Force a resync of all my connected calendars"
   ```

### Analyzing Meeting Content

1. Get the full transcript of a meeting:

   ```
   "Get the transcript from my team meeting with bot ID abc-123"
   ```

2. Find key moments in a meeting:

   ```
   "Identify key moments from yesterday's product planning meeting with bot ID xyz-456"
   ```

3. Share a specific moment from a meeting:

   ```
   "Create a shareable link to the part of meeting abc-123 at timestamp 12:45 where John was talking about the budget"
   ```

### Using Direct Credential Tools

You can provide API credentials directly in your queries:

1. List events with direct credentials:

   ```
   "List events from calendar 5c99f8a4-f498-40d0-88f0-29f698c53c51 using API key tesban where attendee is [email protected]"
   ```

2. Schedule a recording with direct credentials:

   ```
   "Schedule a recording for event 78d06b42-794f-4efe-8195-62db1f0052d5 using API key tesban with bot name 'Weekly Meeting Bot'"
   ```

3. Cancel a recording with direct credentials:

   ```
   "Cancel the recording for event 97cd62f0-ea9b-42b3-add5-7a607ce6d80f using API key tesban"
   ```

4. Get meeting data with direct credentials:

   ```
   "Get meeting data for meeting 47de9462-bea7-406c-b79a-fd6b82c3de76 using API key tesban"
   ```

### Using AI-Generated QR Codes as Bot Avatars

1. Generate a QR code with your contact information and a custom design:

   ```
   "Generate a QR code with the following parameters:
   - Type: email
   - To: [email protected]
   - Prompt: Create a professional-looking QR code with abstract blue patterns that resemble a corporate logo
   - Style: style_crystal"
   ```

2. Use the generated QR code as a bot avatar in a meeting:

   ```
   "Join my Zoom meeting at https://zoom.us/j/123456789 with the following parameters:
   - Bot name: QR Code Assistant
   - Bot image: [URL from the generated QR code]
   - Entry message: Hello everyone, I'm here to record the meeting. You can scan my avatar to get my contact information."
   ```

3. Generate a QR code with a meeting link for easy sharing:

   ```
   "Generate a QR code with the following parameters:
   - Type: url
   - To: https://zoom.us/j/123456789
   - Prompt: Create a colorful QR code with a calendar icon in the center
   - Style: style_rounded"
   ```

### Accessing Meeting Recordings

Meeting recordings can be accessed directly through the Meeting BaaS viewer using the bot ID:

```
https://meetingbaas.com/viewer/{BOT_ID}
```

For example:
```
https://meetingbaas.com/viewer/67738f48-2360-4f9e-a999-275a74208ff5
```

This viewer provides:
- The meeting video recording
- Synchronized transcript with speaker identification
- Navigation by speaker or topic
- Direct link sharing with teammates

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.

> **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.

## Configuration

The server can be configured through environment variables or by editing the `src/config.ts` file.

Key configuration options:

- `PORT`: The port the server listens on (default: 7017)
- `API_BASE_URL`: The base URL for the Meeting BaaS API
- `DEFAULT_API_KEY`: Default API key for testing

## Integration with Cursor

To integrate with Cursor:

1. Open Cursor
2. Go to Settings
3. Navigate to "Model Context Protocol"
4. Add a new server with:
   - Name: "Meeting BaaS MCP"
   - Type: "sse"
   - Server URL: "http://localhost:7017/mcp"
   - Optionally add headers if authentication is required

## Development

### Build

```bash
npm run build
```

### Test with MCP Inspector

```bash
npm run inspect
```

### Development mode (with auto-reload)

```bash
npm run dev
```

### Log Management

The server includes optimized logging with:

```bash
npm run cleanup
```

This command:
- Cleans up unnecessary log files and cached data
- Filters out repetitive ping messages from logs
- Reduces disk usage while preserving important log information
- Maintains a smaller log footprint for long-running servers

## Project Structure

- `src/index.ts`: Main entry point
- `src/tools/`: Tool implementations
- `src/resources/`: Resource definitions
- `src/api/`: API client for the Meeting BaaS backend
- `src/types/`: TypeScript type definitions
- `src/config.ts`: Server configuration
- `src/utils/`: Utility functions
  - `logging.ts`: Log filtering and management
  - `tinyDb.ts`: Persistent bot tracking database

## Authentication

The server expects an API key in the `x-api-key` header for authentication. You can configure the default API key in the configuration.

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.

## License

[MIT](LICENSE)

## QR Code API Key Configuration

The QR code generator tool requires an API key from QR Code AI API. There are several ways to provide this:

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"

2. **As a parameter**: Provide your API key as the `apiKey` parameter when using the `generateQRCode` tool

3. **Environment variable**: Set the `QRCODE_API_KEY` environment variable

4. **Claude Desktop config**: Add the API key to your Claude Desktop configuration file located at:
   - Mac/Linux: `~/Library/Application Support/Claude/claude_desktop_config.json`
   - Windows: `%APPDATA%\Claude\claude_desktop_config.json`

   Example configuration:
   ```json
   {
     "headers": {
       "x-api-key": "qrc_your_key_here" 
     }
   }
   ```

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.

You can obtain an API key by signing up at [QR Code AI API](https://qrcode-ai.com).



```

--------------------------------------------------------------------------------
/src/resources/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Export all MCP resources
 */

export * from "./transcript.js";

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src/**/*"]
}

```

--------------------------------------------------------------------------------
/src/utils/formatters.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Formatting utility functions
 */

/**
 * Format seconds to MM:SS format
 */
export function formatTime(seconds: number): string {
  const minutes = Math.floor(seconds / 60);
  const remainingSeconds = Math.floor(seconds % 60);
  return `${minutes.toString().padStart(2, "0")}:${remainingSeconds
    .toString()
    .padStart(2, "0")}`;
}

/**
 * Format seconds to human-readable duration
 */
export function formatDuration(seconds: number): string {
  const minutes = Math.floor(seconds / 60);
  const remainingSeconds = Math.floor(seconds % 60);
  return `${minutes}m ${remainingSeconds}s`;
}

```

--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Export all MCP tools
 */

// Meeting tools
export * from './meeting.js';
export { getMeetingDataWithCredentialsTool } from './meeting.js';

// Simplified transcript tool
export { getTranscriptTool } from './search.js';

// Calendar tools
export * from './calendar.js';

// Link sharing tools
export * from './links.js';

// Bot management tools
export * from './deleteData.js';
export * from './listBots.js';
export { retranscribeTool } from './retranscribe.js';

// Environment tools
export * from './environment.js';

// QR code generation tool
export { generateQRCodeTool } from './qrcode.js';

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "meetingbaas-mcp",
  "version": "1.0.0",
  "description": "MCP server for Meeting BaaS API",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "npm run build && node dist/index.js",
    "dev": "ts-node --esm src/index.ts",
    "inspect": "fastmcp inspect src/index.ts",
    "cleanup": "bash scripts/cleanup_cursor_logs.sh",
    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
    "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
    "prepare": "husky install"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "fastmcp": "^1.20.2",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "husky": "^8.0.3",
    "lint-staged": "^15.2.0",
    "prettier": "^3.1.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.2.2"
  },
  "lint-staged": {
    "**/*.{ts,tsx,js,jsx,json,md}": "prettier --write"
  }
}

```

--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Type definitions for the MeetingBaaS MCP server
 */

// Session data type
export interface SessionData {
  apiKey: string;
}

// Transcript type
export interface Transcript {
  speaker: string;
  start_time: number;
  words: { text: string }[];
}

// Meeting bot type
export interface Bot {
  bot_id: string;
  bot_name: string;
  meeting_url: string;
  created_at: string;
  ended_at: string | null;
}

// Calendar event type
export interface CalendarEvent {
  uuid: string;
  name: string;
  start_time: string;
  end_time: string;
  deleted: boolean;
  bot_param: unknown;
  meeting_url?: string;
  attendees?: Array<{
    name?: string;
    email: string;
  }>;
  calendar_uuid: string;
  google_id: string;
  is_organizer: boolean;
  is_recurring: boolean;
  last_updated_at: string;
  raw: Record<string, any>;
  recurring_event_id?: string | null;
}

// Calendar type
export interface Calendar {
  uuid: string;
  name: string;
  email: string;
}

```

--------------------------------------------------------------------------------
/src/utils/tool-types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Type definitions for FastMCP Tools that properly handle session auth
 */

import { z } from "zod";
import type { Context, TextContent, ImageContent, ContentResult } from "fastmcp";
import type { SessionAuth } from "../api/client.js";

/**
 * Proper tool type definition that satisfies FastMCP's constraints
 * 
 * This creates a type-safe wrapper for tools that ensures they're compatible
 * with SessionAuth while still allowing them to use their own parameter schemas
 */
export interface MeetingBaaSTool<P extends z.ZodType> {
  name: string;
  description: string;
  parameters: P;
  execute: (
    args: z.infer<P>,
    context: Context<SessionAuth>
  ) => Promise<string | ContentResult | TextContent | ImageContent>;
}

/**
 * Helper function to create a properly typed tool that works with FastMCP and SessionAuth
 * 
 * @param name Tool name
 * @param description Tool description
 * @param parameters Zod schema for tool parameters
 * @param execute Function that executes the tool
 * @returns A properly typed tool compatible with FastMCP SessionAuth
 */
export function createTool<P extends z.ZodType>(
  name: string,
  description: string,
  parameters: P,
  execute: (
    args: z.infer<P>,
    context: Context<SessionAuth>
  ) => Promise<string | ContentResult | TextContent | ImageContent>
): MeetingBaaSTool<P> {
  return {
    name,
    description,
    parameters,
    execute
  };
} 
```

--------------------------------------------------------------------------------
/src/utils/logging.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Logging utilities for the MCP server
 */

// Store the original console methods
const originalConsoleError = console.error;

// Define our ping filter
function isPingMessage(message: string): boolean {
  // Skip ping/pong messages to reduce log noise
  return (
    (typeof message === 'string') && (
      message.includes('"method":"ping"') ||
      (message.includes('"result":{}') && message.includes('"jsonrpc":"2.0"') && message.includes('"id":'))
    )
  );
}

/**
 * Patches the console to filter out ping messages
 */
export function setupPingFiltering(): void {
  // Replace console.error with our filtered version
  console.error = function(...args: any[]) {
    // Check if this is a ping message we want to filter
    const firstArg = args[0];
    
    if (typeof firstArg === 'string' && 
        (firstArg.includes('[meetingbaas]') || firstArg.includes('[MCP Server]'))) {
      // This is a log message from our server
      const messageContent = args.join(' ');
      
      // Skip ping messages to reduce log size
      if (isPingMessage(messageContent)) {
        return; // Don't log ping messages
      }
    }
    
    // For all other messages, pass through to the original
    originalConsoleError.apply(console, args);
  };
}

/**
 * Create standard server logger
 */
export function createServerLogger(prefix: string): (message: string) => void {
  return (message: string) => {
    console.error(`[${prefix}] ${message}`);
  };
} 
```

--------------------------------------------------------------------------------
/FORMATTING.md:
--------------------------------------------------------------------------------

```markdown
# Code Formatting Guidelines

This project uses Prettier for automatic code formatting. This ensures consistent code style across the codebase and prevents code review discussions about formatting.

## Setup

The formatting tools are automatically installed when you run `npm install`. Pre-commit hooks are also set up to format code before committing.

## VS Code Integration

If you're using VS Code, the repository includes settings to automatically format code on save. You'll need to install the Prettier extension:

1. Open VS Code
2. Go to Extensions (Ctrl+Shift+X or Cmd+Shift+X)
3. Search for "Prettier - Code formatter"
4. Install the extension by Prettier

The workspace settings will automatically enable format-on-save.

## Manual Formatting

You can manually format code using these npm scripts:

- `npm run format` - Format all code files
- `npm run format:check` - Check if all files are formatted correctly (useful for CI)

## Formatting Configuration

The formatting rules are defined in `.prettierrc` at the root of the project. Here are the key settings:

- Double quotes for strings
- 2 spaces for indentation
- Maximum line length of 100 characters
- Trailing commas in objects and arrays (ES5 compatible)
- No semicolons at the end of statements

## Pre-commit Hook

A pre-commit hook automatically formats your code before committing. If for some reason this doesn't work, you can reinstall the hooks with:

```
npm run setup-hooks
```

## Ignoring Files

If you need to exclude specific files from formatting, add them to `.prettierignore`. 
```

--------------------------------------------------------------------------------
/scripts/install-hooks.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

/**
 * Script to install pre-commit hooks for code formatting
 *
 * This sets up a Git hook to automatically format your code
 * before committing using Prettier.
 */

import { exec } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// Get the directory of the current script
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Path to Git hooks directory
const gitHooksPath = path.resolve(__dirname, '../.git/hooks');
const preCommitPath = path.join(gitHooksPath, 'pre-commit');

// Pre-commit hook script content
const preCommitScript = `#!/bin/sh
# Pre-commit hook to format code with Prettier

# Get all staged files
FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E "\\.(js|ts|jsx|tsx|json)$")

if [ -n "$FILES" ]; then
  echo "🔍 Formatting staged files with Prettier..."
  npx prettier --write $FILES
  # Add the formatted files back to staging
  git add $FILES
  echo "✅ Formatting complete"
fi
`;

async function installHooks() {
  try {
    // Check if .git directory exists
    try {
      await fs.access(path.resolve(__dirname, '../.git'));
    } catch (error) {
      console.error('❌ No .git directory found. Are you in a Git repository?');
      process.exit(1);
    }

    // Ensure hooks directory exists
    try {
      await fs.access(gitHooksPath);
    } catch (error) {
      await fs.mkdir(gitHooksPath, { recursive: true });
      console.log(`📁 Created hooks directory: ${gitHooksPath}`);
    }

    // Write pre-commit hook
    await fs.writeFile(preCommitPath, preCommitScript, 'utf8');

    // Make it executable
    await new Promise((resolve, reject) => {
      exec(`chmod +x ${preCommitPath}`, (error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      });
    });

    console.log('✅ Pre-commit hook installed successfully');
  } catch (error) {
    console.error('❌ Error installing hooks:', error);
    process.exit(1);
  }
}

// Execute the function
installHooks().catch(console.error);

```

--------------------------------------------------------------------------------
/src/tools/environment.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Environment selection tool
 *
 * Allows switching between different API environments (gmeetbot, preprod, prod)
 */

import { z } from 'zod';
import { Environment, getApiBaseUrl, setEnvironment } from '../config.js';
import { createValidSession } from '../utils/auth.js';
import { createServerLogger } from '../utils/logging.js';
import { createTool } from '../utils/tool-types.js';

const logger = createServerLogger('Environment Tool');

// Define the schema for the environment selection tool parameters
const environmentSelectionSchema = z.object({
  environment: z
    .enum(['gmeetbot', 'preprod', 'prod', 'local'])
    .describe('The environment to use (gmeetbot, preprod, prod, or local)'),
});

// Create the environment selection tool using the helper function
export const selectEnvironmentTool = createTool(
  'select_environment',
  'Select which environment (API endpoint) to use for all MeetingBaaS operations',
  environmentSelectionSchema,
  async (input, context) => {
    const { session, log } = context;

    try {
      // Create a valid session with fallbacks for API key
      const validSession = createValidSession(session, log);

      // Check if we have a valid session with API key
      if (!validSession) {
        return {
          content: [
            {
              type: 'text' as const,
              text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
            },
          ],
          isError: true,
        };
      }

      // Set the environment
      setEnvironment(input.environment as Environment);

      // Get the current API base URL to include in the response
      const apiBaseUrl = getApiBaseUrl();

      logger(`Environment switched to: ${input.environment} (${apiBaseUrl})`);

      return `Environment set to ${input.environment} (${apiBaseUrl})`;
    } catch (error) {
      logger(`Error setting environment: ${error}`);
      return {
        content: [
          {
            type: 'text' as const,
            text: `Failed to set environment: ${error instanceof Error ? error.message : String(error)}`,
          },
        ],
        isError: true,
      };
    }
  },
);

```

--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Configuration and constants for the MeetingBaaS MCP server
 */

// Environment configuration
export type Environment = 'gmeetbot' | 'preprod' | 'prod' | 'local';

// Current active environment (default to prod)
let currentEnvironment: Environment = 'prod';

// API base URLs for different environments
const API_URLS = {
  gmeetbot: 'https://api.gmeetbot.com',
  preprod: 'https://api.pre-prod-meetingbaas.com',
  prod: 'https://api.meetingbaas.com',
  local: 'http://localhost:3001',
};

// Get current API base URL based on the active environment
export const getApiBaseUrl = (): string => {
  return API_URLS[currentEnvironment];
};

// Set the active environment
export const setEnvironment = (env: Environment): void => {
  currentEnvironment = env;
  console.error(`[MCP Server] Environment switched to: ${env} (${API_URLS[env]})`);
};

// For backward compatibility and direct access
export const API_BASE_URL = API_URLS[currentEnvironment];

// Server configuration
export const SERVER_CONFIG = {
  name: 'Meeting BaaS MCP',
  version: '1.0.0',
  port: 7017,
  endpoint: '/mcp',
};

// Bot configuration from environment variables (set in index.ts when loading Claude Desktop config)
export const BOT_CONFIG = {
  // Default bot name displayed in meetings
  defaultBotName: process.env.MEETING_BOT_NAME || null,
  // Default bot image URL
  defaultBotImage: process.env.MEETING_BOT_IMAGE || null,
  // Default bot entry message
  defaultEntryMessage: process.env.MEETING_BOT_ENTRY_MESSAGE || null,
  // Default extra metadata
  defaultExtra: process.env.MEETING_BOT_EXTRA ? JSON.parse(process.env.MEETING_BOT_EXTRA) : null,
};

// Log bot configuration at startup
if (
  BOT_CONFIG.defaultBotName ||
  BOT_CONFIG.defaultBotImage ||
  BOT_CONFIG.defaultEntryMessage ||
  BOT_CONFIG.defaultExtra
) {
  console.error(
    '[MCP Server] Bot configuration loaded:',
    BOT_CONFIG.defaultBotName ? `name="${BOT_CONFIG.defaultBotName}"` : '',
    BOT_CONFIG.defaultBotImage ? 'image=✓' : '',
    BOT_CONFIG.defaultEntryMessage ? 'message=✓' : '',
    BOT_CONFIG.defaultExtra ? 'extra=✓' : '',
  );
}

// Recording modes
export const RECORDING_MODES = ['speaker_view', 'gallery_view', 'audio_only'] as const;
export type RecordingMode = (typeof RECORDING_MODES)[number];

// Speech-to-text providers
export const SPEECH_TO_TEXT_PROVIDERS = ['Gladia', 'Runpod', 'Default'] as const;
export type SpeechToTextProvider = (typeof SPEECH_TO_TEXT_PROVIDERS)[number];

// Audio frequencies
export const AUDIO_FREQUENCIES = ['16khz', '24khz'] as const;
export type AudioFrequency = (typeof AUDIO_FREQUENCIES)[number];

```

--------------------------------------------------------------------------------
/src/utils/auth.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Authentication utilities for handling API keys and sessions
 */

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import type { SessionAuth } from '../api/client.js';

// Define a minimal logger interface rather than importing from fastmcp
interface Logger {
  error: (message: string, ...args: any[]) => void;
  warn: (message: string, ...args: any[]) => void;
  info: (message: string, ...args: any[]) => void;
  debug: (message: string, ...args: any[]) => void;
}

/**
 * Get an API key with robust fallback mechanisms.
 * Tries, in order:
 * 1. Session object
 * 2. Environment variable
 * 3. Claude Desktop config file
 * 
 * @param session The session object, which may contain an API key
 * @param log Optional logger for debugging
 * @returns An object with { apiKey, source } or null if no API key was found
 */
export function getApiKeyWithFallbacks(
  session: any | undefined,
  log?: Logger
): { apiKey: string; source: string } | null {
  // Try to get API key from session
  if (session?.apiKey) {
    log?.debug("Using API key from session");
    return { apiKey: session.apiKey, source: 'session' };
  }

  // Try to get API key from environment variable
  if (process.env.MEETING_BAAS_API_KEY) {
    log?.debug("Using API key from environment variable");
    return { apiKey: process.env.MEETING_BAAS_API_KEY, source: 'environment' };
  }

  // Try to get API key from Claude Desktop config
  try {
    const claudeDesktopConfigPath = path.join(os.homedir(), 'Library/Application Support/Claude/claude_desktop_config.json');
    if (fs.existsSync(claudeDesktopConfigPath)) {
      const configContent = fs.readFileSync(claudeDesktopConfigPath, 'utf8');
      const configJson = JSON.parse(configContent);
      
      if (configJson.mcpServers?.meetingbaas?.headers?.['x-api-key']) {
        const apiKey = configJson.mcpServers.meetingbaas.headers['x-api-key'];
        log?.debug("Using API key from Claude Desktop config");
        return { apiKey, source: 'claude_config' };
      }
    }
  } catch (error) {
    log?.error("Error reading Claude Desktop config", { error });
  }

  // No API key found
  log?.error("No API key found in session, environment, or Claude Desktop config");
  return null;
}

/**
 * Creates a valid session object with an API key
 * 
 * @param session The original session, which may be incomplete
 * @param log Optional logger for debugging
 * @returns A valid session object or null if no API key could be found
 */
export function createValidSession(
  session: any | undefined,
  log?: Logger
): SessionAuth | null {
  const apiKeyInfo = getApiKeyWithFallbacks(session, log);
  
  if (!apiKeyInfo) {
    return null;
  }
  
  return { apiKey: apiKeyInfo.apiKey };
} 
```

--------------------------------------------------------------------------------
/scripts/cleanup_cursor_logs.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash
# cleanup_cursor_logs.sh
# Script to clean up Cursor IDE log files to prevent excessive disk usage

# Location of project
PROJECT_DIR="/Users/lazmini/code/meeting-mcp"

# Check if project directory exists
if [ ! -d "$PROJECT_DIR" ]; then
  echo "Error: Project directory not found at $PROJECT_DIR"
  exit 1
fi

# Check if .cursor directory exists
CURSOR_DIR="$PROJECT_DIR/.cursor"
if [ ! -d "$CURSOR_DIR" ]; then
  echo "No .cursor directory found. Nothing to clean up."
  exit 0
fi

# Current size before cleanup
BEFORE_SIZE=$(du -sh "$CURSOR_DIR" | awk '{print $1}')
echo "Current .cursor directory size: $BEFORE_SIZE"

# Backup important rules files that are not logs
BACKUP_DIR="$PROJECT_DIR/.cursor_backup"
mkdir -p "$BACKUP_DIR"

# Save the rule definitions (not the log content)
if [ -d "$CURSOR_DIR/rules" ]; then
  for file in "$CURSOR_DIR/rules"/*.mdc; do
    if [ -f "$file" ]; then
      # Extract just the first few lines which contain rule definitions
      head -n 10 "$file" > "$BACKUP_DIR/$(basename "$file")"
    fi
  done
  echo "Backed up rule definitions to $BACKUP_DIR"
fi

# Remove or truncate large log files
find "$CURSOR_DIR" -type f -name "*.log" -exec truncate -s 0 {} \;
echo "Truncated log files"

# Check for log files in the parent directory
LOG_DIR="/Users/lazmini/Library/Logs/Claude"
if [ -d "$LOG_DIR" ]; then
  echo "Checking Claude logs directory..."
  
  # Find MCP server logs
  MCP_LOGS=$(find "$LOG_DIR" -name "mcp-server-meetingbaas.log*")
  
  for log_file in $MCP_LOGS; do
    if [ -f "$log_file" ]; then
      echo "Processing log file: $log_file"
      
      # Get file size before
      BEFORE_LOG_SIZE=$(du -h "$log_file" | awk '{print $1}')
      
      # Create a temporary file
      TEMP_FILE=$(mktemp)
      
      # Filter out ping/pong messages and keep other important logs
      grep -v '"method":"ping"' "$log_file" | grep -v '"result":{},"jsonrpc":"2.0","id":[0-9]\+' > "$TEMP_FILE"
      
      # Replace the original file with the filtered content
      mv "$TEMP_FILE" "$log_file"
      
      # Get file size after
      AFTER_LOG_SIZE=$(du -h "$log_file" | awk '{print $1}')
      
      echo "  Removed ping/pong messages: $BEFORE_LOG_SIZE -> $AFTER_LOG_SIZE"
    fi
  done
fi

# Optional: Completely remove the mdc files which contain the full API specs
# Uncomment if you want to remove these completely
# find "$CURSOR_DIR/rules" -type f -name "*.mdc" -delete
# echo "Removed rule definition files"

# Or alternatively, truncate them to just include the essential metadata
for file in "$CURSOR_DIR/rules"/*.mdc; do
  if [ -f "$file" ]; then
    # Keep only the first few lines with metadata and truncate the rest
    head -n 10 "$file" > "$file.tmp" && mv "$file.tmp" "$file"
  fi
done
echo "Truncated rule definition files to essential metadata"

# After cleanup size
AFTER_SIZE=$(du -sh "$CURSOR_DIR" | awk '{print $1}')
echo "New .cursor directory size: $AFTER_SIZE"

echo "Cleanup complete!" 
```

--------------------------------------------------------------------------------
/src/tools/retranscribe.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tool for retranscribing a bot's audio
 */

import { z } from 'zod';
import { apiRequest } from '../api/client.js';
import { createValidSession } from '../utils/auth.js';
import { createTool } from '../utils/tool-types.js';

// Schema for the retranscribe tool parameters
const retranscribeParams = z.object({
  botId: z.string().describe('UUID of the bot to retranscribe'),
  speechToTextProvider: z
    .enum(['Gladia', 'Runpod', 'Default'])
    .optional()
    .describe('Speech-to-text provider to use for transcription (optional)'),
  speechToTextApiKey: z
    .string()
    .optional()
    .describe('API key for the speech-to-text provider if required (optional)'),
  webhookUrl: z
    .string()
    .url()
    .optional()
    .describe('Webhook URL to receive notification when transcription is complete (optional)'),
});

/**
 * Retranscribes a bot's audio using the specified speech-to-text provider.
 * This is useful when you want to:
 * 1. Use a different speech-to-text provider than originally used
 * 2. Retry a failed transcription
 * 3. Get a new transcription with different settings
 */
export const retranscribeTool = createTool(
  'retranscribe_bot',
  "Retranscribe a bot's audio using the Default or your provided Speech to Text Provider",
  retranscribeParams,
  async (args, context) => {
    const { session, log } = context;

    log.info('Retranscribing bot', {
      botId: args.botId,
      provider: args.speechToTextProvider,
      hasApiKey: !!args.speechToTextApiKey,
      hasWebhook: !!args.webhookUrl,
    });

    try {
      // Create a valid session with fallbacks for API key
      const validSession = createValidSession(session, log);

      // Check if we have a valid session with API key
      if (!validSession) {
        return {
          content: [
            {
              type: 'text' as const,
              text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
            },
          ],
          isError: true,
        };
      }

      // Prepare the request body
      const requestBody = {
        bot_uuid: args.botId,
        speech_to_text: args.speechToTextProvider
          ? {
              provider: args.speechToTextProvider,
              api_key: args.speechToTextApiKey,
            }
          : undefined,
        webhook_url: args.webhookUrl,
      };

      // Make the API request
      const response = await apiRequest(validSession, 'post', '/bots/retranscribe', requestBody);

      // Handle different response status codes
      if (response.status === 200) {
        return 'Retranscription request accepted. The transcription will be processed asynchronously.';
      } else if (response.status === 202) {
        return 'Retranscription request accepted and is being processed.';
      } else {
        return `Unexpected response status: ${response.status}`;
      }
    } catch (error) {
      log.error('Error retranscribing bot', { error: String(error), botId: args.botId });
      return `Error retranscribing bot: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
);

```

--------------------------------------------------------------------------------
/src/utils/linkFormatter.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Utilities for formatting and presenting meeting recording links
 */

/**
 * Base URL for the Meeting BaaS viewer
 */
export const VIEWER_BASE_URL = "https://meetingbaas.com/viewer";

/**
 * Formats a meeting link to the recording viewer
 */
export function formatMeetingLink(botId: string, timestamp?: number): string {
  if (!botId) {
    return "";
  }
  
  const baseLink = `${VIEWER_BASE_URL}/${botId}`;
  
  if (timestamp !== undefined && timestamp !== null) {
    return `${baseLink}?t=${Math.floor(timestamp)}`;
  }
  
  return baseLink;
}

/**
 * Creates a rich meeting link display, ready for sharing in chat
 */
export function createShareableLink(
  botId: string, 
  options: {
    title?: string;
    timestamp?: number;
    speakerName?: string;
    description?: string;
  } = {}
): string {
  const { title, timestamp, speakerName, description } = options;
  
  const link = formatMeetingLink(botId, timestamp);
  if (!link) {
    return "⚠️ No meeting link could be generated. Please provide a valid bot ID.";
  }
  
  // Construct the display text
  let displayText = "📽️ **Meeting Recording";
  
  if (title) {
    displayText += `: ${title}**`;
  } else {
    displayText += "**";
  }
  
  // Add timestamp info if provided
  if (timestamp !== undefined) {
    const timestampFormatted = formatTimestamp(timestamp);
    displayText += `\n⏱️ Timestamp: ${timestampFormatted}`;
  }
  
  // Add speaker info if provided
  if (speakerName) {
    displayText += `\n🎤 Speaker: ${speakerName}`;
  }
  
  // Add description if provided
  if (description) {
    displayText += `\n📝 ${description}`;
  }
  
  // Add the actual link
  displayText += `\n\n🔗 [View Recording](${link})`;
  
  return displayText;
}

/**
 * Format a timestamp in seconds to a human-readable format (HH:MM:SS)
 */
function formatTimestamp(seconds: number): string {
  if (seconds === undefined || seconds === null) {
    return "00:00:00";
  }
  
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = Math.floor(seconds % 60);
  
  return [
    hours.toString().padStart(2, "0"),
    minutes.toString().padStart(2, "0"),
    secs.toString().padStart(2, "0"),
  ].join(":");
}

/**
 * Generates a shareable segment for multiple moments in a meeting
 */
export function createMeetingSegmentsList(
  botId: string,
  segments: Array<{
    timestamp: number;
    speaker?: string;
    description: string;
  }>
): string {
  if (!segments || segments.length === 0) {
    return createShareableLink(botId, { title: "Full Recording" });
  }
  
  let result = "## 📽️ Meeting Segments\n\n";
  
  segments.forEach((segment, index) => {
    const link = formatMeetingLink(botId, segment.timestamp);
    const timestampFormatted = formatTimestamp(segment.timestamp);
    
    result += `### Segment ${index + 1}: ${timestampFormatted}\n`;
    if (segment.speaker) {
      result += `**Speaker**: ${segment.speaker}\n`;
    }
    result += `**Description**: ${segment.description}\n`;
    result += `🔗 [Jump to this moment](${link})\n\n`;
  });
  
  result += `\n🔗 [View Full Recording](${formatMeetingLink(botId)})`;
  
  return result;
}

/**
 * Creates a compact single-line meeting link for inline sharing
 */
export function createInlineMeetingLink(botId: string, timestamp?: number, label?: string): string {
  const link = formatMeetingLink(botId, timestamp);
  const displayLabel = label || "View Recording";
  
  return `[${displayLabel}](${link})`;
} 
```

--------------------------------------------------------------------------------
/src/api/client.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * API client for MeetingBaaS API
 */

import axios, { AxiosError, Method } from 'axios';
import { UserError } from 'fastmcp';
import { getApiBaseUrl } from '../config.js';

/**
 * Session type definition
 */
export type SessionAuth = { apiKey: string };

/**
 * Makes a request to the MeetingBaaS API
 */
export async function apiRequest(
  session: SessionAuth | undefined,
  method: Method,
  endpoint: string,
  data: Record<string, unknown> | null = null,
) {
  // Validate session
  if (!session) {
    console.error(`[API Client] No session provided`);
    throw new UserError('Authentication required: No session provided');
  }

  // Extract and validate API key
  const apiKey = session.apiKey;
  if (!apiKey) {
    console.error(`[API Client] No API key in session object`);
    throw new UserError('Authentication required: No API key provided');
  }

  // Normalize API key to string
  const apiKeyString = Array.isArray(apiKey) ? apiKey[0] : apiKey;

  // Make sure we have a valid string API key
  if (typeof apiKeyString !== 'string' || apiKeyString.length === 0) {
    console.error(`[API Client] Invalid API key format`);
    throw new UserError('Authentication error: Invalid API key format');
  }

  try {
    // Set up headers with API key
    const headers = {
      'x-meeting-baas-api-key': apiKeyString,
      'Content-Type': 'application/json',
    };

    // Get the current API base URL
    const apiBaseUrl = getApiBaseUrl();

    // Make the API request
    const response = await axios({
      method,
      url: `${apiBaseUrl}${endpoint}`,
      headers,
      data,
    });

    return response.data;
  } catch (error) {
    // Handle Axios errors
    if (axios.isAxiosError(error)) {
      const axiosError = error as AxiosError;
      console.error(`[API Client] Request failed: ${axiosError.message}`);

      // Handle specific error codes
      if (axiosError.response?.status === 401 || axiosError.response?.status === 403) {
        throw new UserError(`Authentication failed: Invalid API key or insufficient permissions`);
      }

      // Extract error details from response data if available
      if (axiosError.response?.data) {
        throw new UserError(`API Error: ${JSON.stringify(axiosError.response.data)}`);
      }

      throw new UserError(`API Error: ${axiosError.message}`);
    }

    // Handle non-Axios errors
    const err = error instanceof Error ? error : new Error(String(error));
    console.error(`[API Client] Request error: ${err.message}`);
    throw new UserError(`Request error: ${err.message}`);
  }
}

/**
 * Client for the MeetingBaaS API
 */
export class MeetingBaasClient {
  private apiKey: string;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  /**
   * Join a meeting with a bot
   */
  async joinMeeting(params: {
    meeting_url: string;
    bot_name: string | null;
    bot_image?: string | null;
    entry_message?: string | null;
    deduplication_key?: string | null;
    automatic_leave?: {
      noone_joined_timeout?: number | null;
      waiting_room_timeout?: number | null;
    } | null;
    speech_to_text?: {
      provider: string;
      api_key?: string | null;
    } | null;
    streaming?: {
      input?: string | null;
      output?: string | null;
      audio_frequency?: string | null;
    } | null;
    reserved?: boolean;
    recording_mode?: string;
    start_time?: string;
    extra?: Record<string, unknown>;
  }) {
    // Create a session with our API key
    const session: SessionAuth = { apiKey: this.apiKey };

    // Use the existing apiRequest function
    return apiRequest(session, 'post', '/bots', params);
  }
}

```

--------------------------------------------------------------------------------
/src/resources/transcript.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Resources for meeting transcripts and metadata
 */

import type { ResourceResult, ResourceTemplate } from "fastmcp";
import { apiRequest } from "../api/client.js";
import { formatDuration, formatTime } from "../utils/formatters.js";

// Explicitly define transcript interface instead of importing
interface Transcript {
  speaker: string;
  start_time: number;
  words: { text: string }[];
}

// Define our session auth type
type SessionAuth = { apiKey: string };

/**
 * Meeting transcript resource
 */
export const meetingTranscriptResource: ResourceTemplate<
  [
    {
      name: string;
      description: string;
      required: boolean;
    }
  ]
> = {
  uriTemplate: "meeting:transcript/{botId}",
  name: "Meeting Transcript",
  mimeType: "text/plain",
  arguments: [
    {
      name: "botId",
      description: "ID of the bot that recorded the meeting",
      required: true,
    },
  ],
  load: async function (args: Record<string, string>): Promise<ResourceResult> {
    const { botId } = args;

    try {
      const session = { apiKey: "session-key" }; // This will be provided by the context

      const response = await apiRequest(
        session,
        "get",
        `/bots/meeting_data?bot_id=${botId}`
      );

      const transcripts: Transcript[] = response.bot_data.transcripts;

      // Format all transcripts
      const formattedTranscripts = transcripts
        .map((transcript: Transcript) => {
          const text = transcript.words
            .map((word: { text: string }) => word.text)
            .join(" ");
          const startTime = formatTime(transcript.start_time);
          const speaker = transcript.speaker;

          return `[${startTime}] ${speaker}: ${text}`;
        })
        .join("\n\n");

      return {
        text:
          formattedTranscripts || "No transcript available for this meeting.",
      };
    } catch (error: unknown) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      return {
        text: `Error retrieving transcript: ${errorMessage}`,
      };
    }
  },
};

/**
 * Meeting metadata resource
 */
export const meetingMetadataResource: ResourceTemplate<
  [
    {
      name: string;
      description: string;
      required: boolean;
    }
  ]
> = {
  uriTemplate: "meeting:metadata/{botId}",
  name: "Meeting Metadata",
  mimeType: "application/json",
  arguments: [
    {
      name: "botId",
      description: "ID of the bot that recorded the meeting",
      required: true,
    },
  ],
  load: async function (args: Record<string, string>): Promise<ResourceResult> {
    const { botId } = args;

    try {
      const session = { apiKey: "session-key" }; // This will be provided by the context

      const response = await apiRequest(
        session,
        "get",
        `/bots/meeting_data?bot_id=${botId}`
      );

      // Extract and format metadata for easier consumption
      const metadata = {
        duration: response.duration,
        formattedDuration: formatDuration(response.duration),
        videoUrl: response.mp4,
        bot: {
          name: response.bot_data.bot.bot_name,
          meetingUrl: response.bot_data.bot.meeting_url,
          createdAt: response.bot_data.bot.created_at,
          endedAt: response.bot_data.bot.ended_at,
        },
        transcriptSegments: response.bot_data.transcripts.length,
      };

      return {
        text: JSON.stringify(metadata, null, 2),
      };
    } catch (error: unknown) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      return {
        text: `Error retrieving metadata: ${errorMessage}`,
      };
    }
  },
};

```

--------------------------------------------------------------------------------
/src/tools/deleteData.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tool for deleting data associated with a bot
 */

import { z } from 'zod';
import { apiRequest } from '../api/client.js';
import { createValidSession } from '../utils/auth.js';
import { createTool } from '../utils/tool-types.js';

// Schema for the delete data tool parameters
const deleteDataParams = z.object({
  botId: z.string().describe('UUID of the bot to delete data for'),
});

// Delete status types from the API
type DeleteStatus = 'deleted' | 'partiallyDeleted' | 'alreadyDeleted' | 'noDataFound';

// DeleteResponse interface matching the API spec
interface DeleteResponse {
  ok: boolean;
  status: DeleteStatus;
}

/**
 * Deletes the transcription, log files, and video recording, along with all data
 * associated with a bot from Meeting Baas servers.
 *
 * The following public-facing fields will be retained:
 * - meeting_url
 * - created_at
 * - reserved
 * - errors
 * - ended_at
 * - mp4_s3_path (null as the file is deleted)
 * - uuid
 * - bot_param_id
 * - event_id
 * - scheduled_bot_id
 * - api_key_id
 *
 * Note: This endpoint is rate-limited to 5 requests per minute per API key.
 */
export const deleteDataTool = createTool(
  'delete_meeting_data',
  'Delete transcription, log files, and video recording, along with all data associated with a bot',
  deleteDataParams,
  async (args, context) => {
    const { session, log } = context;

    log.info('Deleting data for bot', { botId: args.botId });

    try {
      // Create a valid session with fallbacks for API key
      const validSession = createValidSession(session, log);

      // Check if we have a valid session with API key
      if (!validSession) {
        return {
          content: [
            {
              type: 'text' as const,
              text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
            },
          ],
          isError: true,
        };
      }

      // Make the API request to delete data
      const response = await apiRequest(
        validSession,
        'post',
        `/bots/${args.botId}/delete_data`,
        null,
      );

      // Format response based on status
      if (response.ok) {
        // Handle the DeleteResponse format according to the API spec
        const statusMessages: Record<DeleteStatus, string> = {
          deleted:
            'Successfully deleted all data. The meeting metadata (URL, timestamps, etc.) has been preserved, but all content (recordings, transcriptions, and logs) has been deleted.',
          partiallyDeleted:
            'Partially deleted data. Some content could not be removed, but most data has been deleted. The meeting metadata has been preserved.',
          alreadyDeleted: 'Data was already deleted. No further action was needed.',
          noDataFound: 'No data was found for the specified bot ID.',
        };

        // Extract the DeleteResponse object from the API response
        const data = response.data as unknown as DeleteResponse;

        // Verify the operation was successful according to the API
        if (data && data.ok) {
          // Return the appropriate message based on the status
          return (
            statusMessages[data.status] || `Successfully processed with status: ${data.status}`
          );
        } else if (data && data.status) {
          return `Operation returned ok: false. Status: ${data.status}`;
        } else {
          // Fallback for unexpected response format
          return `Data deleted successfully, but the response format was unexpected: ${JSON.stringify(response.data)}`;
        }
      } else {
        // Handle error responses
        if (response.status === 401) {
          return 'Unauthorized: Missing or invalid API key.';
        } else if (response.status === 403) {
          return "Forbidden: You don't have permission to delete this bot's data.";
        } else if (response.status === 404) {
          return 'Not found: The specified bot ID does not exist.';
        } else if (response.status === 429) {
          return 'Rate limit exceeded: This endpoint is limited to 5 requests per minute per API key. Please try again later.';
        } else {
          return `Failed to delete data: ${JSON.stringify(response)}`;
        }
      }
    } catch (error) {
      log.error('Error deleting data', { error: String(error), botId: args.botId });

      // Check for rate limit error
      if (error instanceof Error && error.message.includes('429')) {
        return 'Rate limit exceeded: This endpoint is limited to 5 requests per minute per API key. Please try again later.';
      }

      return `Error deleting data: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
);

```

--------------------------------------------------------------------------------
/src/tools/search.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Simple MCP tool for retrieving meeting transcripts
 */

import { z } from 'zod';
import { apiRequest } from '../api/client.js';
import { createValidSession } from '../utils/auth.js';
import { createTool } from '../utils/tool-types.js';

// Define transcript-related interfaces that match the actual API response structure
interface TranscriptWord {
  text: string;
  start_time: number;
  end_time: number;
  id?: number;
  bot_id?: number;
  user_id?: number | null;
}

interface TranscriptSegment {
  speaker: string;
  start_time: number;
  end_time?: number | null;
  words: TranscriptWord[];
  id?: number;
  bot_id?: number;
  user_id?: number | null;
  lang?: string | null;
}

interface BotData {
  bot: {
    bot_name: string;
    meeting_url: string;
    [key: string]: any;
  };
  transcripts: TranscriptSegment[];
}

interface MetadataResponse {
  bot_data: BotData;
  duration: number;
  mp4: string;
}

// Define the simple parameters schema
const getTranscriptParams = z.object({
  botId: z.string().describe('ID of the bot/meeting to retrieve transcript for'),
});

/**
 * Tool to get meeting transcript data
 *
 * This tool retrieves meeting data and returns the transcript,
 * properly handling the API response structure.
 */
export const getTranscriptTool = createTool(
  'getMeetingTranscript',
  'Get a meeting transcript with speaker names and content grouped by speaker',
  getTranscriptParams,
  async (args, context) => {
    const { session, log } = context;
    log.info('Getting meeting transcript', { botId: args.botId });

    try {
      // Create a valid session with fallbacks for API key
      const validSession = createValidSession(session, log);

      // Check if we have a valid session with API key
      if (!validSession) {
        return {
          content: [
            {
              type: 'text' as const,
              text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
            },
          ],
          isError: true,
        };
      }

      // Make the API request to get meeting data
      const response = (await apiRequest(
        validSession,
        'get',
        `/bots/meeting_data?bot_id=${args.botId}`,
      )) as MetadataResponse;

      // Check for valid response structure
      if (!response || !response.bot_data) {
        return 'Error: Invalid response structure from API.';
      }

      // Extract meeting information
      const meetingInfo = {
        name: response.bot_data.bot?.bot_name || 'Unknown Meeting',
        url: response.bot_data.bot?.meeting_url || 'Unknown URL',
        duration: response.duration || 0,
      };

      // Extract transcripts from the response
      const transcripts = response.bot_data.transcripts || [];

      // If no transcripts, provide info about the meeting
      if (transcripts.length === 0) {
        return `Meeting "${meetingInfo.name}" has a recording (${Math.floor(meetingInfo.duration / 60)}m ${meetingInfo.duration % 60}s), but no transcript segments are available.`;
      }

      // Group and combine text by speaker
      const speakerTexts: Record<string, string[]> = {};

      // First pass: collect all text segments by speaker
      transcripts.forEach((segment: TranscriptSegment) => {
        const speaker = segment.speaker;
        // Check that words array exists and has content
        if (!segment.words || !Array.isArray(segment.words)) {
          return;
        }

        const text = segment.words
          .map((word) => word.text || '')
          .join(' ')
          .trim();
        if (!text) return; // Skip empty text

        if (!speakerTexts[speaker]) {
          speakerTexts[speaker] = [];
        }

        speakerTexts[speaker].push(text);
      });

      // If after processing we have no text, provide info
      if (Object.keys(speakerTexts).length === 0) {
        return `Meeting "${meetingInfo.name}" has a recording (${Math.floor(meetingInfo.duration / 60)}m ${meetingInfo.duration % 60}s), but could not extract readable transcript.`;
      }

      // Second pass: combine all text segments per speaker
      const combinedBySpeaker = Object.entries(speakerTexts).map(([speaker, texts]) => {
        return {
          speaker,
          text: texts.join(' '),
        };
      });

      // Format the transcript grouped by speaker
      const formattedTranscript = combinedBySpeaker
        .map((entry) => `${entry.speaker}: ${entry.text}`)
        .join('\n\n');

      // Add meeting info header
      const header = `Meeting: "${meetingInfo.name}"\nDuration: ${Math.floor(meetingInfo.duration / 60)}m ${meetingInfo.duration % 60}s\nTranscript:\n\n`;

      return header + formattedTranscript;
    } catch (error) {
      log.error('Error getting transcript', { error: String(error) });
      return `Error getting transcript: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
);

```

--------------------------------------------------------------------------------
/src/utils/tinyDb.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * TinyDB - A simple file-based database for tracking bot usage
 * 
 * This module provides a lightweight persistence layer to track bot usage across sessions.
 * It stores recently used bots along with their metadata for enhanced search experiences.
 */

import fs from 'fs';
import path from 'path';

// Interface for bot metadata in our tiny database
export interface BotRecord {
  id: string;                    // Bot UUID
  name?: string;                 // Bot name if available
  meetingUrl?: string;          // URL of the meeting
  meetingType?: string;         // Type of meeting (e.g., "sales", "standup")
  createdAt?: string;           // When the bot/meeting was created
  lastAccessedAt: string;       // When the bot was last accessed by a user
  accessCount: number;          // How many times this bot has been accessed
  creator?: string;             // Who created/requested the bot
  participants?: string[];      // Meeting participants if known
  topics?: string[];            // Key topics discussed in the meeting
  extra?: Record<string, any>;  // Additional metadata from the original API
}

// Main database class
export class TinyDb {
  private dbPath: string;
  private data: {
    recentBots: BotRecord[];
    lastUpdated: string;
  };
  
  constructor(dbFilePath?: string) {
    // Use provided path or default to the project root
    this.dbPath = dbFilePath || path.resolve(process.cwd(), 'bot-history.json');
    
    // Initialize with empty data
    this.data = {
      recentBots: [],
      lastUpdated: new Date().toISOString()
    };
    
    // Try to load existing data
    this.loadFromFile();
  }
  
  // Load database from file
  private loadFromFile(): void {
    try {
      if (fs.existsSync(this.dbPath)) {
        const fileContent = fs.readFileSync(this.dbPath, 'utf-8');
        this.data = JSON.parse(fileContent);
        console.log(`TinyDB: Loaded ${this.data.recentBots.length} bot records from ${this.dbPath}`);
      } else {
        console.log(`TinyDB: No existing database found at ${this.dbPath}, starting fresh`);
      }
    } catch (error) {
      console.error(`TinyDB: Error loading database:`, error);
      // Continue with empty data
    }
  }
  
  // Save database to file
  private saveToFile(): void {
    try {
      // Update the lastUpdated timestamp
      this.data.lastUpdated = new Date().toISOString();
      
      // Write to file
      fs.writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2), 'utf-8');
      console.log(`TinyDB: Saved ${this.data.recentBots.length} bot records to ${this.dbPath}`);
    } catch (error) {
      console.error(`TinyDB: Error saving database:`, error);
    }
  }
  
  // Add or update a bot record
  public trackBot(botData: Partial<BotRecord> & { id: string }): BotRecord {
    // Find if bot already exists
    const existingIndex = this.data.recentBots.findIndex(bot => bot.id === botData.id);
    
    if (existingIndex >= 0) {
      // Update existing record
      const existingBot = this.data.recentBots[existingIndex];
      
      // Preserve existing data while updating with new data
      const updatedBot: BotRecord = {
        ...existingBot,
        ...botData,
        // Always update these fields
        lastAccessedAt: new Date().toISOString(),
        accessCount: (existingBot.accessCount || 0) + 1,
      };
      
      // Remove from current position
      this.data.recentBots.splice(existingIndex, 1);
      
      // Add to the front (most recent)
      this.data.recentBots.unshift(updatedBot);
      
      // Save changes
      this.saveToFile();
      
      return updatedBot;
    } else {
      // Create new record
      const newBot: BotRecord = {
        ...botData,
        lastAccessedAt: new Date().toISOString(),
        accessCount: 1,
      };
      
      // Add to the front (most recent)
      this.data.recentBots.unshift(newBot);
      
      // Trim the list if it gets too long (keeping most recent 50)
      if (this.data.recentBots.length > 50) {
        this.data.recentBots = this.data.recentBots.slice(0, 50);
      }
      
      // Save changes
      this.saveToFile();
      
      return newBot;
    }
  }
  
  // Get most recent bots (defaults to 5)
  public getRecentBots(limit: number = 5): BotRecord[] {
    return this.data.recentBots.slice(0, limit);
  }
  
  // Get most accessed bots (defaults to 5)
  public getMostAccessedBots(limit: number = 5): BotRecord[] {
    // Sort by access count (descending) and return top ones
    return [...this.data.recentBots]
      .sort((a, b) => (b.accessCount || 0) - (a.accessCount || 0))
      .slice(0, limit);
  }
  
  // Search bots by meeting type
  public getBotsByMeetingType(meetingType: string): BotRecord[] {
    return this.data.recentBots.filter(bot => 
      bot.meetingType?.toLowerCase() === meetingType.toLowerCase()
    );
  }
  
  // Get bot by ID
  public getBot(id: string): BotRecord | undefined {
    return this.data.recentBots.find(bot => bot.id === id);
  }
  
  // Update session with recent bot IDs
  public updateSession(session: any): void {
    if (!session) return;
    
    // Add recentBotIds to session
    session.recentBotIds = this.getRecentBots(5).map(bot => bot.id);
  }
}

// Singleton instance
let db: TinyDb | null = null;

// Get the singleton instance
export function getTinyDb(dbFilePath?: string): TinyDb {
  if (!db) {
    db = new TinyDb(dbFilePath);
  }
  return db;
} 
```

--------------------------------------------------------------------------------
/src/tools/listBots.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tool for listing bots with metadata
 */

import { z } from 'zod';
import { apiRequest } from '../api/client.js';
import { createValidSession } from '../utils/auth.js';
import { createTool } from '../utils/tool-types.js';

// Schema for the list bots with metadata tool parameters
const listBotsParams = z.object({
  limit: z
    .number()
    .int()
    .min(1)
    .max(50)
    .optional()
    .default(10)
    .describe('Maximum number of bots to return (1-50, default: 10)'),
  bot_name: z
    .string()
    .optional()
    .describe('Filter bots by name containing this string (case-insensitive)'),
  meeting_url: z.string().optional().describe('Filter bots by meeting URL containing this string'),
  created_after: z
    .string()
    .optional()
    .describe('Filter bots created after this date (ISO format, e.g., 2023-05-01T00:00:00)'),
  created_before: z
    .string()
    .optional()
    .describe('Filter bots created before this date (ISO format, e.g., 2023-05-31T23:59:59)'),
  filter_by_extra: z
    .string()
    .optional()
    .describe("Filter by extra JSON fields (format: 'field1:value1,field2:value2')"),
  sort_by_extra: z
    .string()
    .optional()
    .describe("Sort by field in extra JSON (format: 'field:asc' or 'field:desc')"),
  cursor: z.string().optional().describe('Cursor for pagination from previous response'),
});

/**
 * Retrieves a paginated list of the user's bots with essential metadata,
 * including IDs, names, and meeting details. Supports filtering, sorting,
 * and advanced querying options.
 */
export const listBotsWithMetadataTool = createTool(
  'list_bots_with_metadata',
  'List recent bots with metadata, including IDs, names, meeting details with filtering and sorting options',
  listBotsParams,
  async (args, context) => {
    const { session, log } = context;

    log.info('Listing bots with metadata', {
      limit: args.limit,
      bot_name: args.bot_name,
      meeting_url: args.meeting_url,
      created_after: args.created_after,
      created_before: args.created_before,
      filter_by_extra: args.filter_by_extra,
      sort_by_extra: args.sort_by_extra,
      cursor: args.cursor,
    });

    try {
      // Create a valid session with fallbacks for API key
      const validSession = createValidSession(session, log);

      // Check if we have a valid session with API key
      if (!validSession) {
        return {
          content: [
            {
              type: 'text' as const,
              text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
            },
          ],
          isError: true,
        };
      }

      // Construct query parameters
      const queryParams = new URLSearchParams();

      if (args.limit !== undefined) queryParams.append('limit', args.limit.toString());
      if (args.bot_name) queryParams.append('bot_name', args.bot_name);
      if (args.meeting_url) queryParams.append('meeting_url', args.meeting_url);
      if (args.created_after) queryParams.append('created_after', args.created_after);
      if (args.created_before) queryParams.append('created_before', args.created_before);
      if (args.filter_by_extra) queryParams.append('filter_by_extra', args.filter_by_extra);
      if (args.sort_by_extra) queryParams.append('sort_by_extra', args.sort_by_extra);
      if (args.cursor) queryParams.append('cursor', args.cursor);

      // Make the API request
      const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
      const response = await apiRequest(
        validSession,
        'get',
        `/bots/bots_with_metadata${queryString}`,
        null,
      );

      // Check if response contains bots
      if (!response.recentBots || !Array.isArray(response.recentBots)) {
        return 'No bots found or unexpected response format.';
      }

      const bots = response.recentBots;

      // Format the response
      if (bots.length === 0) {
        return 'No bots found matching your criteria.';
      }

      // Format the bot list
      const formattedBots = bots
        .map((bot: any, index: number) => {
          // Extract creation date and format it
          const createdAt = bot.createdAt ? new Date(bot.createdAt).toLocaleString() : 'Unknown';

          // Format duration if available
          const duration = bot.duration
            ? `${Math.floor(bot.duration / 60)}m ${bot.duration % 60}s`
            : 'N/A';

          // Extract any customer ID or meeting info from extra (if available)
          const extraInfo = [];
          if (bot.extra) {
            if (bot.extra.customerId) extraInfo.push(`Customer ID: ${bot.extra.customerId}`);
            if (bot.extra.meetingType) extraInfo.push(`Type: ${bot.extra.meetingType}`);
            if (bot.extra.description) extraInfo.push(`Description: ${bot.extra.description}`);
          }

          return `${index + 1}. Bot: ${bot.name || 'Unnamed'} (ID: ${bot.id})
   Created: ${createdAt}
   Duration: ${duration}
   Meeting URL: ${bot.meetingUrl || 'N/A'}
   Status: ${bot.endedAt ? 'Completed' : 'Active'}
   ${extraInfo.length > 0 ? `Additional Info: ${extraInfo.join(', ')}` : ''}
`;
        })
        .join('\n');

      // Add pagination information if available
      let response_text = `Found ${bots.length} bots:\n\n${formattedBots}`;

      if (response.nextCursor) {
        response_text += `\nMore results available. Use cursor: ${response.nextCursor} to see the next page.`;
      }

      return response_text;
    } catch (error) {
      log.error('Error listing bots', { error: String(error) });
      return `Error listing bots: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
);

```

--------------------------------------------------------------------------------
/src/tools/qrcode.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * QR Code Generation Tool
 *
 * This tool generates an AI-powered QR code image that can be used as a bot avatar.
 * It calls the odin.qrcode-ai.com API to generate a customized QR code based on AI prompts.
 */

import { z } from 'zod';
import { createTool, MeetingBaaSTool } from '../utils/tool-types.js';

// API configuration
const QR_API_ENDPOINT = 'https://odin.qrcode-ai.com/api/qrcode';
const DEFAULT_QR_API_KEY = 'qrc_o-Fx3GXW3TC7_cLvatIW1699177588300'; // Default key for demo purposes

// Define available QR code styles
const QR_STYLES = ['style_default', 'style_dots', 'style_rounded', 'style_crystal'] as const;

// Define QR code types
const QR_TYPES = ['url', 'email', 'phone', 'sms', 'text'] as const;

// Define the parameters for the generate QR code tool
const generateQRCodeParams = z.object({
  type: z.enum(QR_TYPES).describe('Type of QR code (url, email, phone, sms, text)'),
  to: z.string().describe('Destination for the QR code (URL, email, phone number, or text)'),
  prompt: z
    .string()
    .max(1000)
    .describe(
      '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"',
    ),
  style: z.enum(QR_STYLES).default('style_default').describe('Style of the QR code'),
  useAsBotImage: z
    .boolean()
    .default(true)
    .describe('Whether to use the generated QR code as the bot avatar'),
  template: z.string().optional().describe('Template ID for the QR code (optional)'),
  apiKey: z
    .string()
    .optional()
    .describe('Your QR Code AI API key (optional, will use default if not provided)'),
});

/**
 * Extracts a QR Code API key from the prompt text
 *
 * @param prompt The prompt text that might contain an API key
 * @returns The extracted API key or null if not found
 */
function extractApiKeyFromPrompt(prompt: string): string | null {
  const patterns = [
    /api\s*key\s*[=:]\s*(qrc_[a-zA-Z0-9_-]+)/i,
    /using\s*api\s*key\s*[=:]\s*(qrc_[a-zA-Z0-9_-]+)/i,
    /with\s*api\s*key\s*[=:]\s*(qrc_[a-zA-Z0-9_-]+)/i,
    /api\s*key\s*is\s*(qrc_[a-zA-Z0-9_-]+)/i,
    /api\s*key\s*(qrc_[a-zA-Z0-9_-]+)/i,
    /(qrc_[a-zA-Z0-9_-]+)/i, // Last resort to just look for the key format
  ];

  for (const pattern of patterns) {
    const match = prompt.match(pattern);
    if (match && match[1]) {
      return match[1];
    }
  }

  return null;
}

/**
 * Cleans the prompt by removing any API key mentions
 *
 * @param prompt The original prompt text
 * @returns The cleaned prompt without API key mentions
 */
function cleanPrompt(prompt: string): string {
  // Remove API key phrases
  let cleaned = prompt.replace(/(\s*api\s*key\s*[=:]\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
  cleaned = cleaned.replace(/(\s*using\s*api\s*key\s*[=:]\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
  cleaned = cleaned.replace(/(\s*with\s*api\s*key\s*[=:]\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
  cleaned = cleaned.replace(/(\s*api\s*key\s*is\s*qrc_[a-zA-Z0-9_-]+)/gi, '');
  cleaned = cleaned.replace(/(\s*api\s*key\s*qrc_[a-zA-Z0-9_-]+)/gi, '');

  // Remove just the key if it exists independently
  cleaned = cleaned.replace(/(\s*qrc_[a-zA-Z0-9_-]+)/gi, '');

  // Trim and clean up double spaces
  cleaned = cleaned.trim().replace(/\s+/g, ' ');

  return cleaned;
}

/**
 * Generate QR Code Tool
 *
 * This tool generates an AI-powered QR code that can be used as a bot avatar.
 */
export const generateQRCodeTool: MeetingBaaSTool<typeof generateQRCodeParams> = createTool(
  'generateQRCode',
  '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".',
  generateQRCodeParams,
  async (args, context) => {
    const { log } = context;
    log.info('Generating QR code', { type: args.type, prompt: args.prompt });

    // 1. Look for API key in the prompt text
    const promptApiKey = extractApiKeyFromPrompt(args.prompt);

    // 2. Clean the prompt by removing API key mentions if found
    const cleanedPrompt = cleanPrompt(args.prompt);

    // 3. Determine which API key to use (priority: 1. Param API key, 2. Prompt API key, 3. Environment variable)
    // Check for QRCODE_API_KEY in process.env or get from config if available
    const defaultApiKey = process.env.QRCODE_API_KEY || DEFAULT_QR_API_KEY || '';
    const effectiveApiKey = args.apiKey || promptApiKey || defaultApiKey;

    // Log which key is being used (without revealing the actual key)
    log.info(
      `Using QR Code API key from: ${args.apiKey ? 'parameter' : promptApiKey ? 'prompt' : defaultApiKey === DEFAULT_QR_API_KEY ? 'default' : 'environment'}`,
    );

    try {
      const response = await fetch(QR_API_ENDPOINT, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': effectiveApiKey,
        },
        body: JSON.stringify({
          type: args.type,
          to: args.to,
          prompt: cleanedPrompt, // Use the cleaned prompt without API key
          style: args.style,
          template: args.template || '67d30dd4d22a25b77317f407', // Default template
        }),
      });

      if (!response.ok) {
        const errorText = await response.text();
        let errorData;
        try {
          errorData = JSON.parse(errorText);
        } catch (e) {
          errorData = { message: errorText };
        }

        log.error('QR Code API error:', { status: response.status, error: errorData });
        return {
          content: [
            {
              type: 'text' as const,
              text: `QR code generation failed: ${response.status} ${errorData.message || errorText}`,
            },
          ],
          isError: true,
        };
      }

      const data = await response.json();

      if (!data.qrcode?.url) {
        log.error('QR code URL not found in response', { data });
        return {
          content: [
            {
              type: 'text' as const,
              text: 'QR code URL not found in response',
            },
          ],
          isError: true,
        };
      }

      // Return the QR code URL
      const qrCodeUrl = data.qrcode.url;
      const responseText =
        `QR code generated successfully!\n\n` +
        `URL: ${qrCodeUrl}\n\n` +
        `This image ${args.useAsBotImage ? 'can be used' : 'will not be used'} as a bot avatar.\n\n` +
        `To create a bot with this QR code image, use the joinMeeting tool with botImage: "${qrCodeUrl}"`;

      return {
        content: [
          {
            type: 'text' as const,
            text: responseText,
          },
        ],
        isError: false,
        metadata: {
          qrCodeUrl: qrCodeUrl,
          useAsBotImage: args.useAsBotImage,
        },
      };
    } catch (error: unknown) {
      log.error('Error generating QR code', { error: String(error) });
      return {
        content: [
          {
            type: 'text' as const,
            text: `Error generating QR code: ${error instanceof Error ? error.message : String(error)}`,
          },
        ],
        isError: true,
      };
    }
  },
);

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Meeting BaaS MCP Server
 *
 * Connects Claude and other AI assistants to Meeting BaaS API,
 * allowing them to manage recordings, transcripts, and calendar data.
 */

import { FastMCP } from 'fastmcp';
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { z } from 'zod';
import { SERVER_CONFIG } from './config.js';

// Import tools from the consolidated export
import {
  cancelRecordingTool,
  cancelRecordingWithCredentialsTool,
  checkCalendarIntegrationTool,
  deleteCalendarTool,
  deleteDataTool,
  findKeyMomentsTool,
  generateQRCodeTool,
  getCalendarTool,
  getEventTool,
  getMeetingDataTool,
  getMeetingDataWithCredentialsTool,
  getTranscriptTool,
  // Meeting tools
  joinMeetingTool,
  leaveMeetingTool,
  listBotsWithMetadataTool,
  listCalendarsTool,
  listEventsTool,
  listEventsWithCredentialsTool,
  listRawCalendarsTool,
  listUpcomingMeetingsTool,
  // Calendar tools
  oauthGuidanceTool,
  resyncAllCalendarsTool,
  retranscribeTool,
  scheduleRecordingTool,
  scheduleRecordingWithCredentialsTool,
  // Environment tools
  selectEnvironmentTool,
  setupCalendarOAuthTool,
  // Link tools
  shareableMeetingLinkTool,
  shareMeetingSegmentsTool,
} from './tools/index.js';

// Import resources
import { meetingMetadataResource, meetingTranscriptResource } from './resources/index.js';

// Define session auth type
type SessionAuth = { apiKey: string };

// Set up proper error logging
// This ensures logs go to stderr instead of stdout to avoid interfering with JSON communication
import { createServerLogger, setupPingFiltering } from './utils/logging.js';

// Set up ping message filtering to reduce log noise
setupPingFiltering();

const serverLog = createServerLogger('MCP Server');

// Add global error handlers to prevent crashes
process.on('unhandledRejection', (reason, promise) => {
  // Check if this is a connection closed error from the MCP protocol
  const error = reason as any;
  if (error && error.code === -32000 && error.message?.includes('Connection closed')) {
    serverLog(`Connection closed gracefully, ignoring error`);
  } else {
    serverLog(`Unhandled Rejection: ${error?.message || String(reason)}`);
    console.error('[MCP Server] Error details:', reason);
  }
});

process.on('uncaughtException', (error) => {
  // Check if this is a connection closed error from the MCP protocol
  const err = error as any; // Cast to any to access non-standard properties
  if (err && err.code === 'ERR_UNHANDLED_ERROR' && err.context?.error?.code === -32000) {
    serverLog(`Connection closed gracefully, ignoring exception`);
  } else {
    serverLog(`Uncaught Exception: ${error?.message || String(error)}`);
    console.error('[MCP Server] Exception details:', error);
  }
});

// Log startup information
serverLog('========== SERVER STARTUP ==========');
serverLog(`Server version: ${SERVER_CONFIG.version}`);
serverLog(`Node version: ${process.version}`);
serverLog(`Running from Claude: ${process.env.MCP_FROM_CLAUDE === 'true' ? 'Yes' : 'No'}`);
serverLog(`Process ID: ${process.pid}`);

// Function to load and process the Claude Desktop config file
async function loadClaudeDesktopConfig() {
  try {
    // Define the expected config path
    const configPath = path.join(
      os.homedir(),
      'Library/Application Support/Claude/claude_desktop_config.json',
    );

    const fileExists = await fs
      .stat(configPath)
      .then(() => true)
      .catch(() => false);
    if (fileExists) {
      serverLog(`Loading config from: ${configPath}`);
      try {
        const configContent = await fs.readFile(configPath, 'utf8');
        const configJson = JSON.parse(configContent);

        // Check for meetingbaas server config
        if (configJson.mcpServers && configJson.mcpServers.meetingbaas) {
          const serverConfig = configJson.mcpServers.meetingbaas;

          // Check for headers
          if (serverConfig.headers) {
            // Check for API key header and set it as an environment variable
            if (serverConfig.headers['x-api-key']) {
              const apiKey = serverConfig.headers['x-api-key'];
              process.env.MEETING_BAAS_API_KEY = apiKey;
              serverLog(`API key loaded from config`);
            }

            // Check for QR code API key in headers
            if (serverConfig.headers['x-api-key']) {
              const qrCodeApiKey = serverConfig.headers['x-api-key'];
              process.env.QRCODE_API_KEY = qrCodeApiKey;
              serverLog(`QR code API key loaded from config`);
            }
          }

          // Check for bot configuration
          if (serverConfig.botConfig) {
            const botConfig = serverConfig.botConfig;
            let configItems = [];

            // Set bot name if available
            if (botConfig.name) {
              process.env.MEETING_BOT_NAME = botConfig.name;
              configItems.push('name');
            }

            // Set bot image if available
            if (botConfig.image) {
              process.env.MEETING_BOT_IMAGE = botConfig.image;
              configItems.push('image');
            }

            // Set bot entry message if available
            if (botConfig.entryMessage) {
              process.env.MEETING_BOT_ENTRY_MESSAGE = botConfig.entryMessage;
              configItems.push('message');
            }

            // Set extra fields if available
            if (botConfig.extra) {
              process.env.MEETING_BOT_EXTRA = JSON.stringify(botConfig.extra);
              configItems.push('extra');
            }

            if (configItems.length > 0) {
              serverLog(`Bot configuration loaded from config: ${configItems.join(', ')}`);
            }
          }
        }
      } catch (parseError) {
        serverLog(`Error parsing config file: ${parseError}`);
      }
    } else {
      serverLog(`Config file not found at ${configPath}`);
    }
  } catch (error) {
    serverLog(`Error loading config file: ${error}`);
  }
}

// Load the Claude Desktop config and set up the server
(async () => {
  // Load and log the Claude Desktop config
  await loadClaudeDesktopConfig();

  // Configure the server
  const server = new FastMCP<SessionAuth>({
    name: SERVER_CONFIG.name,
    version: '1.0.0', // Using explicit semantic version format
    authenticate: async (context: any) => {
      // Use 'any' for now to avoid type errors
      try {
        // Get API key from headers, trying multiple possible locations
        let apiKey =
          // If request object exists (FastMCP newer versions)
          (context.request?.headers && context.request.headers['x-api-key']) ||
          // Or if headers are directly on the context (older versions)
          (context.headers && context.headers['x-api-key']);

        // If API key wasn't found in headers, try environment variable as fallback
        if (!apiKey && process.env.MEETING_BAAS_API_KEY) {
          apiKey = process.env.MEETING_BAAS_API_KEY;
          serverLog(`Using API key from environment variable`);
        }

        if (!apiKey) {
          serverLog(`Authentication failed: No API key found`);
          throw new Response(null, {
            status: 401,
            statusText:
              'API key required in x-api-key header or as MEETING_BAAS_API_KEY environment variable',
          });
        }

        // Ensure apiKey is a string
        const keyValue = Array.isArray(apiKey) ? apiKey[0] : apiKey;

        // Return a session object that will be accessible in context.session
        return { apiKey: keyValue };
      } catch (error) {
        serverLog(`Authentication error: ${error}`);
        throw new Response(null, {
          status: 500,
          statusText: 'Authentication error',
        });
      }
    },
  });

  // Register tools and add debug event listeners
  server.on('connect', (event) => {
    serverLog(`Client connected`);
  });

  server.on('disconnect', (event) => {
    serverLog(`Client disconnected`);
  });

  // Register tools using our helper function - only register the ones we've updated with MeetingBaaSTool
  registerTool(server, joinMeetingTool);
  registerTool(server, leaveMeetingTool);
  registerTool(server, getMeetingDataTool);
  registerTool(server, getMeetingDataWithCredentialsTool);
  registerTool(server, retranscribeTool);

  // For the rest, use the original method until we refactor them
  server.addTool(getTranscriptTool);
  server.addTool(oauthGuidanceTool);
  server.addTool(listRawCalendarsTool);
  server.addTool(setupCalendarOAuthTool);
  server.addTool(listCalendarsTool);
  server.addTool(getCalendarTool);
  server.addTool(deleteCalendarTool);
  server.addTool(resyncAllCalendarsTool);
  server.addTool(listUpcomingMeetingsTool);
  server.addTool(listEventsTool);
  server.addTool(listEventsWithCredentialsTool);
  server.addTool(getEventTool);
  server.addTool(scheduleRecordingTool);
  server.addTool(scheduleRecordingWithCredentialsTool);
  server.addTool(cancelRecordingTool);
  server.addTool(cancelRecordingWithCredentialsTool);
  server.addTool(checkCalendarIntegrationTool);
  server.addTool(shareableMeetingLinkTool);
  server.addTool(shareMeetingSegmentsTool);
  server.addTool(findKeyMomentsTool);
  server.addTool(deleteDataTool);
  server.addTool(listBotsWithMetadataTool);
  server.addTool(selectEnvironmentTool);
  server.addTool(generateQRCodeTool);

  // Register resources
  server.addResourceTemplate(meetingTranscriptResource);
  server.addResourceTemplate(meetingMetadataResource);

  // Determine transport type based on environment
  // If run from Claude Desktop, use stdio transport
  // Otherwise use SSE transport for web/HTTP interfaces
  const isClaudeDesktop = process.env.MCP_FROM_CLAUDE === 'true';

  const transportConfig = isClaudeDesktop
    ? {
        transportType: 'stdio' as const,
        debug: true,
      }
    : {
        transportType: 'sse' as const,
        sse: {
          endpoint: '/mcp' as `/${string}`,
          port: SERVER_CONFIG.port,
        },
      };

  // Start the server
  try {
    server.start(transportConfig);

    if (!isClaudeDesktop) {
      serverLog(`Meeting BaaS MCP Server started on http://localhost:${SERVER_CONFIG.port}/mcp`);
    } else {
      serverLog(`Meeting BaaS MCP Server started in stdio mode for Claude Desktop`);
    }
  } catch (error) {
    serverLog(`Error starting server: ${error}`);
  }
})();

import type { Tool } from 'fastmcp';
import type { MeetingBaaSTool } from './utils/tool-types.js';

/**
 * Helper function to safely register tools with the FastMCP server
 *
 * This function handles the type casting needed to satisfy TypeScript
 * while still using our properly designed MeetingBaaSTool interface
 */
function registerTool<P extends z.ZodType>(
  server: FastMCP<SessionAuth>,
  tool: MeetingBaaSTool<P>,
): void {
  // Cast to any to bypass TypeScript's strict type checking
  // This is safe because our MeetingBaaSTool interface ensures compatibility
  server.addTool(tool as unknown as Tool<SessionAuth, P>);
}

```

--------------------------------------------------------------------------------
/src/tools/meeting.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Meeting tool implementation
 */

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { z } from 'zod';
import { apiRequest, MeetingBaasClient, SessionAuth } from '../api/client.js';
import {
  AUDIO_FREQUENCIES,
  BOT_CONFIG,
  RECORDING_MODES,
  SPEECH_TO_TEXT_PROVIDERS,
} from '../config.js';
import { createValidSession } from '../utils/auth.js';
import { createTool, MeetingBaaSTool } from '../utils/tool-types.js';

// Define the parameters schemas
const joinMeetingParams = z.object({
  meetingUrl: z.string().url().describe('URL of the meeting to join'),
  botName: z
    .string()
    .optional()
    .describe(
      'Name to display for the bot in the meeting (OPTIONAL: if omitted, will use name from configuration)',
    ),
  botImage: z
    .string()
    .nullable()
    .optional()
    .describe(
      "URL to an image to use for the bot's avatar (OPTIONAL: if omitted, will use image from configuration)",
    ),
  entryMessage: z
    .string()
    .optional()
    .describe(
      'Message the bot will send upon joining the meeting (OPTIONAL: if omitted, will use message from configuration)',
    ),
  deduplicationKey: z
    .string()
    .optional()
    .describe(
      'Unique key to override the 5-minute restriction on joining the same meeting with the same API key',
    ),
  nooneJoinedTimeout: z
    .number()
    .int()
    .optional()
    .describe(
      'Timeout in seconds for the bot to wait for participants to join before leaving (default: 600)',
    ),
  waitingRoomTimeout: z
    .number()
    .int()
    .optional()
    .describe(
      'Timeout in seconds for the bot to wait in the waiting room before leaving (default: 600)',
    ),
  speechToTextProvider: z
    .enum(SPEECH_TO_TEXT_PROVIDERS)
    .optional()
    .describe('Speech-to-text provider to use for transcription (default: Default)'),
  speechToTextApiKey: z
    .string()
    .optional()
    .describe('API key for the speech-to-text provider (if required)'),
  streamingInputUrl: z
    .string()
    .optional()
    .describe('WebSocket URL to stream audio input to the bot'),
  streamingOutputUrl: z
    .string()
    .optional()
    .describe('WebSocket URL to stream audio output from the bot'),
  streamingAudioFrequency: z
    .enum(AUDIO_FREQUENCIES)
    .optional()
    .describe('Audio frequency for streaming (default: 16khz)'),
  reserved: z
    .boolean()
    .default(false)
    .describe(
      'Whether to use a bot from the pool of bots or a new one (new ones are created on the fly and instances can take up to 4 minutes to boot',
    ),
  startTime: z
    .string()
    .optional()
    .describe(
      'ISO datetime string. If provided, the bot will join at this time instead of immediately',
    ),
  recordingMode: z.enum(RECORDING_MODES).default('speaker_view').describe('Recording mode'),
  extra: z
    .record(z.string(), z.any())
    .optional()
    .describe(
      'Additional metadata for the meeting (e.g., meeting type, custom summary prompt, search keywords)',
    ),
});

/**
 * Parameters for getting meeting data
 */
const getMeetingDetailsParams = z.object({
  meetingId: z.string().describe('ID of the meeting to get data for'),
});

/**
 * Parameters for getting meeting data with direct credentials
 */
const getMeetingDetailsWithCredentialsParams = z.object({
  meetingId: z.string().describe('ID of the meeting to get data for'),
  apiKey: z.string().describe('API key for authentication'),
});

const stopRecordingParams = z.object({
  botId: z.string().uuid().describe('ID of the bot that recorded the meeting'),
});

// Define our return types
export type JoinMeetingParams = z.infer<typeof joinMeetingParams>;

/**
 * Function to directly read the Claude Desktop config
 */
function readClaudeDesktopConfig(log: any) {
  try {
    // Define the expected config path
    const configPath = path.join(
      os.homedir(),
      'Library/Application Support/Claude/claude_desktop_config.json',
    );

    if (fs.existsSync(configPath)) {
      const configContent = fs.readFileSync(configPath, 'utf8');
      const configJson = JSON.parse(configContent);

      // Check for meetingbaas server config
      if (configJson.mcpServers && configJson.mcpServers.meetingbaas) {
        const serverConfig = configJson.mcpServers.meetingbaas;

        // Check for bot configuration
        if (serverConfig.botConfig) {
          return serverConfig.botConfig;
        }
      }
    }
    return null;
  } catch (error) {
    log.error(`Error reading Claude Desktop config: ${error}`);
    return null;
  }
}

/**
 * Join a meeting
 */
export const joinMeetingTool: MeetingBaaSTool<typeof joinMeetingParams> = createTool(
  'joinMeeting',
  'Have a bot join a meeting now or schedule it for the future. Bot name, image, and entry message will use system defaults if not specified.',
  joinMeetingParams,
  async (args, context) => {
    const { session, log } = context;

    // Directly load Claude Desktop config
    const claudeConfig = readClaudeDesktopConfig(log);

    // Get bot name (user input, config, or prompt to provide)
    let botName = args.botName;

    // If no user-provided name, try Claude config, then BOT_CONFIG
    if (!botName) {
      if (claudeConfig && claudeConfig.name) {
        botName = claudeConfig.name;
      } else if (BOT_CONFIG.defaultBotName) {
        botName = BOT_CONFIG.defaultBotName;
      }
    }

    // Get bot image from various sources
    let botImage: string | null | undefined = args.botImage;
    if (botImage === undefined) {
      if (claudeConfig && claudeConfig.image) {
        botImage = claudeConfig.image;
      } else {
        botImage = BOT_CONFIG.defaultBotImage;
      }
    }

    // Get entry message from various sources
    let entryMessage = args.entryMessage;
    if (!entryMessage) {
      if (claudeConfig && claudeConfig.entryMessage) {
        entryMessage = claudeConfig.entryMessage;
      } else if (BOT_CONFIG.defaultEntryMessage) {
        entryMessage = BOT_CONFIG.defaultEntryMessage;
      }
    }

    // Get extra fields from various sources
    let extra = args.extra;
    if (!extra) {
      if (claudeConfig && claudeConfig.extra) {
        extra = claudeConfig.extra;
      } else if (BOT_CONFIG.defaultExtra) {
        extra = BOT_CONFIG.defaultExtra;
      }
    }

    // Only prompt for a name if no name is available from any source
    if (!botName) {
      log.info('No bot name available from any source');
      return {
        content: [
          {
            type: 'text' as const,
            text: 'Please provide a name for the bot that will join the meeting.',
          },
        ],
        isError: true,
      };
    }

    // Basic logging
    log.info('Joining meeting', { url: args.meetingUrl, botName });

    // Use our utility function to get a valid session with API key
    const validSession = createValidSession(session, log);

    // Verify we have an API key
    if (!validSession) {
      log.error('Authentication failed - no API key available');
      return {
        content: [
          {
            type: 'text' as const,
            text: 'Authentication failed. Please configure your API key in Claude Desktop settings.',
          },
        ],
        isError: true,
      };
    }

    // Prepare API request with the meeting details
    const payload = {
      meeting_url: args.meetingUrl,
      bot_name: botName,
      bot_image: botImage,
      entry_message: entryMessage,
      deduplication_key: args.deduplicationKey,
      reserved: args.reserved,
      recording_mode: args.recordingMode,
      start_time: args.startTime,
      automatic_leave:
        args.nooneJoinedTimeout || args.waitingRoomTimeout
          ? {
              noone_joined_timeout: args.nooneJoinedTimeout,
              waiting_room_timeout: args.waitingRoomTimeout,
            }
          : undefined,
      speech_to_text: args.speechToTextProvider
        ? {
            provider: args.speechToTextProvider,
            api_key: args.speechToTextApiKey,
          }
        : undefined,
      streaming:
        args.streamingInputUrl || args.streamingOutputUrl || args.streamingAudioFrequency
          ? {
              input: args.streamingInputUrl,
              output: args.streamingOutputUrl,
              audio_frequency: args.streamingAudioFrequency,
            }
          : undefined,
      extra: extra,
    };

    try {
      // Use the client to join the meeting with the API key from our valid session
      const client = new MeetingBaasClient(validSession.apiKey);
      const result = await client.joinMeeting(payload);

      // Prepare response message with details
      let responseMessage = `Bot named "${botName}" joined meeting successfully. Bot ID: ${result.bot_id}`;
      if (botImage) responseMessage += '\nCustom bot image is being used.';
      if (entryMessage) responseMessage += '\nThe bot will send an entry message.';
      if (args.startTime) {
        responseMessage += '\nThe bot is scheduled to join at the specified start time.';
      }

      return responseMessage;
    } catch (error) {
      log.error('Failed to join meeting', { error: String(error) });
      return {
        content: [
          {
            type: 'text' as const,
            text: `Failed to join meeting: ${error instanceof Error ? error.message : String(error)}`,
          },
        ],
        isError: true,
      };
    }
  },
);

/**
 * Leave a meeting
 */
export const leaveMeetingTool: MeetingBaaSTool<typeof stopRecordingParams> = createTool(
  'leaveMeeting',
  'Have a bot leave an ongoing meeting',
  stopRecordingParams,
  async (args, context) => {
    const { session, log } = context;
    log.info('Leaving meeting', { botId: args.botId });

    // Create a valid session with fallbacks for API key
    const validSession = createValidSession(session, log);

    // Check if we have a valid session with API key
    if (!validSession) {
      return {
        content: [
          {
            type: 'text' as const,
            text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
          },
        ],
        isError: true,
      };
    }

    const response = await apiRequest(validSession, 'delete', `/bots/${args.botId}`);
    if (response.ok) {
      return 'Bot left the meeting successfully';
    } else {
      return `Failed to make bot leave: ${response.error || 'Unknown error'}`;
    }
  },
);

/**
 * Get meeting metadata and recording information (without transcript details)
 */
export const getMeetingDataTool: MeetingBaaSTool<typeof getMeetingDetailsParams> = createTool(
  'getMeetingData',
  'Get recording metadata and video URL from a meeting',
  getMeetingDetailsParams,
  async (args, context) => {
    const { session, log } = context;
    log.info('Getting meeting metadata', { meetingId: args.meetingId });

    // Create a valid session with fallbacks for API key
    const validSession = createValidSession(session, log);

    // Check if we have a valid session with API key
    if (!validSession) {
      return {
        content: [
          {
            type: 'text' as const,
            text: 'Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly.',
          },
        ],
        isError: true,
      };
    }

    const response = await apiRequest(
      validSession,
      'get',
      `/bots/meeting_data?bot_id=${args.meetingId}`,
    );

    // Format the meeting duration
    const duration = response.duration;
    const minutes = Math.floor(duration / 60);
    const seconds = duration % 60;

    // Extract the meeting name and URL
    const meetingName = response.bot_data.bot?.bot_name || 'Unknown Meeting';
    const meetingUrl = response.bot_data.bot?.meeting_url || 'Unknown URL';

    return {
      content: [
        {
          type: 'text' as const,
          text: `Meeting: "${meetingName}"\nURL: ${meetingUrl}\nDuration: ${minutes}m ${seconds}s\n\nRecording is available at: ${response.mp4}`,
        },
      ],
      isError: false,
    };
  },
);

/**
 * Get meeting metadata and recording information with direct credentials
 */
export const getMeetingDataWithCredentialsTool: MeetingBaaSTool<
  typeof getMeetingDetailsWithCredentialsParams
> = createTool(
  'getMeetingDataWithCredentials',
  'Get recording metadata and video URL from a meeting using direct API credentials',
  getMeetingDetailsWithCredentialsParams,
  async (args, context) => {
    const { log } = context;
    log.info('Getting meeting metadata with direct credentials', { meetingId: args.meetingId });

    // Create a session object with the provided API key
    const session: SessionAuth = { apiKey: args.apiKey };

    const response = await apiRequest(
      session,
      'get',
      `/bots/meeting_data?bot_id=${args.meetingId}`,
    );

    // Format the meeting duration
    const duration = response.duration;
    const minutes = Math.floor(duration / 60);
    const seconds = duration % 60;

    // Extract the meeting name and URL
    const meetingName = response.bot_data.bot?.bot_name || 'Unknown Meeting';
    const meetingUrl = response.bot_data.bot?.meeting_url || 'Unknown URL';

    return {
      content: [
        {
          type: 'text' as const,
          text: `Meeting: "${meetingName}"\nURL: ${meetingUrl}\nDuration: ${minutes}m ${seconds}s\n\nRecording is available at: ${response.mp4}`,
        },
      ],
      isError: false,
    };
  },
);

```

--------------------------------------------------------------------------------
/set_up_for_other_mcp.md:
--------------------------------------------------------------------------------

```markdown
# Meeting BaaS MCP Server Setup Guide

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.

## Prerequisites

- Node.js (v16 or higher)
- npm (v8 or higher)
- Redis (optional, for state management)

## Project Setup

1. Create a new directory for your project and initialize it:

```bash
mkdir my-mcp-server
cd my-mcp-server
npm init -y
```

2. Update your package.json with the necessary dependencies:

```json
{
  "name": "meetingbaas-mcp",
  "version": "1.0.0",
  "description": "Meeting BaaS MCP Server",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node-esm src/index.ts",
    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
    "prepare": "husky install"
  },
  "dependencies": {
    "@meeting-baas/sdk": "^0.2.0",
    "axios": "^1.6.0",
    "express": "^4.18.2",
    "fastmcp": "^1.20.2",
    "redis": "^4.6.13",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.11.19",
    "@types/redis": "^4.0.11",
    "husky": "^8.0.3",
    "prettier": "^3.1.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.3.3"
  }
}
```

3. Install the dependencies:

```bash
npm install
```

4. Create a TypeScript configuration file (tsconfig.json):

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "dist",
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true
  },
  "include": ["src/**/*", "lib/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

## File Structure

Create the following directory structure:

```
my-mcp-server/
├── lib/
│   ├── mcp-api-handler.ts
│   └── tool-adapter.ts
├── src/
│   ├── api/
│   │   └── client.ts
│   ├── tools/
│   │   ├── index.ts
│   │   ├── calendar.ts
│   │   ├── deleteData.ts
│   │   ├── environment.ts
│   │   ├── links.ts
│   │   ├── listBots.ts
│   │   ├── meeting.ts
│   │   ├── qrcode.ts
│   │   ├── retranscribe.ts
│   │   └── search.ts
│   ├── utils/
│   │   ├── logging.ts
│   │   └── tool-types.ts
│   └── index.ts
└── tsconfig.json
```

## Key Files Implementation

### 1. lib/mcp-api-handler.ts

```typescript
import type { Request, Response } from 'express';
import type { Context, FastMCP } from 'fastmcp';
import { createClient } from 'redis';

interface McpApiHandlerOptions {
  capabilities?: {
    tools?: Record<string, { description: string }>;
  };
  port?: number;
  transportType?: 'stdio' | 'sse';
  sse?: {
    endpoint: `/${string}`;
  };
}

export function initializeMcpApiHandler(
  setupServer: (server: FastMCP) => Promise<void>,
  options: McpApiHandlerOptions = {},
): (req: Request, res: Response) => Promise<void> {
  const {
    capabilities = {},
    port = 3000,
    transportType = 'sse',
    sse = { endpoint: '/mcp' },
  } = options;

  // Initialize Redis client
  const redisClient = createClient({
    url: process.env.REDIS_URL || 'redis://localhost:6379',
  });

  // Initialize FastMCP server
  const server = new FastMCP({
    name: 'Meeting BaaS MCP Server',
    version: '1.0.0' as const,
    authenticate: async (context: Context) => {
      const apiKey = context.request?.headers?.['x-api-key'];
      if (!apiKey) {
        throw new Error('API key required');
      }
      return { apiKey: String(apiKey) };
    },
  });

  // Set up server capabilities
  server.setCapabilities(capabilities);

  // Set up server tools
  setupServer(server).catch((error) => {
    console.error('Error setting up server:', error);
  });

  // Handle SSE transport
  if (transportType === 'sse') {
    return async (req: Request, res: Response) => {
      if (req.path !== sse.endpoint) {
        res.status(404).send('Not found');
        return;
      }

      res.setHeader('Content-Type', 'text/event-stream');
      res.setHeader('Cache-Control', 'no-cache');
      res.setHeader('Connection', 'keep-alive');

      const transport = server.createTransport('sse', {
        request: req,
        response: res,
      });

      await server.handleTransport(transport);
    };
  }

  // Handle stdio transport
  if (transportType === 'stdio') {
    const transport = server.createTransport('stdio', {
      stdin: process.stdin,
      stdout: process.stdout,
    });

    server.handleTransport(transport).catch((error) => {
      console.error('Error handling stdio transport:', error);
    });
  }

  // Return a no-op handler for non-SSE requests
  return async (req: Request, res: Response) => {
    res.status(404).send('Not found');
  };
}
```

### 2. lib/tool-adapter.ts

```typescript
import type { Context, Tool } from 'fastmcp';
import type { SessionAuth } from '../src/api/client.js';
import type { MeetingBaaSTool } from '../src/utils/tool-types.js';

// Adapter function to convert Meeting BaaS tools to FastMCP tools
export function adaptTool<P extends Record<string, any>>(
  meetingTool: MeetingBaaSTool<any>
): Tool<SessionAuth, any> {
  return {
    name: meetingTool.name,
    description: meetingTool.description,
    parameters: meetingTool.parameters,
    execute: meetingTool.execute,
  };
}
```

### 3. src/utils/tool-types.ts

```typescript
import type { z } from 'zod';
import type { Context } from 'fastmcp';
import type { SessionAuth } from '../api/client.js';

/**
 * Type for tool execution result
 */
export type ToolResult = {
  content: { type: 'text'; text: string }[];
};

/**
 * Base type for Meeting BaaS tools
 */
export type MeetingBaaSTool<P extends z.ZodType> = {
  name: string;
  description: string;
  parameters: P;
  execute: (params: z.infer<P>, context: Context<SessionAuth>) => Promise<ToolResult>;
};

/**
 * Helper function to create a Meeting BaaS tool with the correct type
 */
export function createTool<P extends z.ZodType>(
  name: string,
  description: string,
  parameters: P,
  execute: (params: z.infer<P>, context: Context<SessionAuth>) => Promise<ToolResult>
): MeetingBaaSTool<P> {
  return {
    name,
    description,
    parameters,
    execute,
  };
}
```

### 4. src/utils/logging.ts

```typescript
/**
 * Creates a server logger
 * 
 * @param prefix - Prefix to add to log messages
 * @returns Logging function
 */
export function createServerLogger(prefix: string) {
  return function log(message: string) {
    const timestamp = new Date().toISOString();
    console.error(`[${timestamp}] [${prefix}] ${message}`);
  };
}

/**
 * Sets up filtering for ping messages
 * 
 * This reduces log noise by filtering out the periodic ping messages
 */
export function setupPingFiltering() {
  const originalStdoutWrite = process.stdout.write.bind(process.stdout);
  const originalStderrWrite = process.stderr.write.bind(process.stderr);

  // Replace stdout.write
  process.stdout.write = ((
    chunk: string | Uint8Array,
    encoding?: BufferEncoding,
    callback?: (err?: Error) => void
  ) => {
    // Filter out ping messages
    if (typeof chunk === 'string' && chunk.includes('"method":"ping"')) {
      return true;
    }
    return originalStdoutWrite(chunk, encoding, callback);
  }) as typeof process.stdout.write;

  // Replace stderr.write
  process.stderr.write = ((
    chunk: string | Uint8Array,
    encoding?: BufferEncoding,
    callback?: (err?: Error) => void
  ) => {
    // Filter out ping messages
    if (typeof chunk === 'string' && chunk.includes('"method":"ping"')) {
      return true;
    }
    return originalStderrWrite(chunk, encoding, callback);
  }) as typeof process.stderr.write;
}
```

### 5. src/api/client.ts

```typescript
import axios from 'axios';

// Session auth type for authenticating with the Meeting BaaS API
export type SessionAuth = { apiKey: string };

// Create an API client with the session auth
export function createApiClient(session: SessionAuth) {
  const apiUrl = process.env.MEETING_BAAS_API_URL || 'https://api.meetingbaas.com';
  
  return axios.create({
    baseURL: apiUrl,
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': session.apiKey,
    },
  });
}
```

### 6. src/index.ts

```typescript
/**
 * Meeting BaaS MCP Server
 *
 * Connects Claude and other AI assistants to Meeting BaaS API,
 * allowing them to manage recordings, transcripts, and calendar data.
 */

import { BaasClient, MpcClient } from '@meeting-baas/sdk';
import type { Context } from 'fastmcp';
import { FastMCP } from 'fastmcp';
import { z } from 'zod';
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { initializeMcpApiHandler } from '../lib/mcp-api-handler.js';
import { adaptTool } from '../lib/tool-adapter.js';
import type { SessionAuth } from './api/client.js';
import { createTool } from './utils/tool-types.js';

// Set up proper error logging
import { createServerLogger, setupPingFiltering } from './utils/logging.js';

// Set up ping message filtering to reduce log noise
setupPingFiltering();

const serverLog = createServerLogger('MCP Server');

// Add global error handlers to prevent crashes
process.on('unhandledRejection', (reason, promise) => {
  // Check if this is a connection closed error from the MCP protocol
  const error = reason as any;
  if (error && error.code === -32000 && error.message?.includes('Connection closed')) {
    serverLog(`Connection closed gracefully, ignoring error`);
  } else {
    serverLog(`Unhandled Rejection: ${error?.message || String(reason)}`);
    console.error('[MCP Server] Error details:', reason);
  }
});

process.on('uncaughtException', (error) => {
  // Check if this is a connection closed error from the MCP protocol
  const err = error as any;
  if (err && err.code === 'ERR_UNHANDLED_ERROR' && err.context?.error?.code === -32000) {
    serverLog(`Connection closed gracefully, ignoring exception`);
  } else {
    serverLog(`Uncaught Exception: ${error?.message || String(error)}`);
    console.error('[MCP Server] Exception details:', error);
  }
});

// Log startup information
serverLog('========== SERVER STARTUP ==========');
serverLog(`Node version: ${process.version}`);
serverLog(`Running from Claude: ${process.env.MCP_FROM_CLAUDE === 'true' ? 'Yes' : 'No'}`);
serverLog(`Process ID: ${process.pid}`);

// Function to load and process the Claude Desktop config file
async function loadClaudeDesktopConfig() {
  try {
    // Define the expected config path
    const configPath = path.join(
      os.homedir(),
      'Library/Application Support/Claude/claude_desktop_config.json',
    );

    const fileExists = await fs
      .stat(configPath)
      .then(() => true)
      .catch(() => false);
    if (fileExists) {
      serverLog(`Loading config from: ${configPath}`);
      try {
        const configContent = await fs.readFile(configPath, 'utf8');
        const configJson = JSON.parse(configContent);

        // Check for meetingbaas server config
        if (configJson.mcpServers && configJson.mcpServers.meetingbaas) {
          const serverConfig = configJson.mcpServers.meetingbaas;

          // Check for headers
          if (serverConfig.headers) {
            // Check for API key header and set it as an environment variable
            if (serverConfig.headers['x-api-key']) {
              const apiKey = serverConfig.headers['x-api-key'];
              process.env.MEETING_BAAS_API_KEY = apiKey;
              serverLog(`API key loaded from config`);
            }

            // Check for QR code API key in headers
            if (serverConfig.headers['x-api-key']) {
              const qrCodeApiKey = serverConfig.headers['x-api-key'];
              process.env.QRCODE_API_KEY = qrCodeApiKey;
              serverLog(`QR code API key loaded from config`);
            }
          }

          // Check for bot configuration
          if (serverConfig.botConfig) {
            const botConfig = serverConfig.botConfig;
            let configItems = [];

            // Set bot name if available
            if (botConfig.name) {
              process.env.MEETING_BOT_NAME = botConfig.name;
              configItems.push('name');
            }

            // Set bot image if available
            if (botConfig.image) {
              process.env.MEETING_BOT_IMAGE = botConfig.image;
              configItems.push('image');
            }

            // Set bot entry message if available
            if (botConfig.entryMessage) {
              process.env.MEETING_BOT_ENTRY_MESSAGE = botConfig.entryMessage;
              configItems.push('message');
            }

            // Set extra fields if available
            if (botConfig.extra) {
              process.env.MEETING_BOT_EXTRA = JSON.stringify(botConfig.extra);
              configItems.push('extra');
            }

            if (configItems.length > 0) {
              serverLog(`Bot configuration loaded from config: ${configItems.join(', ')}`);
            }
          }
        }
      } catch (parseError) {
        serverLog(`Error parsing config file: ${parseError}`);
      }
    } else {
      serverLog(`Config file not found at ${configPath}`);
    }
  } catch (error) {
    serverLog(`Error loading config file: ${error}`);
  }
}

// Initialize the BaaS client
const baasClient = new BaasClient({
  apiKey: process.env.MEETING_BAAS_API_KEY || '',
});

// Initialize MPC client for tool registration
const mpcClient = new MpcClient({
  serverUrl: process.env.MPC_SERVER_URL || 'http://localhost:7020',
});

interface ToolParameter {
  name: string;
  required?: boolean;
  schema?: {
    type: string;
  };
}

// Helper function to convert MPC parameter definition to Zod schema
function convertToZodSchema(parameters: ToolParameter[]): z.ZodType {
  const schema: Record<string, z.ZodType> = {};
  for (const param of parameters) {
    if (param.required) {
      schema[param.name] = z.string(); // Default to string for now, can be expanded based on param.schema.type
    } else {
      schema[param.name] = z.string().optional();
    }
  }
  return z.object(schema);
}

const handler = initializeMcpApiHandler(
  async (server: FastMCP) => {
    // Register all Meeting BaaS tools automatically
    const tools = mpcClient.getRegisteredTools();
    for (const tool of tools) {
      const paramsSchema = convertToZodSchema(tool.parameters || []);
      const meetingTool = createTool(
        tool.name,
        tool.description || 'Meeting BaaS tool',
        paramsSchema,
        async (params: Record<string, unknown>, context: Context<SessionAuth>) => {
          // Handle tool execution here
          return {
            content: [{ type: 'text', text: `Tool ${tool.name} executed` }],
          };
        },
      );
      server.addTool(adaptTool(meetingTool));
    }

    // Keep the existing echo tool as an example
    const echoTool = createTool(
      'echo',
      'Echo a message',
      z.object({ message: z.string() }),
      async ({ message }: { message: string }, context: Context<SessionAuth>) => ({
        content: [{ type: 'text', text: `Tool echo: ${message}` }],
      }),
    );
    server.addTool(adaptTool(echoTool));
  },
  {
    capabilities: {
      tools: {
        echo: {
          description: 'Echo a message',
        },
        // Meeting BaaS tools will be automatically added to capabilities
      },
    },
    port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
    transportType: process.env.MCP_FROM_CLAUDE === 'true' ? 'stdio' : 'sse',
    sse: {
      endpoint: '/mcp' as `/${string}`,
    },
  },
);

// Load the Claude Desktop config and start the server
(async () => {
  // Load and log the Claude Desktop config
  await loadClaudeDesktopConfig();

  // Start the server
  try {
    const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
    const isClaudeDesktop = process.env.MCP_FROM_CLAUDE === 'true';

    if (!isClaudeDesktop) {
      serverLog(`Meeting BaaS MCP Server starting on http://localhost:${port}/mcp`);
    } else {
      serverLog(`Meeting BaaS MCP Server starting in stdio mode for Claude Desktop`);
    }

    // Start the server using the appropriate method based on environment
    if (isClaudeDesktop) {
      // For Claude Desktop, we use stdio transport
      process.stdin.pipe(process.stdout);
    } else {
      // For web/HTTP interface, we use the handler directly
      const http = require('http');
      const server = http.createServer(handler);
      server.listen(port, () => {
        serverLog(`Server listening on port ${port}`);
      });
    }
  } catch (error) {
    serverLog(`Error starting server: ${error}`);
  }
})();

export default handler;
```

## Tool Examples

Here are examples of tools that you can implement in the tools directory:

### src/tools/environment.ts

```typescript
import { z } from 'zod';
import { createTool } from '../utils/tool-types.js';

/**
 * Tool to select the environment for API requests
 */
export const selectEnvironmentTool = createTool(
  'selectEnvironment',
  'Switch between API environments',
  z.object({
    environment: z.enum(['production', 'staging', 'development']),
  }),
  async ({ environment }, context) => {
    // Set the environment for subsequent requests
    process.env.MEETING_BAAS_API_ENVIRONMENT = environment;
    
    return {
      content: [
        { type: 'text', text: `Environment set to ${environment}` },
      ],
    };
  }
);
```

### src/tools/deleteData.ts

```typescript
import { z } from 'zod';
import { createApiClient } from '../api/client.js';
import { createTool } from '../utils/tool-types.js';

/**
 * Tool to delete data associated with a bot
 */
export const deleteDataTool = createTool(
  'deleteData',
  'Delete data associated with a bot',
  z.object({
    botId: z.string().uuid(),
  }),
  async ({ botId }, context) => {
    try {
      const client = createApiClient(context.session);
      
      // Call the API to delete bot data
      await client.delete(`/bots/${botId}/data`);
      
      return {
        content: [
          { type: 'text', text: `Successfully deleted data for bot ${botId}` },
        ],
      };
    } catch (error: any) {
      return {
        content: [
          { type: 'text', text: `Error deleting data: ${error.message}` },
        ],
      };
    }
  }
);
```

### src/tools/listBots.ts

```typescript
import { z } from 'zod';
import { createApiClient } from '../api/client.js';
import { createTool } from '../utils/tool-types.js';

/**
 * Tool to list bots with metadata
 */
export const listBotsWithMetadataTool = createTool(
  'listBotsWithMetadata',
  'List all bots with their metadata',
  z.object({}),
  async (_, context) => {
    try {
      const client = createApiClient(context.session);
      
      // Call the API to get bots
      const response = await client.get('/bots');
      const bots = response.data;
      
      return {
        content: [
          { type: 'text', text: JSON.stringify(bots, null, 2) },
        ],
      };
    } catch (error: any) {
      return {
        content: [
          { type: 'text', text: `Error listing bots: ${error.message}` },
        ],
      };
    }
  }
);
```

### src/tools/index.ts

```typescript
// Export all tools
export { deleteDataTool } from './deleteData.js';
export { listBotsWithMetadataTool } from './listBots.js';
export { selectEnvironmentTool } from './environment.js';
// Add exports for other tools
```

## Running the Server

1. Build the project:

```bash
npm run build
```

2. Start the server:

```bash
npm start
```

## Environment Variables

The server supports the following environment variables:

- `MEETING_BAAS_API_KEY`: API key for authenticating with the Meeting BaaS API
- `MEETING_BAAS_API_URL`: Base URL for the Meeting BaaS API (default: https://api.meetingbaas.com)
- `PORT`: Port to run the server on (default: 3000)
- `MCP_FROM_CLAUDE`: Set to 'true' when running from Claude Desktop
- `REDIS_URL`: URL for Redis connection (optional)
- `MPC_SERVER_URL`: URL for the MPC server (default: http://localhost:7020)
- `MEETING_BOT_NAME`: Default name for bots
- `MEETING_BOT_IMAGE`: Default image URL for bots
- `MEETING_BOT_ENTRY_MESSAGE`: Default entry message for bots
- `MEETING_BOT_EXTRA`: JSON string with extra bot configuration

## Additional Resources

- [FastMCP Documentation](https://github.com/fastmcp/fastmcp)
- [Meeting BaaS SDK Documentation](https://github.com/meeting-baas/sdk)
- [Zod Documentation](https://github.com/colinhacks/zod)
```

--------------------------------------------------------------------------------
/src/tools/links.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Meeting recording link generation and sharing tools
 */

import { z } from "zod";
import type { Context, TextContent } from "fastmcp";
import { apiRequest, SessionAuth } from "../api/client.js";
import { 
  createShareableLink, 
  createMeetingSegmentsList, 
  createInlineMeetingLink 
} from "../utils/linkFormatter.js";
import { createValidSession } from "../utils/auth.js";

/**
 * Schema for generating a shareable link to a meeting
 */
const shareableMeetingLinkParams = z.object({
  botId: z.string().describe("ID of the bot that recorded the meeting"),
  timestamp: z.number().optional().describe("Timestamp in seconds to link to a specific moment (optional)"),
  title: z.string().optional().describe("Title to display for the meeting (optional)"),
  speakerName: z.string().optional().describe("Name of the speaker at this timestamp (optional)"),
  description: z.string().optional().describe("Brief description of what's happening at this timestamp (optional)"),
});

/**
 * Tool for generating a shareable meeting link
 */
export const shareableMeetingLinkTool = {
  name: "shareableMeetingLink",
  description: "Generate a shareable link to a specific moment in a meeting recording",
  parameters: shareableMeetingLinkParams,
  execute: async (args: z.infer<typeof shareableMeetingLinkParams>, context: Context<SessionAuth>) => {
    const { session, log } = context;
    log.info("Generating shareable meeting link", { botId: args.botId });
    
    try {
      // Create a valid session with fallbacks for API key
      const validSession = createValidSession(session, log);
      
      // Check if we have a valid session with API key
      if (!validSession) {
        return {
          content: [
            {
              type: "text" as const,
              text: "Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly."
            }
          ],
          isError: true
        };
      }
      
      // Get the meeting data to verify the bot ID exists
      const response = await apiRequest(
        validSession,
        "get",
        `/bots/meeting_data?bot_id=${args.botId}`
      );
      
      // If we got a response, the bot exists, so we can generate a link
      const shareableLink = createShareableLink(args.botId, {
        timestamp: args.timestamp,
        title: args.title,
        speakerName: args.speakerName,
        description: args.description
      });
      
      return shareableLink;
      
    } catch (error) {
      return `Error generating shareable link: ${error instanceof Error ? error.message : String(error)}. Please check that the bot ID is correct.`;
    }
  }
};

/**
 * Schema for generating links to multiple timestamps in a meeting
 */
const shareMeetingSegmentsParams = z.object({
  botId: z.string().describe("ID of the bot that recorded the meeting"),
  segments: z.array(
    z.object({
      timestamp: z.number().describe("Timestamp in seconds"),
      speaker: z.string().optional().describe("Name of the speaker at this timestamp (optional)"),
      description: z.string().describe("Brief description of what's happening at this timestamp"),
    })
  ).describe("List of meeting segments to share")
});

/**
 * Tool for sharing multiple segments from a meeting
 */
export const shareMeetingSegmentsTool = {
  name: "shareMeetingSegments",
  description: "Generate a list of links to important moments in a meeting",
  parameters: shareMeetingSegmentsParams,
  execute: async (args: z.infer<typeof shareMeetingSegmentsParams>, context: Context<SessionAuth>) => {
    const { session, log } = context;
    log.info("Sharing meeting segments", { botId: args.botId, segments: args.segments });
    
    try {
      // Create a valid session with fallbacks for API key
      const validSession = createValidSession(session, log);
      
      // Check if we have a valid session with API key
      if (!validSession) {
        return {
          content: [
            {
              type: "text" as const,
              text: "Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly."
            }
          ],
          isError: true
        };
      }
      
      // Get the meeting data to verify the bot ID exists
      const response = await apiRequest(
        validSession,
        "get",
        `/bots/meeting_data?bot_id=${args.botId}`
      );
      
      // If we got a response, the bot exists, so we can generate the segments
      const segmentsList = createMeetingSegmentsList(args.botId, args.segments);
      
      return segmentsList;
      
    } catch (error) {
      return `Error generating meeting segments: ${error instanceof Error ? error.message : String(error)}. Please check that the bot ID is correct.`;
    }
  }
};

/**
 * Schema for finding key moments in a meeting and sharing them
 */
const findKeyMomentsParams = z.object({
  botId: z.string().describe("ID of the bot that recorded the meeting - required"),
  meetingTitle: z.string().optional().describe("Title of the meeting (optional)"),
  topics: z.array(z.string()).optional().describe("List of topics to look for in the meeting (optional)"),
  maxMoments: z.number().default(5).describe("Maximum number of key moments to find"),
  granularity: z.enum(["high", "medium", "low"]).default("medium")
    .describe("Level of detail for topic extraction: 'high' finds many specific topics, 'medium' is balanced, 'low' finds fewer broad topics"),
  autoDetectTopics: z.boolean().default(true)
    .describe("Automatically detect important topics in the meeting without requiring predefined topics"),
  initialChunkSize: z.number().default(1200)
    .describe("Initial chunk size in seconds to analyze (default 20 minutes)"),
});

/**
 * Tool for automatically finding and sharing key moments from a meeting
 */
export const findKeyMomentsTool = {
  name: "findKeyMoments",
  description: "Automatically find and share key moments and topics from a meeting recording with configurable granularity",
  parameters: findKeyMomentsParams,
  execute: async (args: z.infer<typeof findKeyMomentsParams>, context: Context<SessionAuth>) => {
    const { session, log } = context;
    log.info("Finding key moments in meeting", { 
      botId: args.botId, 
      granularity: args.granularity,
      maxMoments: args.maxMoments,
      initialChunkSize: args.initialChunkSize
    });
    
    try {
      // Create a valid session with fallbacks for API key
      const validSession = createValidSession(session, log);
      
      // Check if we have a valid session with API key
      if (!validSession) {
        return {
          content: [
            {
              type: "text" as const,
              text: "Authentication failed. Please configure your API key in Claude Desktop settings or provide it directly."
            }
          ],
          isError: true
        };
      }
      
      // Get the meeting data using the explicitly provided botId
      const response = await apiRequest(
        validSession,
        "get",
        `/bots/meeting_data?bot_id=${args.botId}`
      );
      
      if (!response?.bot_data?.bot) {
        return `Could not find meeting data for the provided bot ID: ${args.botId}`;
      }
      
      const meetingTitle = args.meetingTitle || response.bot_data.bot.bot_name || "Meeting Recording";
      
      // Get the transcripts
      const transcripts = response.bot_data.transcripts || [];
      
      if (transcripts.length === 0) {
        return `No transcript found for meeting "${meetingTitle}". You can still view the recording:\n\n${createShareableLink(args.botId, { title: meetingTitle })}`;
      }
      
      // Sort all transcripts chronologically
      const sortedTranscripts = [...transcripts].sort((a, b) => a.start_time - b.start_time);
      
      // Get meeting duration info
      const meetingStart = sortedTranscripts[0].start_time;
      const meetingEnd = sortedTranscripts[sortedTranscripts.length - 1].start_time;
      const meetingDuration = meetingEnd - meetingStart;
      
      log.info("Processing meeting transcript", { 
        segmentCount: sortedTranscripts.length,
        durationSeconds: meetingDuration
      });
      
      // STEP 1: Group transcripts into larger contextual chunks
      // This preserves context while making processing more manageable
      const contextChunks = groupTranscriptsIntoChunks(sortedTranscripts, 300); // 5-minute chunks
      
      // STEP 2: Identify important segments and topics
      let allMeetingTopics: string[] = args.topics || [];
      const candidateSegments: any[] = [];
      
      // First, analyze each chunk to find patterns and topics
      for (const chunk of contextChunks) {
        // Only do topic detection if requested
        if (args.autoDetectTopics) {
          const detectedTopics = identifyTopicsWithAI(chunk);
          allMeetingTopics = [...allMeetingTopics, ...detectedTopics];
        }
        
        // Find important segments in this chunk
        const importantSegments = findImportantSegments(chunk);
        candidateSegments.push(...importantSegments);
        
        // Find conversation segments (multiple speakers)
        const conversationSegments = findConversationalExchanges(chunk);
        candidateSegments.push(...conversationSegments);
      }
      
      // Deduplicate topics
      const uniqueTopics = [...new Set(allMeetingTopics)];
      
      // STEP 3: Score and rank all candidate segments
      const scoredSegments = scoreSegments(candidateSegments);
      
      // STEP 4: Ensure structural segments (beginning, end) are included
      const structuralSegments = getStructuralSegments(sortedTranscripts);
      const allSegments = [...scoredSegments, ...structuralSegments];
      
      // STEP 5: Sort by importance, then deduplicate
      allSegments.sort((a, b) => b.importance - a.importance);
      const dedupedSegments = deduplicateSegments(allSegments);
      
      // STEP 6: Resort by chronological order and take top N
      const chronologicalSegments = dedupedSegments.sort((a, b) => a.timestamp - b.timestamp);
      const finalSegments = chronologicalSegments.slice(0, args.maxMoments);
      
      // If we have no segments, return a message
      if (finalSegments.length === 0) {
        return `No key moments found in meeting "${meetingTitle}". You can view the full recording:\n\n${createShareableLink(args.botId, { title: meetingTitle })}`;
      }
      
      // Format the segments for display
      const formattedSegments = finalSegments.map(segment => ({
        timestamp: segment.timestamp,
        speaker: segment.speaker,
        description: segment.description
      }));
      
      // Create the segments list with the full title
      const segmentsList = createMeetingSegmentsList(args.botId, formattedSegments);
      
      // Include topics if they were detected
      let result = `# Key Moments from ${meetingTitle}\n\n`;
      
      if (uniqueTopics.length > 0) {
        const topicLimit = args.granularity === "high" ? 10 : args.granularity === "medium" ? 7 : 5;
        const topTopics = uniqueTopics.slice(0, topicLimit);
        
        result += `## Main Topics Discussed\n${topTopics.map(topic => `- ${topic}`).join('\n')}\n\n`;
      }
      
      result += segmentsList;
      
      return result;
      
    } catch (error) {
      return `Error finding key moments: ${error instanceof Error ? error.message : String(error)}. Please check that the bot ID is correct.`;
    }
  }
};

/**
 * Group transcripts into larger chunks for context preservation
 */
function groupTranscriptsIntoChunks(transcripts: any[], maxChunkDuration: number = 300): any[][] {
  if (!transcripts || transcripts.length === 0) return [];
  
  const chunks: any[][] = [];
  let currentChunk: any[] = [];
  let chunkStartTime = transcripts[0].start_time;
  
  for (const segment of transcripts) {
    if (currentChunk.length === 0 || (segment.start_time - chunkStartTime <= maxChunkDuration)) {
      currentChunk.push(segment);
    } else {
      chunks.push(currentChunk);
      currentChunk = [segment];
      chunkStartTime = segment.start_time;
    }
  }
  
  // Add the last chunk if it has any segments
  if (currentChunk.length > 0) {
    chunks.push(currentChunk);
  }
  
  return chunks;
}

/**
 * AI-based topic identification that works across any domain or language
 * Uses natural language processing patterns to identify important concepts
 */
function identifyTopicsWithAI(transcripts: any[]): string[] {
  if (!transcripts || transcripts.length === 0) return [];
  
  // Extract the text from all segments
  const allText = transcripts.map(t => {
    return t.words ? t.words.map((w: any) => w.text).join(" ") : "";
  }).join(" ");
  
  // Split into sentences for better context
  const sentences = allText.split(/[.!?]+/).filter(s => s.trim().length > 0);
  
  // Identify potential topics through pattern analysis
  const topics: Record<string, number> = {};
  
  // AI-like pattern recognition for topics:
  // 1. Look for repeated meaningful phrases
  // 2. Look for phrases that appear after introductory patterns
  // 3. Look for phrases with specific part-of-speech patterns (noun phrases)
  
  // Pattern 1: Repeated phrases (frequency-based)
  const phraseFrequency = findRepeatedPhrases(allText);
  Object.entries(phraseFrequency)
    .filter(([_, count]) => count > 1) // Only phrases that appear multiple times
    .forEach(([phrase, _]) => {
      topics[phrase] = (topics[phrase] || 0) + 2; // Weight by 2
    });
  
  // Pattern 2: Introductory phrases
  // Look for phrases like "talking about X", "discussing X", "focused on X"
  for (const sentence of sentences) {
    const introPatterns = [
      {regex: /(?:talk|talking|discuss|discussing|focus|focusing|about|regarding)\s+([a-z0-9\s]{3,30})/i, group: 1},
      {regex: /(?:main|key|important)\s+(?:topic|point|issue|concern)\s+(?:is|was|being)\s+([a-z0-9\s]{3,30})/i, group: 1},
      {regex: /(?:related to|concerning|with regards to)\s+([a-z0-9\s]{3,30})/i, group: 1},
    ];
    
    for (const pattern of introPatterns) {
      const matches = sentence.match(pattern.regex);
      if (matches && matches[pattern.group]) {
        const topic = matches[pattern.group].trim();
        if (topic.length > 3) {
          topics[topic] = (topics[topic] || 0) + 3; // Weight by 3
        }
      }
    }
  }
  
  // Pattern 3: Noun phrase detection (simplified)
  // Look for phrases with specific patterns like "Noun Noun" or "Adjective Noun"
  const nounPhrasePatterns = [
    /(?:[A-Z][a-z]+)\s+(?:[a-z]+ing|[a-z]+ment|[a-z]+tion)/g, // E.g., "Data processing", "Risk management"
    /(?:[A-Z][a-z]+)\s+(?:[A-Z][a-z]+)/g, // E.g., "Health Insurance", "Business Agreement"
    /(?:the|our|your|their)\s+([a-z]+\s+[a-z]+)/gi, // E.g., "the pricing model", "your business needs"
  ];
  
  for (const pattern of nounPhrasePatterns) {
    const matches = allText.match(pattern) || [];
    for (const match of matches) {
      if (match.length > 5) {
        topics[match] = (topics[match] || 0) + 1;
      }
    }
  }
  
  // Sort topics by score and take top N
  const sortedTopics = Object.entries(topics)
    .sort((a, b) => b[1] - a[1])
    .map(([topic]) => topic);
  
  return sortedTopics.slice(0, 10); // Return top 10 topics
}

/**
 * Find repeated phrases in text that might indicate important topics
 */
function findRepeatedPhrases(text: string): Record<string, number> {
  const phrases: Record<string, number> = {};
  
  // Normalize text
  const normalizedText = text.toLowerCase().replace(/[^\w\s]/g, '');
  
  // Split text into words
  const words = normalizedText.split(/\s+/).filter(w => w.length > 2);
  
  // Look for 2-3 word phrases
  for (let size = 2; size <= 3; size++) {
    if (words.length < size) continue;
    
    for (let i = 0; i <= words.length - size; i++) {
      const phrase = words.slice(i, i + size).join(' ');
      
      // Filter out phrases that are too short
      if (phrase.length > 5) {
        phrases[phrase] = (phrases[phrase] || 0) + 1;
      }
    }
  }
  
  return phrases;
}

/**
 * Find segments that appear to be important based on content analysis
 */
function findImportantSegments(transcripts: any[]): any[] {
  if (!transcripts || transcripts.length === 0) return [];
  
  const importantSegments = [];
  
  // Patterns that indicate importance
  const importancePatterns = [
    {regex: /(?:important|key|critical|essential|significant|main|major)/i, weight: 3},
    {regex: /(?:summarize|summary|summarizing|conclude|conclusion|in conclusion|to sum up)/i, weight: 4},
    {regex: /(?:need to|have to|must|should|will|going to|plan to|action item)/i, weight: 2},
    {regex: /(?:agree|disagree|consensus|decision|decide|decided|determined)/i, weight: 3},
    {regex: /(?:problem|issue|challenge|obstacle|difficulty)/i, weight: 2},
    {regex: /(?:solution|resolve|solve|approach|strategy|tactic)/i, weight: 2},
    {regex: /(?:next steps|follow up|get back|circle back|future|next time)/i, weight: 3},
  ];
  
  for (const transcript of transcripts) {
    if (!transcript.words) continue;
    
    const text = transcript.words.map((w: any) => w.text).join(" ");
    
    // Calculate an importance score based on matching patterns
    let importanceScore = 0;
    
    // Check for matches with importance patterns
    for (const pattern of importancePatterns) {
      if (pattern.regex.test(text)) {
        importanceScore += pattern.weight;
      }
    }
    
    // Also consider length - longer segments might be more substantive
    importanceScore += Math.min(2, Math.floor(text.split(/\s+/).length / 20));
    
    // If the segment has some importance, add it to results
    if (importanceScore > 0) {
      importantSegments.push({
        timestamp: transcript.start_time,
        speaker: transcript.speaker || "Unknown speaker",
        text,
        importance: importanceScore,
        type: 'content',
        description: determineDescription(text, importanceScore)
      });
    }
  }
  
  return importantSegments;
}

/**
 * Determine an appropriate description for a segment based on its content
 */
function determineDescription(text: string, importance: number): string {
  // Try to find a suitable description based on content patterns
  
  if (/(?:summarize|summary|summarizing|conclude|conclusion|in conclusion|to sum up)/i.test(text)) {
    return "Summary or conclusion";
  }
  
  if (/(?:next steps|follow up|moving forward|future|plan)/i.test(text)) {
    return "Discussion about next steps";
  }
  
  if (/(?:agree|disagree|consensus|decision|decide|decided|determined)/i.test(text)) {
    return "Decision point";
  }
  
  if (/(?:problem|issue|challenge|obstacle|difficulty)/i.test(text)) {
    return "Problem discussion";
  }
  
  if (/(?:solution|resolve|solve|approach|strategy|tactic)/i.test(text)) {
    return "Solution discussion";
  }
  
  // Default description based on importance
  if (importance > 5) {
    return "Highly important discussion";
  } else if (importance > 3) {
    return "Important point";
  } else {
    return "Notable discussion";
  }
}

/**
 * Find segments with active conversation between multiple speakers
 */
function findConversationalExchanges(transcripts: any[]): any[] {
  if (!transcripts || transcripts.length < 3) return [];
  
  const conversationSegments = [];
  
  // Look for rapid exchanges between different speakers
  for (let i = 0; i < transcripts.length - 2; i++) {
    const segment1 = transcripts[i];
    const segment2 = transcripts[i+1];
    const segment3 = transcripts[i+2];
    
    // Check if there are at least 2 different speakers
    const speakers = new Set([
      segment1.speaker, 
      segment2.speaker, 
      segment3.speaker
    ].filter(Boolean));
    
    if (speakers.size >= 2) {
      // Check if the segments are close in time (rapid exchange)
      const timeSpan = segment3.start_time - segment1.start_time;
      
      if (timeSpan < 60) { // Less than 1 minute for 3 segments = pretty active conversation
        conversationSegments.push({
          timestamp: segment1.start_time,
          speaker: segment1.speaker || "Unknown speaker",
          text: segment1.words ? segment1.words.map((w: any) => w.text).join(" ") : "",
          importance: 2 + speakers.size, // More speakers = more important
          type: 'conversation',
          description: `Active discussion with ${speakers.size} participants`
        });
        
        // Skip ahead to avoid overlapping conversation segments
        i += 2;
      }
    }
  }
  
  return conversationSegments;
}

/**
 * Get structural segments like start and end of meeting
 */
function getStructuralSegments(transcripts: any[]): any[] {
  if (!transcripts || transcripts.length === 0) return [];
  
  const result = [];
  
  // Add meeting start
  const first = transcripts[0];
  result.push({
    timestamp: first.start_time,
    speaker: first.speaker || "Unknown speaker",
    text: first.words ? first.words.map((w: any) => w.text).join(" ") : "",
    importance: 5, // High importance
    type: 'structural',
    description: "Meeting start"
  });
  
  // Add meeting end if it's a different segment
  if (transcripts.length > 1) {
    const last = transcripts[transcripts.length - 1];
    if (last.start_time !== first.start_time) {
      result.push({
        timestamp: last.start_time,
        speaker: last.speaker || "Unknown speaker",
        text: last.words ? last.words.map((w: any) => w.text).join(" ") : "",
        importance: 4, // High importance
        type: 'structural',
        description: "Meeting conclusion"
      });
    }
  }
  
  return result;
}

/**
 * Score segments based on various factors to determine overall importance
 */
function scoreSegments(segments: any[]): any[] {
  if (!segments || segments.length === 0) return [];
  
  return segments.map(segment => {
    // Add any additional scoring factors here
    return segment;
  });
}

/**
 * Deduplicate segments that are too close to each other
 * Keeps the most important segment when duplicates are found
 */
function deduplicateSegments(segments: any[]): any[] {
  if (segments.length <= 1) return segments;
  
  const result: any[] = [];
  const usedTimeRanges: number[] = [];
  
  // Process segments in order of importance
  for (const segment of segments) {
    // Check if this segment is too close to an already included one
    const isTooClose = usedTimeRanges.some(range => 
      Math.abs(segment.timestamp - range) < 30  // 30 seconds threshold
    );
    
    if (!isTooClose) {
      result.push(segment);
      usedTimeRanges.push(segment.timestamp);
    }
  }
  
  return result;
} 

```

--------------------------------------------------------------------------------
/src/tools/calendar.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * MCP tools for calendar integration
 *
 * This module provides a comprehensive set of tools for integrating with
 * Google and Microsoft calendars through the Meeting BaaS API. It includes
 * tools for OAuth setup, calendar management, event management, and recording scheduling.
 */

import type { Context, TextContent } from 'fastmcp';
import { z } from 'zod';
import { apiRequest } from '../api/client.js';
import { Calendar, CalendarEvent } from '../types/index.js';

// Define our session auth type
type SessionAuth = { apiKey: string };

// Define parameter schemas
const emptyParams = z.object({});

// Schema for OAuth setup and raw calendar listing
const oauthSetupParams = z.object({
  platform: z
    .enum(['Google', 'Microsoft'])
    .describe('The calendar provider platform (Google or Microsoft)'),
  clientId: z
    .string()
    .describe('OAuth client ID obtained from Google Cloud Console or Microsoft Azure portal'),
  clientSecret: z
    .string()
    .describe('OAuth client secret obtained from Google Cloud Console or Microsoft Azure portal'),
  refreshToken: z
    .string()
    .describe('OAuth refresh token obtained after user grants calendar access'),
  rawCalendarId: z
    .string()
    .optional()
    .describe(
      'Optional ID of specific calendar to integrate (from listRawCalendars). If not provided, the primary calendar is used',
    ),
});

const calendarIdParams = z.object({
  calendarId: z.string().uuid().describe('UUID of the calendar to query'),
});

const upcomingMeetingsParams = z.object({
  calendarId: z.string().uuid().describe('UUID of the calendar to query'),
  status: z
    .enum(['upcoming', 'past', 'all'])
    .optional()
    .default('upcoming')
    .describe("Filter for meeting status: 'upcoming' (default), 'past', or 'all'"),
  limit: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .default(20)
    .describe('Maximum number of events to return'),
});

const listEventsParams = z.object({
  calendarId: z.string().uuid().describe('UUID of the calendar to query'),
  status: z
    .enum(['upcoming', 'past', 'all'])
    .optional()
    .default('upcoming')
    .describe("Filter for meeting status: 'upcoming' (default), 'past', or 'all'"),
  startDateGte: z
    .string()
    .optional()
    .describe(
      "Filter events with start date ≥ this ISO-8601 timestamp (e.g., '2023-01-01T00:00:00Z')",
    ),
  startDateLte: z
    .string()
    .optional()
    .describe(
      "Filter events with start date ≤ this ISO-8601 timestamp (e.g., '2023-12-31T23:59:59Z')",
    ),
  attendeeEmail: z
    .string()
    .email()
    .optional()
    .describe('Filter events including this attendee email'),
  organizerEmail: z.string().email().optional().describe('Filter events with this organizer email'),
  updatedAtGte: z
    .string()
    .optional()
    .describe('Filter events updated at or after this ISO-8601 timestamp'),
  cursor: z.string().optional().describe('Pagination cursor from previous response'),
});

const eventIdParams = z.object({
  eventId: z.string().uuid().describe('UUID of the calendar event to query'),
});

const scheduleRecordingParams = z.object({
  eventId: z.string().uuid().describe('UUID of the calendar event to record'),
  botName: z.string().describe('Name to display for the bot in the meeting'),
  botImage: z.string().url().optional().describe("URL to an image for the bot's avatar (optional)"),
  entryMessage: z
    .string()
    .optional()
    .describe('Message the bot will send when joining the meeting (optional)'),
  recordingMode: z
    .enum(['speaker_view', 'gallery_view', 'audio_only'] as const)
    .default('speaker_view')
    .describe("Recording mode: 'speaker_view' (default), 'gallery_view', or 'audio_only'"),
  speechToTextProvider: z
    .enum(['Gladia', 'Runpod', 'Default'] as const)
    .optional()
    .describe('Provider for speech-to-text transcription (optional)'),
  speechToTextApiKey: z
    .string()
    .optional()
    .describe('API key for the speech-to-text provider if required (optional)'),
  extra: z
    .record(z.any())
    .optional()
    .describe('Additional metadata about the meeting (e.g., meetingType, participants)'),
  allOccurrences: z
    .boolean()
    .optional()
    .default(false)
    .describe(
      'For recurring events, schedule recording for all occurrences (true) or just this instance (false)',
    ),
});

const cancelRecordingParams = z.object({
  eventId: z.string().uuid().describe('UUID of the calendar event to cancel recording for'),
  allOccurrences: z
    .boolean()
    .optional()
    .default(false)
    .describe(
      'For recurring events, cancel recording for all occurrences (true) or just this instance (false)',
    ),
});

// Tool type with correct typing
type Tool<P extends z.ZodType<any, any>> = {
  name: string;
  description: string;
  parameters: P;
  execute: (
    args: z.infer<P>,
    context: Context<SessionAuth>,
  ) => Promise<string | { content: TextContent[] }>;
};

/**
 * List available calendars
 */
export const listCalendarsTool: Tool<typeof emptyParams> = {
  name: 'listCalendars',
  description: 'List all calendars integrated with Meeting BaaS',
  parameters: emptyParams,
  execute: async (_args, context) => {
    const { session, log } = context;
    log.info('Listing calendars');

    const response = await apiRequest(session, 'get', '/calendars/');

    if (response.length === 0) {
      return 'No calendars found. You can add a calendar using the setupCalendarOAuth tool.';
    }

    const calendarList = response
      .map((cal: Calendar) => `- ${cal.name} (${cal.email}) [ID: ${cal.uuid}]`)
      .join('\n');

    return `Found ${response.length} calendars:\n\n${calendarList}`;
  },
};

/**
 * Get calendar details
 */
export const getCalendarTool: Tool<typeof calendarIdParams> = {
  name: 'getCalendar',
  description: 'Get detailed information about a specific calendar integration',
  parameters: calendarIdParams,
  execute: async (args, context) => {
    const { session, log } = context;
    log.info('Getting calendar details', { calendarId: args.calendarId });

    const response = await apiRequest(session, 'get', `/calendars/${args.calendarId}`);

    return `Calendar Details:
Name: ${response.name}
Email: ${response.email}
Platform ID: ${response.google_id || response.microsoft_id}
UUID: ${response.uuid}
${response.resource_id ? `Resource ID: ${response.resource_id}` : ''}`;
  },
};

/**
 * List raw calendars (before integration)
 */
export const listRawCalendarsTool: Tool<typeof oauthSetupParams> = {
  name: 'listRawCalendars',
  description: 'List available calendars from Google or Microsoft before integration',
  parameters: oauthSetupParams,
  execute: async (args, context) => {
    const { session, log } = context;
    log.info('Listing raw calendars', { platform: args.platform });

    const payload = {
      oauth_client_id: args.clientId,
      oauth_client_secret: args.clientSecret,
      oauth_refresh_token: args.refreshToken,
      platform: args.platform,
    };

    try {
      const response = await apiRequest(session, 'post', '/calendars/raw', payload);

      if (!response.calendars || response.calendars.length === 0) {
        return 'No calendars found. Please check your OAuth credentials.';
      }

      const calendarList = response.calendars
        .map((cal: any) => {
          const isPrimary = cal.is_primary ? ' (Primary)' : '';
          return `- ${cal.email}${isPrimary} [ID: ${cal.id}]`;
        })
        .join('\n');

      return `Found ${response.calendars.length} raw calendars. You can use the setupCalendarOAuth tool to integrate any of these:\n\n${calendarList}\n\nGuidance: Copy the ID of the calendar you want to integrate and use it as the rawCalendarId parameter in setupCalendarOAuth.`;
    } catch (error) {
      return `Error listing raw calendars: ${error instanceof Error ? error.message : String(error)}\n\nGuidance for obtaining OAuth credentials:\n\n1. For Google:\n   - Go to Google Cloud Console (https://console.cloud.google.com)\n   - Create a project and enable the Google Calendar API\n   - Create OAuth 2.0 credentials (client ID and secret)\n   - Set up consent screen with calendar scopes\n   - Use OAuth playground (https://developers.google.com/oauthplayground) to get a refresh token\n\n2. For Microsoft:\n   - Go to Azure Portal (https://portal.azure.com)\n   - Register an app in Azure AD\n   - Add Microsoft Graph API permissions for calendars\n   - Create a client secret\n   - Use a tool like Postman to get a refresh token`;
    }
  },
};

/**
 * Setup calendar OAuth integration
 */
export const setupCalendarOAuthTool: Tool<typeof oauthSetupParams> = {
  name: 'setupCalendarOAuth',
  description: 'Integrate a calendar using OAuth credentials',
  parameters: oauthSetupParams,
  execute: async (args, context) => {
    const { session, log } = context;
    log.info('Setting up calendar OAuth', { platform: args.platform });

    const payload: {
      oauth_client_id: string;
      oauth_client_secret: string;
      oauth_refresh_token: string;
      platform: 'Google' | 'Microsoft';
      raw_calendar_id?: string;
    } = {
      oauth_client_id: args.clientId,
      oauth_client_secret: args.clientSecret,
      oauth_refresh_token: args.refreshToken,
      platform: args.platform,
    };

    if (args.rawCalendarId) {
      payload.raw_calendar_id = args.rawCalendarId;
    }

    try {
      const response = await apiRequest(session, 'post', '/calendars/', payload);

      return `Calendar successfully integrated!\n\nDetails:
Name: ${response.calendar.name}
Email: ${response.calendar.email}
UUID: ${response.calendar.uuid}

You can now use this UUID to list events or schedule recordings.`;
    } catch (error) {
      return `Error setting up calendar: ${error instanceof Error ? error.message : String(error)}\n\nPlease verify your OAuth credentials. Here's how to obtain them:\n\n1. For Google Calendar:\n   - Visit https://console.cloud.google.com\n   - Create a project and enable Google Calendar API\n   - Configure OAuth consent screen\n   - Create OAuth client ID and secret\n   - Use OAuth playground to get refresh token\n\n2. For Microsoft Calendar:\n   - Visit https://portal.azure.com\n   - Register an application\n   - Add Microsoft Graph calendar permissions\n   - Create client secret\n   - Complete OAuth flow to get refresh token`;
    }
  },
};

/**
 * Delete calendar integration
 */
export const deleteCalendarTool: Tool<typeof calendarIdParams> = {
  name: 'deleteCalendar',
  description: 'Permanently remove a calendar integration',
  parameters: calendarIdParams,
  execute: async (args, context) => {
    const { session, log } = context;
    log.info('Deleting calendar', { calendarId: args.calendarId });

    try {
      await apiRequest(session, 'delete', `/calendars/${args.calendarId}`);

      return 'Calendar integration has been successfully removed. All associated events and scheduled recordings have been deleted.';
    } catch (error) {
      return `Error deleting calendar: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
};

/**
 * Force resync of all calendars
 */
export const resyncAllCalendarsTool: Tool<typeof emptyParams> = {
  name: 'resyncAllCalendars',
  description: 'Force a resync of all connected calendars',
  parameters: emptyParams,
  execute: async (_args, context) => {
    const { session, log } = context;
    log.info('Resyncing all calendars');

    try {
      const response = await apiRequest(session, 'post', '/calendars/resync_all');

      const syncedCount = response.synced_calendars?.length || 0;
      const errorCount = response.errors?.length || 0;

      let result = `Calendar sync operation completed.\n\n${syncedCount} calendars synced successfully.`;

      if (errorCount > 0) {
        result += `\n\n${errorCount} calendars failed to sync:`;
        response.errors.forEach((error: any) => {
          result += `\n- Calendar ${error[0]}: ${error[1]}`;
        });
      }

      return result;
    } catch (error) {
      return `Error resyncing calendars: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
};

/**
 * List upcoming meetings
 */
export const listUpcomingMeetingsTool: Tool<typeof upcomingMeetingsParams> = {
  name: 'listUpcomingMeetings',
  description: 'List upcoming meetings from a calendar',
  parameters: upcomingMeetingsParams,
  execute: async (args, context) => {
    const { session, log } = context;
    log.info('Listing upcoming meetings', {
      calendarId: args.calendarId,
      status: args.status,
    });

    const response = await apiRequest(
      session,
      'get',
      `/calendar_events/?calendar_id=${args.calendarId}&status=${args.status}`,
    );

    if (!response.data || response.data.length === 0) {
      return `No ${args.status} meetings found in this calendar.`;
    }

    const meetings = response.data.slice(0, args.limit);

    const meetingList = meetings
      .map((meeting: CalendarEvent) => {
        const startTime = new Date(meeting.start_time).toLocaleString();
        const hasBot = meeting.bot_param ? '🤖 Bot scheduled' : '';
        const meetingLink = meeting.meeting_url ? `Link: ${meeting.meeting_url}` : '';

        return `- ${meeting.name} [${startTime}] ${hasBot} ${meetingLink} [ID: ${meeting.uuid}]`;
      })
      .join('\n');

    let result = `${args.status.charAt(0).toUpperCase() + args.status.slice(1)} meetings:\n\n${meetingList}`;

    if (response.next) {
      result += `\n\nMore meetings available. Use 'cursor: ${response.next}' to see more.`;
    }

    return result;
  },
};

/**
 * List events with comprehensive filtering
 */
export const listEventsTool: Tool<typeof listEventsParams> = {
  name: 'listEvents',
  description: 'List calendar events with comprehensive filtering options',
  parameters: listEventsParams,
  execute: async (args, context) => {
    const { session, log } = context;
    log.info('Listing calendar events', {
      calendarId: args.calendarId,
      filters: args,
    });

    // Build the query parameters
    let queryParams = `calendar_id=${args.calendarId}`;
    if (args.status) queryParams += `&status=${args.status}`;
    if (args.startDateGte)
      queryParams += `&start_date_gte=${encodeURIComponent(args.startDateGte)}`;
    if (args.startDateLte)
      queryParams += `&start_date_lte=${encodeURIComponent(args.startDateLte)}`;
    if (args.attendeeEmail)
      queryParams += `&attendee_email=${encodeURIComponent(args.attendeeEmail)}`;
    if (args.organizerEmail)
      queryParams += `&organizer_email=${encodeURIComponent(args.organizerEmail)}`;
    if (args.updatedAtGte)
      queryParams += `&updated_at_gte=${encodeURIComponent(args.updatedAtGte)}`;
    if (args.cursor) queryParams += `&cursor=${encodeURIComponent(args.cursor)}`;

    try {
      const response = await apiRequest(session, 'get', `/calendar_events/?${queryParams}`);

      if (!response.data || response.data.length === 0) {
        return 'No events found matching your criteria.';
      }

      const eventList = response.data
        .map((event: CalendarEvent) => {
          const startTime = new Date(event.start_time).toLocaleString();
          const endTime = new Date(event.end_time).toLocaleString();
          const hasBot = event.bot_param ? '🤖 Bot scheduled' : '';
          const meetingLink = event.meeting_url ? `\n   Link: ${event.meeting_url}` : '';
          const attendees =
            event.attendees && event.attendees.length > 0
              ? `\n   Attendees: ${event.attendees.map((a: { name?: string; email: string }) => a.name || a.email).join(', ')}`
              : '';

          return `- ${event.name}\n   From: ${startTime}\n   To: ${endTime}${meetingLink}${attendees}\n   ${hasBot} [ID: ${event.uuid}]`;
        })
        .join('\n\n');

      let result = `Events (${response.data.length}):\n\n${eventList}`;

      if (response.next) {
        result += `\n\nMore events available. Use cursor: "${response.next}" to see more.`;
      }

      return result;
    } catch (error) {
      return `Error listing events: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
};

/**
 * Get event details
 */
export const getEventTool: Tool<typeof eventIdParams> = {
  name: 'getEvent',
  description: 'Get detailed information about a specific calendar event',
  parameters: eventIdParams,
  execute: async (args, context) => {
    const { session, log } = context;
    log.info('Getting event details', { eventId: args.eventId });

    try {
      const event = await apiRequest(session, 'get', `/calendar_events/${args.eventId}`);

      const startTime = new Date(event.start_time).toLocaleString();
      const endTime = new Date(event.end_time).toLocaleString();

      const attendees =
        event.attendees && event.attendees.length > 0
          ? event.attendees
              .map(
                (a: { name?: string; email: string }) => `   - ${a.name || 'Unnamed'} (${a.email})`,
              )
              .join('\n')
          : '   None';

      let botDetails = 'None';
      if (event.bot_param) {
        botDetails = `
   Name: ${event.bot_param.bot_name}
   Recording Mode: ${event.bot_param.recording_mode || 'speaker_view'}
   Meeting Type: ${event.bot_param.extra?.meetingType || 'Not specified'}`;
      }

      return `Event Details:
Title: ${event.name}
Time: ${startTime} to ${endTime}
Meeting URL: ${event.meeting_url || 'Not available'}
Is Organizer: ${event.is_organizer ? 'Yes' : 'No'}
Is Recurring: ${event.is_recurring ? 'Yes' : 'No'}
${event.recurring_event_id ? `Recurring Event ID: ${event.recurring_event_id}` : ''}

Attendees:
${attendees}

Bot Configuration:
${botDetails}

Event ID: ${event.uuid}`;
    } catch (error) {
      return `Error getting event details: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
};

/**
 * Schedule a recording bot
 */
export const scheduleRecordingTool: Tool<typeof scheduleRecordingParams> = {
  name: 'scheduleRecording',
  description: 'Schedule a bot to record an upcoming meeting from your calendar',
  parameters: scheduleRecordingParams,
  execute: async (args, context) => {
    const { session, log } = context;
    log.info('Scheduling meeting recording', {
      eventId: args.eventId,
      botName: args.botName,
      recordingMode: args.recordingMode,
      allOccurrences: args.allOccurrences,
    });

    const payload: any = {
      bot_name: args.botName,
      extra: args.extra || {},
    };

    if (args.botImage) payload.bot_image = args.botImage;
    if (args.entryMessage) payload.enter_message = args.entryMessage;
    if (args.recordingMode) payload.recording_mode = args.recordingMode;

    if (args.speechToTextProvider) {
      payload.speech_to_text = {
        provider: args.speechToTextProvider,
      };

      if (args.speechToTextApiKey) {
        payload.speech_to_text.api_key = args.speechToTextApiKey;
      }
    }

    try {
      let url = `/calendar_events/${args.eventId}/bot`;
      if (args.allOccurrences) {
        url += `?all_occurrences=true`;
      }

      const response = await apiRequest(session, 'post', url, payload);

      // Check if we got a successful response with event data
      if (Array.isArray(response) && response.length > 0) {
        const eventCount = response.length;
        const firstEventName = response[0].name;

        if (eventCount === 1) {
          return `Recording has been scheduled successfully for "${firstEventName}".`;
        } else {
          return `Recording has been scheduled successfully for ${eventCount} instances of "${firstEventName}".`;
        }
      }

      return 'Recording has been scheduled successfully.';
    } catch (error) {
      return `Error scheduling recording: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
};

/**
 * Cancel a scheduled recording
 */
export const cancelRecordingTool: Tool<typeof cancelRecordingParams> = {
  name: 'cancelRecording',
  description: 'Cancel a previously scheduled recording',
  parameters: cancelRecordingParams,
  execute: async (args, context) => {
    const { session, log } = context;
    log.info('Canceling recording', {
      eventId: args.eventId,
      allOccurrences: args.allOccurrences,
    });

    try {
      let url = `/calendar_events/${args.eventId}/bot`;
      if (args.allOccurrences) {
        url += `?all_occurrences=true`;
      }

      const response = await apiRequest(session, 'delete', url);

      // Check if we got a successful response with event data
      if (Array.isArray(response) && response.length > 0) {
        const eventCount = response.length;
        const firstEventName = response[0].name;

        if (eventCount === 1) {
          return `Recording has been canceled successfully for "${firstEventName}".`;
        } else {
          return `Recording has been canceled successfully for ${eventCount} instances of "${firstEventName}".`;
        }
      }

      return 'Recording has been canceled successfully.';
    } catch (error) {
      return `Error canceling recording: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
};

/**
 * Provides guidance on setting up OAuth for calendar integration
 */
export const oauthGuidanceTool: Tool<typeof emptyParams> = {
  name: 'oauthGuidance',
  description: 'Get detailed guidance on setting up OAuth for calendar integration',
  parameters: emptyParams,
  execute: async (_args, context) => {
    const { log } = context;
    log.info('Providing OAuth guidance');

    return `# Calendar Integration Options

## Quick Integration Options

You have two simple ways to integrate your calendar:

### Option 1: Provide credentials directly in this chat
You can simply provide your credentials right here:

\`\`\`
"Set up my calendar with these credentials:
- Platform: Google (or Microsoft)
- Client ID: your-client-id-here
- Client Secret: your-client-secret-here
- Refresh Token: your-refresh-token-here
- Raw Calendar ID: [email protected] (optional)"
\`\`\`

### Option 2: Configure once in Claude Desktop settings (recommended)
For a more permanent solution that doesn't require entering credentials each time:

1. Edit your configuration file:
   \`\`\`bash
   vim ~/Library/Application\\ Support/Claude/claude_desktop_config.json
   \`\`\`

2. Add the \`calendarOAuth\` section to your botConfig:
   \`\`\`json
   "botConfig": {
     // other bot configuration...
     
     "calendarOAuth": {
       "platform": "Google",  // or "Microsoft"
       "clientId": "YOUR_OAUTH_CLIENT_ID",
       "clientSecret": "YOUR_OAUTH_CLIENT_SECRET", 
       "refreshToken": "YOUR_REFRESH_TOKEN",
       "rawCalendarId": "[email protected]"  // Optional
     }
   }
   \`\`\`

3. Save the file and restart Claude Desktop

> **Note:** Calendar integration is completely optional. You can use Meeting BaaS without connecting a calendar.

## Need OAuth Credentials?

If you need to obtain OAuth credentials first, here's how:

<details>
<summary>## Detailed Google Calendar OAuth Setup Instructions</summary>

### Step 1: Create a Google Cloud Project
1. Go to [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project or select an existing one
3. Enable the Google Calendar API for your project

### Step 2: Set Up OAuth Consent Screen
1. Go to "OAuth consent screen" in the left sidebar
2. Select user type (Internal or External)
3. Fill in required app information
4. Add scopes for Calendar API:
   - \`https://www.googleapis.com/auth/calendar.readonly\`
   - \`https://www.googleapis.com/auth/calendar.events.readonly\`

### Step 3: Create OAuth Client ID
1. Go to "Credentials" in the left sidebar
2. Click "Create Credentials" > "OAuth client ID"
3. Select "Web application" as application type
4. Add authorized redirect URIs (including \`https://developers.google.com/oauthplayground\` for testing)
5. Save your Client ID and Client Secret

### Step 4: Get Refresh Token
1. Go to [OAuth Playground](https://developers.google.com/oauthplayground)
2. Click the gear icon (settings) and check "Use your own OAuth credentials"
3. Enter your Client ID and Client Secret
4. Select Calendar API scopes and authorize
5. Exchange authorization code for tokens
6. Save the refresh token
</details>

<details>
<summary>## Detailed Microsoft Calendar OAuth Setup Instructions</summary>

### Step 1: Register Application in Azure
1. Go to [Azure Portal](https://portal.azure.com)
2. Navigate to "App registrations" and create a new registration
3. Set redirect URIs (web or mobile as appropriate)

### Step 2: Set API Permissions
1. Go to "API permissions" in your app registration
2. Add Microsoft Graph permissions:
   - \`Calendars.Read\`
   - \`User.Read\`
3. Grant admin consent if required

### Step 3: Create Client Secret
1. Go to "Certificates & secrets"
2. Create a new client secret and save the value immediately

### Step 4: Get Refresh Token
1. Use Microsoft's OAuth endpoints to get an authorization code
2. Exchange the code for an access token and refresh token
3. Save the refresh token
</details>

## Using the Integration Tools

Once you have your credentials, you can:

1. Use \`listRawCalendars\` to see available calendars
2. Use \`setupCalendarOAuth\` to integrate a specific calendar
3. Use \`listCalendars\` to verify the integration

Need help with a specific step? Just ask!`;
  },
};

/**
 * Helper function to check if a calendar has the Meeting BaaS integration
 */
export const checkCalendarIntegrationTool: Tool<typeof emptyParams> = {
  name: 'checkCalendarIntegration',
  description: 'Check and diagnose calendar integration status',
  parameters: emptyParams,
  execute: async (_args, context) => {
    const { session, log } = context;
    log.info('Checking calendar integration status');

    try {
      // List calendars
      const calendars = await apiRequest(session, 'get', '/calendars/');

      if (!calendars || calendars.length === 0) {
        return `No calendars integrated. To integrate a calendar:

1. You need Google/Microsoft OAuth credentials:
   - Client ID
   - Client Secret
   - Refresh Token

2. Use the \`oauthGuidance\` tool for detailed steps to obtain these credentials.

3. Use the \`setupCalendarOAuth\` tool to connect your calendar.

Example command:
"Connect my Google Calendar using these OAuth credentials: [client-id], [client-secret], [refresh-token]"`;
      }

      // List some recent events to check functionality
      const calendarId = calendars[0].uuid;
      const events = await apiRequest(
        session,
        'get',
        `/calendar_events/?calendar_id=${calendarId}&status=upcoming`,
      );

      let eventStatus = '';
      if (!events.data || events.data.length === 0) {
        eventStatus = 'No upcoming events found in this calendar.';
      } else {
        const eventCount = events.data.length;
        const scheduledCount = events.data.filter((e: any) => e.bot_param).length;
        eventStatus = `Found ${eventCount} upcoming events, ${scheduledCount} have recording bots scheduled.`;
      }

      return `Calendar integration status: ACTIVE

Found ${calendars.length} integrated calendar(s):
${calendars.map((cal: any) => `- ${cal.name} (${cal.email}) [ID: ${cal.uuid}]`).join('\n')}

${eventStatus}

To schedule recordings for upcoming meetings:
1. Use \`listUpcomingMeetings\` to see available meetings
2. Use \`scheduleRecording\` to set up a recording bot for a meeting

To manage calendar integrations:
- Use \`resyncAllCalendars\` to force a refresh of calendar data
- Use \`deleteCalendar\` to remove a calendar integration`;
    } catch (error) {
      return `Error checking calendar integration: ${error instanceof Error ? error.message : String(error)}

This could indicate:
- API authentication issues
- Missing or expired OAuth credentials
- Network connectivity problems

Try the following:
1. Verify your API key is correct
2. Check if OAuth credentials need to be refreshed
3. Use \`oauthGuidance\` for help setting up OAuth correctly`;
    }
  },
};

/**
 * List events with comprehensive filtering - VERSION WITH DYNAMIC CREDENTIALS
 */
const listEventsWithCredentialsParams = z.object({
  calendarId: z.string().describe('UUID of the calendar to retrieve events from'),
  apiKey: z.string().describe('API key for authentication'),
  status: z
    .enum(['upcoming', 'past', 'all'])
    .optional()
    .describe('Filter events by status (upcoming, past, all)'),
  startDateGte: z
    .string()
    .optional()
    .describe('Filter events with start date greater than or equal to (ISO format)'),
  startDateLte: z
    .string()
    .optional()
    .describe('Filter events with start date less than or equal to (ISO format)'),
  attendeeEmail: z.string().optional().describe('Filter events with specific attendee email'),
  organizerEmail: z.string().optional().describe('Filter events with specific organizer email'),
  updatedAtGte: z
    .string()
    .optional()
    .describe('Filter events updated after specified date (ISO format)'),
  cursor: z.string().optional().describe('Pagination cursor for retrieving more results'),
  limit: z.number().optional().describe('Maximum number of events to return'),
});

export const listEventsWithCredentialsTool: Tool<typeof listEventsWithCredentialsParams> = {
  name: 'listEventsWithCredentials',
  description:
    'List calendar events with comprehensive filtering options using directly provided credentials',
  parameters: listEventsWithCredentialsParams,
  execute: async (args, context) => {
    const { log } = context;

    // Create a session with the provided API key
    const session = { apiKey: args.apiKey };

    log.info('Listing calendar events with provided credentials', {
      calendarId: args.calendarId,
      filters: args,
    });

    // Build the query parameters
    let queryParams = `calendar_id=${args.calendarId}`;
    if (args.status) queryParams += `&status=${args.status}`;
    if (args.startDateGte)
      queryParams += `&start_date_gte=${encodeURIComponent(args.startDateGte)}`;
    if (args.startDateLte)
      queryParams += `&start_date_lte=${encodeURIComponent(args.startDateLte)}`;
    if (args.attendeeEmail)
      queryParams += `&attendee_email=${encodeURIComponent(args.attendeeEmail)}`;
    if (args.organizerEmail)
      queryParams += `&organizer_email=${encodeURIComponent(args.organizerEmail)}`;
    if (args.updatedAtGte)
      queryParams += `&updated_at_gte=${encodeURIComponent(args.updatedAtGte)}`;
    if (args.cursor) queryParams += `&cursor=${encodeURIComponent(args.cursor)}`;
    if (args.limit) queryParams += `&limit=${args.limit}`;

    try {
      const response = await apiRequest(session, 'get', `/calendar_events/?${queryParams}`);

      if (!response.data || response.data.length === 0) {
        return 'No events found matching your criteria.';
      }

      const eventList = response.data
        .map((event: CalendarEvent) => {
          const startTime = new Date(event.start_time).toLocaleString();
          const endTime = new Date(event.end_time).toLocaleString();
          const hasBot =
            event.bot_param && typeof event.bot_param === 'object' && 'uuid' in event.bot_param;
          const meetingStatus = hasBot ? '🟢 Recording scheduled' : '⚪ No recording';

          // Get attendee names
          const attendeeList =
            (event.attendees || []).map((a) => a.name || a.email).join(', ') || 'None listed';

          return (
            `## ${event.name}\n` +
            `**Time**: ${startTime} to ${endTime}\n` +
            `**Status**: ${meetingStatus}\n` +
            `**Event ID**: ${event.uuid}\n` +
            `**Organizer**: ${event.is_organizer ? 'You' : 'Other'}\n` +
            `**Attendees**: ${attendeeList}\n`
          );
        })
        .join('\n\n');

      let result = `Calendar Events:\n\n${eventList}`;

      if (response.next) {
        result += `\n\nMore events available. Use 'cursor: ${response.next}' to see more.`;
      }

      return result;
    } catch (error) {
      return `Error listing events: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
};

/**
 * Schedule a recording with direct credentials
 */
const scheduleRecordingWithCredentialsParams = z.object({
  eventId: z.string().uuid().describe('UUID of the calendar event to record'),
  apiKey: z.string().describe('API key for authentication'),
  botName: z.string().describe('Name to display for the bot in the meeting'),
  botImage: z.string().url().optional().describe("URL to an image for the bot's avatar (optional)"),
  entryMessage: z
    .string()
    .optional()
    .describe('Message the bot will send when joining the meeting (optional)'),
  recordingMode: z
    .enum(['speaker_view', 'gallery_view', 'audio_only'] as const)
    .default('speaker_view')
    .describe("Recording mode: 'speaker_view' (default), 'gallery_view', or 'audio_only'"),
  speechToTextProvider: z
    .enum(['Gladia', 'Runpod', 'Default'] as const)
    .optional()
    .describe('Provider for speech-to-text transcription (optional)'),
  speechToTextApiKey: z
    .string()
    .optional()
    .describe('API key for the speech-to-text provider if required (optional)'),
  extra: z
    .record(z.any())
    .optional()
    .describe('Additional metadata about the meeting (e.g., meetingType, participants)'),
  allOccurrences: z
    .boolean()
    .optional()
    .default(false)
    .describe(
      'For recurring events, schedule recording for all occurrences (true) or just this instance (false)',
    ),
});

export const scheduleRecordingWithCredentialsTool: Tool<
  typeof scheduleRecordingWithCredentialsParams
> = {
  name: 'scheduleRecordingWithCredentials',
  description: 'Schedule a bot to record an upcoming meeting using directly provided credentials',
  parameters: scheduleRecordingWithCredentialsParams,
  execute: async (args, context) => {
    const { log } = context;

    // Create a session with the provided API key
    const session = { apiKey: args.apiKey };

    log.info('Scheduling meeting recording with provided credentials', {
      eventId: args.eventId,
      botName: args.botName,
      recordingMode: args.recordingMode,
      allOccurrences: args.allOccurrences,
    });

    const payload: any = {
      bot_name: args.botName,
      extra: args.extra || {},
    };

    if (args.botImage) payload.bot_image = args.botImage;
    if (args.entryMessage) payload.enter_message = args.entryMessage;
    if (args.recordingMode) payload.recording_mode = args.recordingMode;

    if (args.speechToTextProvider) {
      payload.speech_to_text = {
        provider: args.speechToTextProvider,
      };

      if (args.speechToTextApiKey) {
        payload.speech_to_text.api_key = args.speechToTextApiKey;
      }
    }

    try {
      let url = `/calendar_events/${args.eventId}/bot`;
      if (args.allOccurrences) {
        url += `?all_occurrences=true`;
      }

      const response = await apiRequest(session, 'post', url, payload);

      // Check if we got a successful response with event data
      if (Array.isArray(response) && response.length > 0) {
        const eventCount = response.length;
        const firstEventName = response[0].name;

        if (eventCount === 1) {
          return `Recording has been scheduled successfully for "${firstEventName}".`;
        } else {
          return `Recording has been scheduled successfully for ${eventCount} instances of "${firstEventName}".`;
        }
      }

      return 'Recording has been scheduled successfully.';
    } catch (error) {
      return `Error scheduling recording: ${error instanceof Error ? error.message : String(error)}`;
    }
  },
};

/**
 * Cancel a scheduled recording with direct credentials
 */
const cancelRecordingWithCredentialsParams = z.object({
  eventId: z.string().uuid().describe('UUID of the calendar event to cancel recording for'),
  apiKey: z.string().describe('API key for authentication'),
  allOccurrences: z
    .boolean()
    .optional()
    .default(false)
    .describe(
      'For recurring events, cancel recording for all occurrences (true) or just this instance (false)',
    ),
});

export const cancelRecordingWithCredentialsTool: Tool<typeof cancelRecordingWithCredentialsParams> =
  {
    name: 'cancelRecordingWithCredentials',
    description: 'Cancel a previously scheduled recording using directly provided credentials',
    parameters: cancelRecordingWithCredentialsParams,
    execute: async (args, context) => {
      const { log } = context;

      // Create a session with the provided API key
      const session = { apiKey: args.apiKey };

      log.info('Canceling recording with provided credentials', {
        eventId: args.eventId,
        allOccurrences: args.allOccurrences,
      });

      try {
        let url = `/calendar_events/${args.eventId}/bot`;
        if (args.allOccurrences) {
          url += `?all_occurrences=true`;
        }

        await apiRequest(session, 'delete', url);

        return `Recording has been successfully canceled for event ${args.eventId}${args.allOccurrences ? ' and all its occurrences' : ''}.`;
      } catch (error) {
        return `Error canceling recording: ${error instanceof Error ? error.message : String(error)}`;
      }
    },
  };

```
Page 1/2FirstPrevNextLast