From ccc8f18aa83a9651adc7e22d3999206e2682a917 Mon Sep 17 00:00:00 2001 From: Amanda Martin <290842976+amanda-vapi@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:35:59 -0700 Subject: [PATCH 1/3] brings main in line with the currently deployed production version --- README.md | 31 ++---- package.json | 2 +- skill/SKILL.md | 18 +--- src/auth.ts | 257 --------------------------------------------- src/index.ts | 116 ++------------------ src/tools/utils.ts | 31 ------ 6 files changed, 20 insertions(+), 435 deletions(-) delete mode 100644 src/auth.ts diff --git a/README.md b/README.md index 9c5a979..cc58504 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ Build AI voice assistants and phone agents with [Vapi](https://vapi.ai) using th -## Claude Code Setup (Recommended) +## Claude Code Setup -The easiest way to get started. No API key needed - authenticate via browser on first use. +The MCP server requires a Vapi API key. Get one from the [Vapi dashboard](https://dashboard.vapi.ai/org/api-keys). ### 1. Add MCP Server ```bash -claude mcp add vapi -- npx -y @vapi-ai/mcp-server +claude mcp add -e VAPI_TOKEN=your_vapi_token vapi -- npx -y @vapi-ai/mcp-server ``` ### 2. Install Skill (Optional) @@ -29,28 +29,15 @@ curl -o ~/.claude/skills/vapi/SKILL.md https://raw.githubusercontent.com/VapiAI/ ### 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. +After restarting, use `/vapi` or ask Claude to help build a voice assistant. --- ## Claude Desktop Setup -### With OAuth (No API Key) +### Local Configuration -```json -{ - "mcpServers": { - "vapi": { - "command": "npx", - "args": ["-y", "@vapi-ai/mcp-server"] - } - } -} -``` - -### With API Key - -If you prefer to use an API key directly, get one from the [Vapi dashboard](https://dashboard.vapi.ai/org/api-keys): +Get an API key from the [Vapi dashboard](https://dashboard.vapi.ai/org/api-keys): ```json { @@ -195,12 +182,6 @@ Connect to Vapi's hosted MCP server from any MCP client: | `vapi_update_tool` | Update tool | | `vapi_delete_tool` | Delete tool | -### Authentication -| Tool | Description | -|------|-------------| -| `vapi_login` | Start OAuth flow | -| `vapi_logout` | Log out and clear credentials | - --- ## Development diff --git a/package.json b/package.json index b447975..15a45ba 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@vapi-ai/mcp-server", "description": "Vapi MCP Server - Build AI voice assistants with Claude", - "version": "0.0.10", + "version": "0.0.9", "main": "dist/index.js", "types": "dist/index.d.ts", "type": "module", diff --git a/skill/SKILL.md b/skill/SKILL.md index 431ceaa..5d7cd50 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -16,18 +16,14 @@ When a user wants to build a voice assistant or phone agent, follow these steps: First, check if the Vapi MCP server is available by looking for `vapi_` tools. If not available, tell the user to run: ```bash -claude mcp add vapi -- npx -y @vapi-ai/mcp-server +claude mcp add -e VAPI_TOKEN=your_vapi_token vapi -- npx -y @vapi-ai/mcp-server ``` Then restart Claude Code and continue with Step 2. -### Step 2: Authenticate with Vapi +### Step 2: Confirm Vapi Credentials -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 +If the tools return authentication errors, tell the user to configure `VAPI_TOKEN` with a Vapi API key in their MCP server environment and restart Claude Code. ### Step 3: Build the Voice Assistant @@ -37,10 +33,6 @@ Use these guidelines to craft effective voice assistant prompts based on what th ## Available Tools -### Authentication -- `vapi_login` - Start OAuth authentication flow -- `vapi_logout` - Log out and clear stored credentials - ### Assistants - `vapi_list_assistants` - List all assistants - `vapi_get_assistant` - Get assistant details @@ -71,7 +63,7 @@ Use these guidelines to craft effective voice assistant prompts based on what th **Claude should:** 1. Check for Vapi MCP -> install if needed -2. Authenticate if needed +2. Confirm `VAPI_TOKEN` is configured if the tools return authentication errors 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 @@ -81,7 +73,7 @@ Use these guidelines to craft effective voice assistant prompts based on what th **User:** "Make me a phone bot that answers questions about my business" **Claude should:** -1. Ensure Vapi MCP is installed and authenticated +1. Ensure Vapi MCP is installed and configured with `VAPI_TOKEN` 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 diff --git a/src/auth.ts b/src/auth.ts deleted file mode 100644 index b9a1e5d..0000000 --- a/src/auth.ts +++ /dev/null @@ -1,257 +0,0 @@ -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'; -import { spawn } from 'child_process'; - -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); - } -} - -/** - * Clear stored Vapi configuration and reset in-memory state - */ -export function clearConfig(): void { - try { - if (fs.existsSync(CONFIG_FILE)) { - fs.unlinkSync(CONFIG_FILE); - } - } catch (error) { - // Ignore errors - } - cachedConfig = null; -} - -/** - * 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)}`; - - openBrowser(authUrl); - resolve(authUrl); - - // Timeout after 10 minutes - setTimeout(() => { - if (authInProgress) { - cleanupAuth(); - } - }, 10 * 60 * 1000); - }); - - authServer.on('error', (err) => { - authInProgress = false; - reject(err); - }); - }); -} - -function openBrowser(url: string) { - try { - const cmd = process.platform === 'darwin' - ? 'open' - : process.platform === 'win32' - ? 'start' - : 'xdg-open'; - const child = spawn(cmd, [url], { - detached: true, - stdio: 'ignore', - }); - child.unref(); - } catch { - // Ignore — URL is still returned in the response as fallback - } -} - -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 1c03b67..7080776 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,127 +2,27 @@ 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, clearConfig } from './auth.js'; import { registerAllTools } from './tools/index.js'; +import { createVapiClient } from './client.js'; import dotenv from 'dotenv'; dotenv.config(); -// Lazy-initialized Vapi client -let vapiClient: VapiClient | null = null; - -function getVapiClient(): VapiClient { - const token = getToken(); - if (!token) { - throw new Error('Not authenticated'); - } - // Reset client if token changed - if (!vapiClient) { - vapiClient = new VapiClient({ token }); +function createMcpServer() { + const vapiToken = process.env.VAPI_TOKEN; + if (!vapiToken) { + throw new Error('VAPI_TOKEN environment variable is required'); } - return vapiClient; -} -function createMcpServer() { + const vapiClient = createVapiClient(vapiToken); + const mcpServer = new McpServer({ name: 'Vapi MCP', version: '0.1.0', capabilities: [], }); - // 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 we have a token and validate it - if (hasValidToken()) { - try { - const client = getVapiClient(); - await client.assistants.list({ limit: 1 }); - return { - content: [ - { - type: 'text' as const, - text: 'Already authenticated with Vapi! You can now use other Vapi tools.', - }, - ], - }; - } catch { - // Token is stale — clear it and restart auth - clearConfig(); - vapiClient = null; - } - } - - // 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:\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:\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 logout tool - mcpServer.tool( - 'vapi_logout', - 'Log out of Vapi and clear stored credentials. Use this if your auth token is stale or you want to switch accounts.', - {}, - async () => { - clearConfig(); - vapiClient = null; - return { - content: [ - { - type: 'text' as const, - text: 'Logged out of Vapi. Use vapi_login to sign in again.', - }, - ], - }; - } - ); - - // 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); + registerAllTools(mcpServer, vapiClient); return mcpServer; } diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 07522e8..dc4c73a 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -1,9 +1,7 @@ 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 { @@ -29,39 +27,10 @@ export function createErrorResponse(error: any): ToolResponse { }; } -export function createAuthRequiredResponse(url: string): ToolResponse { - return { - content: [ - { - type: 'text' as const, - text: `Authentication required. Please sign in:\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 788d9ed595bd8547d60f3919b9173678596e5fce Mon Sep 17 00:00:00 2001 From: Amanda Martin <290842976+amanda-vapi@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:37:30 -0700 Subject: [PATCH 2/3] fix mock test path --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15a45ba..091ec9d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "dev:shttp-example": "tsx examples/shttp-client.ts", "inspector": "mcp-inspector", "test": "NODE_OPTIONS=--experimental-vm-modules jest", - "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest src/tests/mcp-server-mock.test.ts", + "test:unit": "VAPI_TOKEN=test-mock-token NODE_OPTIONS=--experimental-vm-modules jest src/__tests__/mcp-server-mock.test.ts", "test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest src/tests/mcp-server-e2e.test.ts" }, "files": [ From 6ff6fff84884e49bc591a5b73780097c546e951c Mon Sep 17 00:00:00 2001 From: Amanda Martin <290842976+amanda-vapi@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:37:29 -0700 Subject: [PATCH 3/3] fix e2e with __tests__ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 091ec9d..3183281 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "inspector": "mcp-inspector", "test": "NODE_OPTIONS=--experimental-vm-modules jest", "test:unit": "VAPI_TOKEN=test-mock-token NODE_OPTIONS=--experimental-vm-modules jest src/__tests__/mcp-server-mock.test.ts", - "test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest src/tests/mcp-server-e2e.test.ts" + "test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest src/__tests__/mcp-server-e2e.test.ts" }, "files": [ "dist",
You can close this window and return to Claude.
${message}