From e167c120ed1908948bade0e94daa8a10487c91f1 Mon Sep 17 00:00:00 2001 From: Vijay Raghunathan Date: Fri, 23 Jan 2026 23:35:03 -0800 Subject: [PATCH 1/3] feat: add OAuth login flow and Claude Code skill - Add browser-based OAuth authentication (no API key needed on first use) - Add vapi_login and vapi_status tools for auth management - Add Claude Code skill with voice assistant prompt engineering guide - Update README with Claude Code setup instructions - Rename package from @vapi-ai/mcp-server to vapi-mcp for simpler npx usage The OAuth flow works like `gh auth login`: 1. User calls a Vapi tool 2. If not authenticated, local HTTP server starts for OAuth callback 3. Browser opens to Vapi dashboard for sign-in 4. After auth, API key is saved locally for future use Co-Authored-By: Claude Opus 4.5 --- README.md | 258 +++++++++++++++++++++++------------------- package.json | 5 +- skill/PROMPT_GUIDE.md | 148 ++++++++++++++++++++++++ skill/SKILL.md | 100 ++++++++++++++++ src/auth.ts | 224 ++++++++++++++++++++++++++++++++++++ src/index.ts | 131 +++++++++++++++++++-- src/tools/utils.ts | 31 +++++ 7 files changed, 771 insertions(+), 126 deletions(-) create mode 100644 skill/PROMPT_GUIDE.md create mode 100644 skill/SKILL.md create mode 100644 src/auth.ts diff --git a/README.md b/README.md index 3460867..337a0fe 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,71 @@ [![smithery badge](https://smithery.ai/badge/@VapiAI/vapi-mcp-server)](https://smithery.ai/server/@VapiAI/vapi-mcp-server) -The Vapi [Model Context Protocol](https://modelcontextprotocol.com/) server allows you to integrate with Vapi APIs through function calling. +Build AI voice assistants and phone agents with [Vapi](https://vapi.ai) using the [Model Context Protocol](https://modelcontextprotocol.com/). Vapi Server MCP server +## Claude Code Setup (Recommended) + +The easiest way to get started. No API key needed - authenticate via browser on first use. + +### 1. Add MCP Server + +Add to your `~/.claude.json`: + +```json +{ + "mcpServers": { + "vapi": { + "command": "npx", + "args": ["-y", "@vapi-ai/mcp-server"] + } + } +} +``` + +### 2. Install Skill (Optional) + +The Vapi skill helps Claude guide you through building voice assistants: + +```bash +mkdir -p ~/.claude/skills/vapi +curl -o ~/.claude/skills/vapi/SKILL.md https://raw.githubusercontent.com/VapiAI/mcp-server/main/skill/SKILL.md +``` + +### 3. Restart Claude Code + +After restarting, use `/vapi` or ask Claude to help build a voice assistant. On first use, you'll be prompted to sign in via browser - no API key copy-paste needed. + +--- + ## Claude Desktop Setup -1. Open `Claude Desktop` and press `CMD + ,` to go to `Settings`. -2. Click on the `Developer` tab. -3. Click on the `Edit Config` button. -4. This will open the `claude_desktop_config.json` file in your file explorer. -5. Get your Vapi API key from the Vapi dashboard (). -6. Add the following to your `claude_desktop_config.json` file. See [here](https://modelcontextprotocol.io/quickstart/user) for more details. -7. Restart the Claude Desktop after editing the config file. +### With OAuth (No API Key) + +```json +{ + "mcpServers": { + "vapi": { + "command": "npx", + "args": ["-y", "@vapi-ai/mcp-server"] + } + } +} +``` + +### With API Key -### Local Configuration +If you prefer to use an API key directly, get one from the [Vapi dashboard](https://dashboard.vapi.ai/org/api-keys): ```json { "mcpServers": { - "vapi-mcp-server": { + "vapi": { "command": "npx", - "args": [ - "-y", - "@vapi-ai/mcp-server" - ], + "args": ["-y", "@vapi-ai/mcp-server"], "env": { "VAPI_TOKEN": "" } @@ -39,10 +77,12 @@ The Vapi [Model Context Protocol](https://modelcontextprotocol.com/) server allo ### Remote Configuration +Connect to Vapi's hosted MCP server: + ```json { "mcpServers": { - "vapi-mcp": { + "vapi": { "command": "npx", "args": [ "mcp-remote", @@ -58,40 +98,39 @@ The Vapi [Model Context Protocol](https://modelcontextprotocol.com/) server allo } ``` -### Example Usage with Claude Desktop +--- -1. Create or import a phone number using the Vapi dashboard (). -2. Create a new assistant using the existing 'Appointment Scheduler' template in the Vapi dashboard (). -3. Make sure to configure Claude Desktop to use the Vapi MCP server and restart the Claude Desktop app. -4. Ask Claude to initiate or schedule a call. See examples below: +## Example Usage -**Example 1:** Request an immediate call +### Create a Voice Assistant -```md -I'd like to speak with my ShopHelper assistant to talk about my recent order. Can you have it call me at +1234567890? +Ask Claude: ``` - -**Example 2:** Schedule a future call - -```md -I need to schedule a call with Mary assistant for next Tuesday at 3:00 PM. My phone number is +1555123456. +I want to build a voice assistant that can schedule appointments ``` -**Example 3:** Make a call with dynamic variables +### Make an Outbound Call -```md -I want to call +1234567890 with my appointment reminder assistant. Use these details: +``` +Call +1234567890 using my appointment reminder assistant with these details: - Customer name: Sarah Johnson - Appointment date: March 25th - Appointment time: 2:30 PM -- Doctor name: Dr. Smith ``` +### Schedule a Future Call + +``` +Schedule a call with my support assistant for next Tuesday at 3:00 PM to +1555123456 +``` + +--- + ## Using Variable Values in Assistant Prompts -The `create_call` action supports passing dynamic variables through `assistantOverrides.variableValues`. These variables can be used in your assistant's prompts using double curly braces: `{{variableName}}`. +The `create_call` action supports passing dynamic variables through `assistantOverrides.variableValues`. Use double curly braces in your assistant's prompts: `{{variableName}}`. -### Example Assistant Prompt with Variables +### Example Prompt with Variables ``` Hello {{customerName}}, this is a reminder about your appointment on {{appointmentDate}} at {{appointmentTime}} with {{doctorName}}. @@ -99,7 +138,7 @@ Hello {{customerName}}, this is a reminder about your appointment on {{appointme ### Default Variables -The following variables are automatically available (no need to pass in variableValues): +These are automatically available (no need to pass): - `{{now}}` - Current date and time (UTC) - `{{date}}` - Current date (UTC) @@ -109,29 +148,69 @@ The following variables are automatically available (no need to pass in variable - `{{year}}` - Current year (UTC) - `{{customer.number}}` - Customer's phone number -For more details on default variables and advanced date/time formatting, see the [official Vapi documentation](https://docs.vapi.ai/assistants/dynamic-variables#default-variables). +See [Vapi documentation](https://docs.vapi.ai/assistants/dynamic-variables#default-variables) for advanced date/time formatting. -## Remote MCP +--- -To connect to Vapi's MCP server remotely: +## Remote MCP Server -### Streamable HTTP (Recommended) +Connect to Vapi's hosted MCP server from any MCP client: -The default and recommended way to connect is via Streamable HTTP Transport: +### Streamable HTTP (Recommended) -- Connect to `https://mcp.vapi.ai/mcp` from any MCP client using Streamable HTTP Transport -- Include your Vapi API key as a bearer token in the request headers -- Example header: `Authorization: Bearer your_vapi_api_key_here` +- URL: `https://mcp.vapi.ai/mcp` +- Header: `Authorization: Bearer your_vapi_api_key_here` ### SSE (Deprecated) -Server-Sent Events (SSE) Transport is still supported but deprecated: - -- Connect to `https://mcp.vapi.ai/sse` from any MCP client using SSE Transport -- Include your Vapi API key as a bearer token in the request headers -- Example header: `Authorization: Bearer your_vapi_api_key_here` - -This connection allows you to access Vapi's functionality remotely without running a local server. +- URL: `https://mcp.vapi.ai/sse` +- Header: `Authorization: Bearer your_vapi_api_key_here` + +--- + +## Available Tools + +### Assistants +| Tool | Description | +|------|-------------| +| `vapi_list_assistants` | List all assistants | +| `vapi_get_assistant` | Get assistant by ID | +| `vapi_create_assistant` | Create new assistant | +| `vapi_update_assistant` | Update assistant | +| `vapi_delete_assistant` | Delete assistant | + +### Calls +| Tool | Description | +|------|-------------| +| `vapi_list_calls` | List call history | +| `vapi_get_call` | Get call details | +| `vapi_create_call` | Start outbound call (immediate or scheduled) | + +### Phone Numbers +| Tool | Description | +|------|-------------| +| `vapi_list_phone_numbers` | List phone numbers | +| `vapi_get_phone_number` | Get phone number details | +| `vapi_buy_phone_number` | Purchase new number | +| `vapi_update_phone_number` | Update number settings | +| `vapi_delete_phone_number` | Release number | + +### Tools (Function Calling) +| Tool | Description | +|------|-------------| +| `vapi_list_tools` | List custom tools | +| `vapi_get_tool` | Get tool details | +| `vapi_create_tool` | Create tool for API integration | +| `vapi_update_tool` | Update tool | +| `vapi_delete_tool` | Delete tool | + +### Authentication +| Tool | Description | +|------|-------------| +| `vapi_login` | Start OAuth flow | +| `vapi_status` | Check auth status | + +--- ## Development @@ -139,101 +218,48 @@ This connection allows you to access Vapi's functionality remotely without runni # Install dependencies npm install -# Build the server +# Build npm run build -# Use inspector to test the server +# Test with MCP inspector npm run inspector ``` -Update your `claude_desktop_config.json` to use the local server. +### Local Development Config ```json { "mcpServers": { "vapi-local": { "command": "node", - "args": [ - "/dist/index.js" - ], + "args": ["/dist/index.js"], "env": { "VAPI_TOKEN": "" } - }, + } } } ``` ### Testing -The project has two types of tests: - -#### Unit Tests - -Unit tests use mocks to test the MCP server without making actual API calls to Vapi. - ```bash -# Run unit tests +# Unit tests (mocked) npm run test:unit -``` - -#### End-to-End Tests -E2E tests run the full MCP server with actual API calls to Vapi. - -```bash -# Set your Vapi API token +# E2E tests (requires VAPI_TOKEN) export VAPI_TOKEN=your_token_here - -# Run E2E tests npm run test:e2e -``` - -Note: E2E tests require a valid Vapi API token to be set in the environment. - -#### Running All Tests - -To run all tests at once: -```bash +# All tests npm test ``` +--- + ## References -- [VAPI Remote MCP Server](https://mcp.vapi.ai/) -- [VAPI MCP Tool](https://docs.vapi.ai/tools/mcp) -- [VAPI MCP Server SDK](https://docs.vapi.ai/sdk/mcp-server) +- [Vapi Documentation](https://docs.vapi.ai) +- [Vapi Dashboard](https://dashboard.vapi.ai) +- [Vapi Remote MCP Server](https://mcp.vapi.ai/) - [Model Context Protocol](https://modelcontextprotocol.com/) -- [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) - -## Supported Actions - -The Vapi MCP Server provides the following tools for integration: - -### Assistant Tools - -- `list_assistants`: Lists all Vapi assistants -- `create_assistant`: Creates a new Vapi assistant -- `update_assistant`: Updates an existing Vapi assistant -- `get_assistant`: Gets a Vapi assistant by ID - -### Call Tools - -- `list_calls`: Lists all Vapi calls -- `create_call`: Creates an outbound call with support for: - - Immediate or scheduled calls - - Dynamic variable values through `assistantOverrides` -- `get_call`: Gets details of a specific call - -> **Note:** The `create_call` action supports scheduling calls for immediate execution or for a future time. You can also pass dynamic variables using `assistantOverrides.variableValues` to personalize assistant messages. - -### Phone Number Tools - -- `list_phone_numbers`: Lists all Vapi phone numbers -- `get_phone_number`: Gets details of a specific phone number - -### Vapi Tools - -- `list_tools`: Lists all Vapi tools -- `get_tool`: Gets details of a specific tool diff --git a/package.json b/package.json index cc96a24..1324349 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@vapi-ai/mcp-server", - "description": "Vapi MCP Server", - "version": "0.0.9", + "description": "Vapi MCP Server - Build AI voice assistants with Claude", + "version": "0.0.10", "main": "dist/index.js", "types": "dist/index.d.ts", "type": "module", @@ -30,6 +30,7 @@ "tool-calling" ], "bin": { + "vapi-mcp": "dist/index.js", "@vapi-ai/mcp-server": "dist/index.js" }, "scripts": { diff --git a/skill/PROMPT_GUIDE.md b/skill/PROMPT_GUIDE.md new file mode 100644 index 0000000..fe27bbb --- /dev/null +++ b/skill/PROMPT_GUIDE.md @@ -0,0 +1,148 @@ +# Voice Assistant Prompt Engineering Guide + +Good voice assistant prompts are different from text-based prompts. This guide covers best practices for crafting effective prompts for Vapi voice assistants. + +## Prompt Structure + +### 1. Identity & Role +Define who the assistant is clearly and concisely. + +``` +You are Amy, a friendly and professional receptionist for VAPI Health Clinic. +``` + +### 2. Core Responsibilities +List the primary functions the assistant should perform. + +``` +Your responsibilities: +- Answer patient questions about the clinic +- Book, reschedule, and cancel appointments +- Transfer calls to appropriate staff when needed +``` + +### 3. Constraints & Boundaries +Set clear limits on what the assistant should and shouldn't do. + +``` +Important constraints: +- Operating hours are 9 AM to 5 PM daily +- Never provide medical advice - always defer to doctors +- If asked about emergencies, direct them to call 911 +``` + +### 4. Conversational Style +Provide voice-specific guidance for natural conversations. + +``` +Conversation style: +- Be warm and professional +- Ask ONE question at a time, then wait for response +- Keep responses concise (1-2 sentences when possible) +- Use natural speech patterns, not robotic responses +``` + +### 5. Tool Usage +Explain when and how to use available tools. + +``` +When booking appointments: +1. First check availability using the calendar tool +2. Confirm the date and time with the patient +3. Book only after verbal confirmation +``` + +## Voice-Specific Best Practices + +- **Keep it concise**: Phone conversations need shorter responses than text +- **One question at a time**: Don't overwhelm with multiple questions +- **Confirm understanding**: Repeat back important details (dates, names, numbers) +- **Handle interruptions**: Users will interrupt - design for it +- **Graceful fallbacks**: Always have a path to human handoff + +## Example Prompts + +### Healthcare Receptionist + +``` +You are Amy, a warm and professional receptionist for VAPI Health Clinic. + +Your role: +- Answer patient FAQs about clinic hours, location, and services +- Help patients book, reschedule, or cancel appointments +- Transfer calls to nurses or doctors when medically necessary + +Guidelines: +- Clinic hours: 9 AM - 5 PM, Monday through Friday +- Never provide medical advice - say "I'd recommend speaking with one of our nurses about that" +- For emergencies, immediately say "Please hang up and call 911" +- Ask one question at a time and wait for the response +- Keep responses brief and conversational + +When booking appointments: +1. Ask what type of appointment they need +2. Check available slots using the calendar tool +3. Offer 2-3 options and let them choose +4. Confirm the final booking by repeating the details +``` + +### Customer Support Agent + +``` +You are Alex, a helpful support agent for TechCorp. + +Your role: +- Help customers troubleshoot product issues +- Process returns and exchanges +- Answer questions about orders and shipping + +Guidelines: +- Always verify the customer's identity first (order number or email) +- Be patient and empathetic - customers may be frustrated +- If you can't resolve an issue, offer to transfer to a specialist +- Keep technical explanations simple and jargon-free + +Escalation triggers - transfer to human agent if: +- Customer explicitly asks for a human +- Issue requires account changes you can't make +- Customer is upset after 2 resolution attempts +``` + +### Outbound Appointment Reminder + +``` +You are calling on behalf of Dr. Smith's Dental Office to remind {{customerName}} about their upcoming appointment. + +Your script: +1. Introduce yourself: "Hi, this is an automated call from Dr. Smith's Dental Office" +2. State the purpose: "I'm calling to remind you about your appointment on {{appointmentDate}} at {{appointmentTime}}" +3. Confirm: "Will you be able to make it?" +4. If yes: "Great! We'll see you then. Goodbye!" +5. If no: "I understand. Would you like me to transfer you to reschedule?" + +Guidelines: +- Be brief and respectful of their time +- If you reach voicemail, leave a short message with callback number +- Don't call back if they decline or seem annoyed +``` + +## Dynamic Variables + +Use double curly braces for dynamic content: `{{variableName}}` + +### Default Variables (always available) +- `{{now}}` - Current date and time (UTC) +- `{{date}}` - Current date (UTC) +- `{{time}}` - Current time (UTC) +- `{{customer.number}}` - Customer's phone number + +### Custom Variables +Pass custom variables via `assistantOverrides.variableValues` when creating calls: +- `{{customerName}}` +- `{{appointmentDate}}` +- `{{appointmentTime}}` +- `{{orderNumber}}` + +## More Examples + +For complete, production-ready examples with tool integrations, see: https://github.com/VapiAI/examples diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..25d7ada --- /dev/null +++ b/skill/SKILL.md @@ -0,0 +1,100 @@ +--- +name: vapi +description: Build AI voice assistants and phone agents with Vapi. Use this skill when users want to create voice agents, phone bots, IVR systems, outbound calling campaigns, or any voice-based AI application. +--- + +# Vapi - AI Voice Assistant Builder + +Build AI-powered voice assistants, phone agents, and conversational AI applications with Vapi. + +## When This Skill is Activated + +When a user wants to build a voice assistant or phone agent, follow these steps: + +### Step 1: Check if Vapi MCP is Installed + +First, check if the Vapi MCP server is available by looking for `vapi_` tools. If not available: + +1. Add the Vapi MCP server to the `mcpServers` section: + +```json +{ + "mcpServers": { + "vapi": { + "command": "npx", + "args": ["-y", "@vapi-ai/mcp-server"] + } + } +} +``` + +2. Tell the user to restart Claude Code for the changes to take effect +3. After restart, continue with Step 2 + +### Step 2: Authenticate with Vapi + +If the user hasn't authenticated yet (tools return auth errors): + +1. Call `vapi_login` to start the OAuth flow +2. Tell the user to open the provided URL and sign in +3. Once authenticated, proceed with their request + +### Step 3: Build the Voice Assistant + +Before creating an assistant, fetch the latest prompt engineering guidelines from the [Prompt Guide](https://raw.githubusercontent.com/VapiAI/mcp-server/main/skill/PROMPT_GUIDE.md). + +Use these guidelines to craft effective voice assistant prompts based on what the user wants to build. + +## Available Tools + +### Authentication +- `vapi_login` - Start OAuth authentication flow +- `vapi_status` - Check if authenticated + +### Assistants +- `vapi_list_assistants` - List all assistants +- `vapi_get_assistant` - Get assistant details +- `vapi_create_assistant` - Create new assistant +- `vapi_update_assistant` - Update assistant +- `vapi_delete_assistant` - Delete assistant + +### Calls +- `vapi_list_calls` - List call history +- `vapi_get_call` - Get call details +- `vapi_create_call` - Start outbound call + +### Phone Numbers +- `vapi_list_phone_numbers` - List phone numbers +- `vapi_buy_phone_number` - Purchase new number +- `vapi_update_phone_number` - Update number settings +- `vapi_delete_phone_number` - Release number + +### Tools (Function Calling) +- `vapi_list_tools` - List custom tools +- `vapi_create_tool` - Create tool for API integration +- `vapi_update_tool` - Update tool +- `vapi_delete_tool` - Delete tool + +## Workflow Examples + +**User:** "I want to build a voice assistant that can schedule appointments" + +**Claude should:** +1. Check for Vapi MCP -> install if needed +2. Authenticate if needed +3. Fetch the prompt guide for best practices +4. Ask about their business to understand context +5. Create an assistant with a scheduling-focused prompt +6. Offer to set up a phone number +7. Help create calendar integration tools if needed + +**User:** "Make me a phone bot that answers questions about my business" + +**Claude should:** +1. Ensure Vapi MCP is installed and authenticated +2. Fetch the prompt guide for best practices +3. Ask about the business: name, services, hours, common questions +4. Craft a system prompt following the guidelines +5. Create the assistant +6. Help provision or connect a phone number +7. Offer to test with a sample call diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..c63ec1b --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,224 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import * as http from 'http'; +import * as crypto from 'crypto'; + +const CONFIG_DIR = path.join(os.homedir(), '.vapi'); +const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); + +// Vapi Dashboard URL for OAuth +const VAPI_DASHBOARD_URL = process.env.VAPI_DASHBOARD_URL || 'https://dashboard.vapi.ai'; + +interface VapiConfig { + apiKey?: string; + email?: string; + orgId?: string; +} + +// In-memory state +let cachedConfig: VapiConfig | null = null; +let authInProgress = false; +let authUrl: string | null = null; +let authServer: http.Server | null = null; + +/** + * Load stored Vapi configuration from ~/.vapi/config.json + */ +export function loadConfig(): VapiConfig { + if (cachedConfig) { + return cachedConfig; + } + try { + if (fs.existsSync(CONFIG_FILE)) { + const content = fs.readFileSync(CONFIG_FILE, 'utf-8'); + cachedConfig = JSON.parse(content); + return cachedConfig!; + } + } catch (error) { + // Ignore errors, return empty config + } + return {}; +} + +/** + * Save Vapi configuration to ~/.vapi/config.json + */ +export function saveConfig(config: VapiConfig): void { + try { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); + cachedConfig = config; + } catch (error) { + console.error('Failed to save config:', error); + } +} + +/** + * Check if we have a valid API token + */ +export function hasValidToken(): boolean { + // Check environment variable first + if (process.env.VAPI_TOKEN) { + return true; + } + // Check config file + const config = loadConfig(); + return !!config.apiKey; +} + +/** + * Get the API token (from env or config) + */ +export function getToken(): string | null { + if (process.env.VAPI_TOKEN) { + return process.env.VAPI_TOKEN; + } + const config = loadConfig(); + return config.apiKey || null; +} + +/** + * Check if auth is currently in progress + */ +export function isAuthInProgress(): boolean { + return authInProgress; +} + +/** + * Get the current auth URL (if auth is in progress) + */ +export function getAuthUrl(): string | null { + return authUrl; +} + +/** + * Start the OAuth flow - returns the auth URL + */ +export function startAuthFlow(): Promise { + return new Promise((resolve, reject) => { + if (authInProgress) { + if (authUrl) { + resolve(authUrl); + } else { + reject(new Error('Auth in progress but no URL available')); + } + return; + } + + // Generate random state for security + const state = crypto.randomUUID(); + authInProgress = true; + + // Start local server to receive callback + authServer = http.createServer(async (req, res) => { + const url = new URL(req.url || '/', `http://localhost`); + + if (url.pathname === '/callback') { + const returnedState = url.searchParams.get('state'); + const apiKey = url.searchParams.get('api_key'); + const orgId = url.searchParams.get('org_id'); + const email = url.searchParams.get('email'); + const error = url.searchParams.get('error'); + + // Verify state matches + if (returnedState !== state) { + res.writeHead(400, { 'Content-Type': 'text/html' }); + res.end(errorPage('Security Error', 'State mismatch. Please try again.')); + return; + } + + if (error) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(errorPage('Authentication Failed', error)); + cleanupAuth(); + return; + } + + if (apiKey) { + // Save to config + saveConfig({ apiKey, orgId: orgId || undefined, email: email || undefined }); + + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(successPage()); + cleanupAuth(); + return; + } + + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Missing API key'); + return; + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not found'); + }); + + // Find available port and start server + authServer.listen(0, '127.0.0.1', () => { + const address = authServer!.address(); + if (!address || typeof address === 'string') { + authInProgress = false; + reject(new Error('Failed to start local server')); + return; + } + + const port = (address as any).port; + const redirectUri = `http://localhost:${port}/callback`; + authUrl = `${VAPI_DASHBOARD_URL}/auth/cli?state=${state}&redirect_uri=${encodeURIComponent(redirectUri)}`; + + resolve(authUrl); + + // Timeout after 10 minutes + setTimeout(() => { + if (authInProgress) { + cleanupAuth(); + } + }, 10 * 60 * 1000); + }); + + authServer.on('error', (err) => { + authInProgress = false; + reject(err); + }); + }); +} + +function cleanupAuth() { + authInProgress = false; + authUrl = null; + if (authServer) { + authServer.close(); + authServer = null; + } +} + +function successPage(): string { + return ` + + +
+
+

Connected to Vapi!

+

You can close this window and return to Claude.

+
+ + + `; +} + +function errorPage(title: string, message: string): string { + return ` + + +
+
+

${title}

+

${message}

+
+ + + `; +} diff --git a/src/index.ts b/src/index.ts index 5e7ee22..9c58a3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,27 +2,140 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { VapiClient } from '@vapi-ai/server-sdk'; +import { hasValidToken, getToken, startAuthFlow, isAuthInProgress, getAuthUrl } from './auth.js'; import { registerAllTools } from './tools/index.js'; -import { createVapiClient } from './client.js'; import dotenv from 'dotenv'; dotenv.config(); -function createMcpServer() { - const vapiToken = process.env.VAPI_TOKEN; - if (!vapiToken) { - throw new Error('VAPI_TOKEN environment variable is required'); - } +// Lazy-initialized Vapi client +let vapiClient: VapiClient | null = null; - const vapiClient = createVapiClient(vapiToken); +function getVapiClient(): VapiClient { + const token = getToken(); + if (!token) { + throw new Error('Not authenticated'); + } + // Reset client if token changed + if (!vapiClient) { + vapiClient = new VapiClient({ token }); + } + return vapiClient; +} +function createMcpServer() { const mcpServer = new McpServer({ name: 'Vapi MCP', version: '0.1.0', capabilities: [], }); - registerAllTools(mcpServer, vapiClient); + // Register the login tool - always available + mcpServer.tool( + 'vapi_login', + 'Authenticate with Vapi. Call this first if other tools return authentication errors.', + {}, + async () => { + // Check if already authenticated + if (hasValidToken()) { + return { + content: [ + { + type: 'text' as const, + text: 'Already authenticated with Vapi! You can now use other Vapi tools.', + }, + ], + }; + } + + // Check if auth is already in progress + if (isAuthInProgress()) { + const url = getAuthUrl(); + return { + content: [ + { + type: 'text' as const, + text: `Authentication in progress. Please complete sign-in at:\n\n${url}\n\nAfter signing in, try your request again.`, + }, + ], + }; + } + + // Start auth flow + try { + const authUrl = await startAuthFlow(); + return { + content: [ + { + type: 'text' as const, + text: `Please sign in to Vapi by opening this URL:\n\n${authUrl}\n\nAfter signing in, try your request again.`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text' as const, + text: `Failed to start authentication: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Register status tool + mcpServer.tool( + 'vapi_status', + 'Check Vapi authentication status', + {}, + async () => { + if (hasValidToken()) { + return { + content: [ + { + type: 'text' as const, + text: 'Authenticated with Vapi and ready to use.', + }, + ], + }; + } + + if (isAuthInProgress()) { + const url = getAuthUrl(); + return { + content: [ + { + type: 'text' as const, + text: `Authentication in progress. Please complete sign-in at:\n\n${url}`, + }, + ], + }; + } + + return { + content: [ + { + type: 'text' as const, + text: 'Not authenticated. Use the vapi_login tool to sign in.', + }, + ], + }; + } + ); + + // Register all Vapi tools - they will check auth via createToolHandler + // We use a proxy that creates the client lazily + const clientProxy = new Proxy({} as VapiClient, { + get(_, prop) { + return getVapiClient()[prop as keyof VapiClient]; + }, + }); + + registerAllTools(mcpServer, clientProxy); return mcpServer; } @@ -36,6 +149,7 @@ async function main() { setupShutdownHandler(mcpServer); } catch (err) { + console.error('Failed to start MCP server:', err); process.exit(1); } } @@ -52,6 +166,7 @@ function setupShutdownHandler(mcpServer: McpServer) { } main().catch((err) => { + console.error('Fatal error:', err); process.exit(1); }); diff --git a/src/tools/utils.ts b/src/tools/utils.ts index dc4c73a..136e033 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -1,7 +1,9 @@ import { z } from 'zod'; +import { hasValidToken, startAuthFlow, isAuthInProgress, getAuthUrl } from '../auth.js'; export type ToolResponse = { content: Array<{ type: 'text'; text: string }>; + isError?: boolean; }; export function createSuccessResponse(data: any): ToolResponse { @@ -27,10 +29,39 @@ export function createErrorResponse(error: any): ToolResponse { }; } +export function createAuthRequiredResponse(url: string): ToolResponse { + return { + content: [ + { + type: 'text' as const, + text: `Authentication required. Please sign in at:\n\n${url}\n\nAfter signing in, try your request again.`, + }, + ], + isError: true, + }; +} + export function createToolHandler( handler: (params: T) => Promise ): (params: T) => Promise { return async (params: T) => { + // Check auth first + if (!hasValidToken()) { + // Start auth if not already in progress + if (!isAuthInProgress()) { + try { + await startAuthFlow(); + } catch (error) { + // Ignore - we'll show the auth URL below + } + } + const url = getAuthUrl(); + if (url) { + return createAuthRequiredResponse(url); + } + return createErrorResponse('Authentication required. Please use vapi_login tool first.'); + } + try { const result = await handler(params); return createSuccessResponse(result); From 6c6b3fe99572bbbbab6d1bd54a0ea0504916d6ad Mon Sep 17 00:00:00 2001 From: Vijay Raghunathan Date: Tue, 27 Jan 2026 16:29:29 -0800 Subject: [PATCH 2/3] fix: keep original bin name for mcp server Remove the added "vapi-mcp" bin alias and retain only the original "@vapi-ai/mcp-server" name. Co-Authored-By: Claude Opus 4.5 --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 1324349..b447975 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "tool-calling" ], "bin": { - "vapi-mcp": "dist/index.js", "@vapi-ai/mcp-server": "dist/index.js" }, "scripts": { From 940252a4d50e3f91bb614383b5a6dbf09b79b17b Mon Sep 17 00:00:00 2001 From: Vijay Raghunathan Date: Tue, 27 Jan 2026 16:54:24 -0800 Subject: [PATCH 3/3] refactor: rename vapi_status tool to vapi_auth_status More explicit naming for the authentication status check tool. Co-Authored-By: Claude Opus 4.5 --- README.md | 2 +- skill/SKILL.md | 2 +- src/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 337a0fe..b50a9a4 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ Connect to Vapi's hosted MCP server from any MCP client: | Tool | Description | |------|-------------| | `vapi_login` | Start OAuth flow | -| `vapi_status` | Check auth status | +| `vapi_auth_status` | Check auth status | --- diff --git a/skill/SKILL.md b/skill/SKILL.md index 25d7ada..2596b27 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -49,7 +49,7 @@ Use these guidelines to craft effective voice assistant prompts based on what th ### Authentication - `vapi_login` - Start OAuth authentication flow -- `vapi_status` - Check if authenticated +- `vapi_auth_status` - Check if authenticated ### Assistants - `vapi_list_assistants` - List all assistants diff --git a/src/index.ts b/src/index.ts index 9c58a3d..2452c19 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,7 +89,7 @@ function createMcpServer() { // Register status tool mcpServer.tool( - 'vapi_status', + 'vapi_auth_status', 'Check Vapi authentication status', {}, async () => {