├── .env.example ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── Dockerfile ├── LICENSE ├── README.md ├── examples ├── sse-client.ts └── stdio-client.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── __tests__ │ ├── mcp-server-e2e.test.ts │ ├── mcp-server-mock.test.ts │ └── mocks │ │ └── vapi-client.mock.ts ├── client.ts ├── index.ts ├── schemas │ └── index.ts ├── tools │ ├── assistant.ts │ ├── call.ts │ ├── index.ts │ ├── phone-number.ts │ ├── tool.ts │ └── utils.ts ├── transformers │ └── index.ts └── utils │ └── response.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Vapi Configuration 2 | VAPI_TOKEN= 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push] 4 | 5 | jobs: 6 | compile: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout repo 11 | uses: actions/checkout@v3 12 | 13 | - name: Set up node 14 | uses: actions/setup-node@v3 15 | 16 | - name: Compile 17 | run: npm install && npm run build 18 | 19 | test: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout repo 24 | uses: actions/checkout@v3 25 | 26 | - name: Set up node 27 | uses: actions/setup-node@v3 28 | 29 | - name: Compile 30 | run: npm install 31 | 32 | # - name: Test 33 | # run: npm run test 34 | 35 | publish: 36 | needs: [ compile ] 37 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout repo 41 | uses: actions/checkout@v3 42 | - name: Set up node 43 | uses: actions/setup-node@v3 44 | - name: Install dependencies 45 | run: npm install 46 | - name: Build 47 | run: npm run build 48 | 49 | - name: Publish to npm 50 | run: | 51 | npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} 52 | if [[ ${GITHUB_REF} == *alpha* ]]; then 53 | npm publish --access public --tag alpha 54 | elif [[ ${GITHUB_REF} == *beta* ]]; then 55 | npm publish --access public --tag beta 56 | else 57 | npm publish --access public 58 | fi 59 | env: 60 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build output 8 | dist/ 9 | build/ 10 | 11 | # Environment variables 12 | .env 13 | .env.local 14 | .env.*.local 15 | 16 | # IDE and editor files 17 | .idea/ 18 | .vscode/ 19 | *.swp 20 | *.swo 21 | .DS_Store 22 | 23 | # Logs 24 | logs/ 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # Testing 31 | coverage/ 32 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 23 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | # Create app directory 5 | WORKDIR /app 6 | 7 | # Install app dependencies 8 | COPY package.json package-lock.json ./ 9 | RUN npm install --ignore-scripts 10 | 11 | # Copy source code 12 | COPY . . 13 | 14 | # Build the project 15 | RUN npm run build 16 | 17 | # Expose port if needed (optional, not specified in source, so leaving as is) 18 | 19 | # Command to run the MCP server 20 | CMD ["node", "dist/index.js"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Vapi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vapi MCP Server 2 | 3 | [![smithery badge](https://smithery.ai/badge/@VapiAI/vapi-mcp-server)](https://smithery.ai/server/@VapiAI/vapi-mcp-server) 4 | 5 | The Vapi [Model Context Protocol](https://modelcontextprotocol.com/) server allows you to integrate with Vapi APIs through function calling. 6 | 7 | 8 | Vapi Server MCP server 9 | 10 | 11 | ## Claude Desktop Setup 12 | 13 | 1. Open `Claude Desktop` and press `CMD + ,` to go to `Settings`. 14 | 2. Click on the `Developer` tab. 15 | 3. Click on the `Edit Config` button. 16 | 4. This will open the `claude_desktop_config.json` file in your file explorer. 17 | 5. Get your Vapi API key from the Vapi dashboard (). 18 | 6. Add the following to your `claude_desktop_config.json` file. See [here](https://modelcontextprotocol.io/quickstart/user) for more details. 19 | 7. Restart the Claude Desktop after editing the config file. 20 | 21 | ```json 22 | { 23 | "mcpServers": { 24 | "vapi-mcp-server": { 25 | "command": "npx", 26 | "args": [ 27 | "-y", 28 | "@vapi-ai/mcp-server" 29 | ], 30 | "env": { 31 | "VAPI_TOKEN": "" 32 | } 33 | } 34 | } 35 | } 36 | 37 | ``` 38 | 39 | ### Example Usage with Claude Desktop 40 | 41 | 1. Create or import a phone number using the Vapi dashboard (). 42 | 2. Create a new assistant using the existing 'Appointment Scheduler' template in the Vapi dashboard (). 43 | 3. Make sure to configure Claude Desktop to use the Vapi MCP server and restart the Claude Desktop app. 44 | 4. Ask Claude to initiate or schedule a call. See examples below: 45 | 46 | **Example 1:** Request an immediate call 47 | 48 | ```md 49 | I'd like to speak with my ShopHelper assistant to talk about my recent order. Can you have it call me at +1234567890? 50 | ``` 51 | 52 | **Example 2:** Schedule a future call 53 | 54 | ```md 55 | I need to schedule a call with Mary assistant for next Tuesday at 3:00 PM. My phone number is +1555123456. 56 | ``` 57 | 58 | ## Remote SSE Connection 59 | 60 | To connect to Vapi's MCP server via Server-Sent Events (SSE) Transport: 61 | 62 | - Connect to `https://mcp.vapi.ai/sse` from any MCP client using SSE Transport 63 | - Include your Vapi API key as a bearer token in the request headers 64 | - Example header: `Authorization: Bearer your_vapi_api_key_here` 65 | 66 | This connection allows you to access Vapi's functionality remotely without running a local server. 67 | 68 | ## Development 69 | 70 | ```bash 71 | # Install dependencies 72 | npm install 73 | 74 | # Build the server 75 | npm run build 76 | ``` 77 | 78 | Update your `claude_desktop_config.json` to use the local server. 79 | 80 | ```json 81 | { 82 | "mcpServers": { 83 | "vapi-local": { 84 | "command": "node", 85 | "args": [ 86 | "/dist/index.js" 87 | ], 88 | "env": { 89 | "VAPI_TOKEN": "" 90 | } 91 | }, 92 | } 93 | } 94 | ``` 95 | 96 | ### Testing 97 | 98 | The project has two types of tests: 99 | 100 | #### Unit Tests 101 | 102 | Unit tests use mocks to test the MCP server without making actual API calls to Vapi. 103 | 104 | ```bash 105 | # Run unit tests 106 | npm run test:unit 107 | ``` 108 | 109 | #### End-to-End Tests 110 | 111 | E2E tests run the full MCP server with actual API calls to Vapi. 112 | 113 | ```bash 114 | # Set your Vapi API token 115 | export VAPI_TOKEN=your_token_here 116 | 117 | # Run E2E tests 118 | npm run test:e2e 119 | ``` 120 | 121 | Note: E2E tests require a valid Vapi API token to be set in the environment. 122 | 123 | #### Running All Tests 124 | 125 | To run all tests at once: 126 | 127 | ```bash 128 | npm test 129 | ``` 130 | 131 | ## References 132 | 133 | - [VAPI Remote MCP Server](https://mcp.vapi.ai/) 134 | - [VAPI MCP Tool](https://docs.vapi.ai/tools/mcp) 135 | - [Model Context Protocol](https://modelcontextprotocol.com/) 136 | - [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) 137 | 138 | ## Supported Actions 139 | 140 | The Vapi MCP Server provides the following tools for integration: 141 | 142 | ### Assistant Tools 143 | 144 | - `list_assistants`: Lists all Vapi assistants 145 | - `create_assistant`: Creates a new Vapi assistant 146 | - `update_assistant`: Updates an existing Vapi assistant 147 | - `get_assistant`: Gets a Vapi assistant by ID 148 | 149 | ### Call Tools 150 | 151 | - `list_calls`: Lists all Vapi calls 152 | - `create_call`: Creates an outbound call 153 | - `get_call`: Gets details of a specific call 154 | 155 | > **Note:** The `create_call` action supports scheduling calls for immediate execution or for a future time. 156 | 157 | ### Phone Number Tools 158 | 159 | - `list_phone_numbers`: Lists all Vapi phone numbers 160 | - `get_phone_number`: Gets details of a specific phone number 161 | 162 | ### Vapi Tools 163 | 164 | - `list_tools`: Lists all Vapi tools 165 | - `get_tool`: Gets details of a specific tool 166 | -------------------------------------------------------------------------------- /examples/sse-client.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 4 | import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; 5 | import dotenv from 'dotenv'; 6 | import { parseToolResponse } from '../src/utils/response.js'; 7 | 8 | // Load environment variables from .env file 9 | dotenv.config(); 10 | 11 | // Ensure API key is available 12 | if (!process.env.VAPI_TOKEN) { 13 | console.error('Error: VAPI_TOKEN environment variable is required'); 14 | process.exit(1); 15 | } 16 | 17 | async function main() { 18 | try { 19 | // Initialize MCP client 20 | const mcpClient = new Client({ 21 | name: 'vapi-client-example', 22 | version: '1.0.0', 23 | }); 24 | 25 | // Create SSE transport for connection to remote Vapi MCP server 26 | const serverUrl = 'https://mcp.vapi.ai/sse'; 27 | const headers = { 28 | Authorization: `Bearer ${process.env.VAPI_TOKEN}`, 29 | }; 30 | const options: Record = { 31 | requestInit: { headers: headers }, 32 | eventSourceInit: { 33 | fetch: (url: string, init?: RequestInit) => { 34 | return fetch(url, { 35 | ...(init || {}), 36 | headers: { 37 | ...(init?.headers || {}), 38 | ...headers, 39 | }, 40 | }); 41 | }, 42 | }, 43 | }; 44 | const transport = new SSEClientTransport(new URL(serverUrl), options); 45 | 46 | console.log('Connecting to Vapi MCP server via SSE...'); 47 | await mcpClient.connect(transport); 48 | console.log('Connected successfully'); 49 | 50 | try { 51 | // List available tools 52 | const toolsResult = await mcpClient.listTools(); 53 | console.log('Available tools:'); 54 | toolsResult.tools.forEach((tool) => { 55 | console.log(`- ${tool.name}: ${tool.description}`); 56 | }); 57 | 58 | // List assistants 59 | console.log('\nListing assistants...'); 60 | const assistantsResponse = await mcpClient.callTool({ 61 | name: 'list_assistants', 62 | arguments: {}, 63 | }); 64 | 65 | const assistants = parseToolResponse(assistantsResponse); 66 | 67 | if (!(Array.isArray(assistants) && assistants.length > 0)) { 68 | console.log( 69 | 'No assistants found. Please create an assistant in the Vapi dashboard first.' 70 | ); 71 | return; 72 | } 73 | 74 | console.log('Your assistants:'); 75 | assistants.forEach((assistant: any) => { 76 | console.log(`- ${assistant.name} (${assistant.id})`); 77 | }); 78 | 79 | // List phone numbers 80 | console.log('\nListing phone numbers...'); 81 | const phoneNumbersResponse = await mcpClient.callTool({ 82 | name: 'list_phone_numbers', 83 | arguments: {}, 84 | }); 85 | 86 | const phoneNumbers = parseToolResponse(phoneNumbersResponse); 87 | 88 | if (!(Array.isArray(phoneNumbers) && phoneNumbers.length > 0)) { 89 | console.log( 90 | 'No phone numbers found. Please add a phone number in the Vapi dashboard first.' 91 | ); 92 | return; 93 | } 94 | 95 | console.log('Your phone numbers:'); 96 | phoneNumbers.forEach((phoneNumber: any) => { 97 | console.log(`- ${phoneNumber.phoneNumber} (${phoneNumber.id})`); 98 | }); 99 | 100 | // Create a call using the first assistant and first phone number 101 | const phoneNumberId = phoneNumbers[0].id; 102 | const assistantId = assistants[0].id; 103 | 104 | console.log( 105 | `\nCreating a call using assistant (${assistantId}) and phone number (${phoneNumberId})...` 106 | ); 107 | const createCallResponse = await mcpClient.callTool({ 108 | name: 'create_call', 109 | arguments: { 110 | assistantId: assistantId, 111 | phoneNumberId: phoneNumberId, 112 | customer: { 113 | phoneNumber: '+1234567890', // Replace with actual customer phone number 114 | }, 115 | // Optional: schedule a call for the future 116 | // scheduledAt: "2025-04-15T15:30:00Z" 117 | }, 118 | }); 119 | 120 | const createdCall = parseToolResponse(createCallResponse); 121 | console.log('Call created:', JSON.stringify(createdCall, null, 2)); 122 | 123 | // List calls 124 | console.log('\nListing calls...'); 125 | const callsResponse = await mcpClient.callTool({ 126 | name: 'list_calls', 127 | arguments: {}, 128 | }); 129 | 130 | const calls = parseToolResponse(callsResponse); 131 | 132 | if (Array.isArray(calls) && calls.length > 0) { 133 | console.log('Your calls:'); 134 | calls.forEach((call: any) => { 135 | const createdAt = call.createdAt ? new Date(call.createdAt).toLocaleString() : 'N/A'; 136 | const customerPhone = call.customer?.phoneNumber || 'N/A'; 137 | const endedReason = call.endedReason || 'N/A'; 138 | 139 | console.log(`- ID: ${call.id} | Status: ${call.status} | Created: ${createdAt} | Customer: ${customerPhone} | Ended reason: ${endedReason}`); 140 | }); 141 | } else { 142 | console.log('No calls found. Try creating a call first.'); 143 | } 144 | 145 | } finally { 146 | console.log('\nDisconnecting from server...'); 147 | await mcpClient.close(); 148 | console.log('Disconnected'); 149 | } 150 | } catch (error) { 151 | console.error('Error:', error); 152 | process.exit(1); 153 | } 154 | } 155 | 156 | main(); 157 | -------------------------------------------------------------------------------- /examples/stdio-client.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 4 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; 5 | import { join } from 'path'; 6 | import { parseToolResponse } from '../src/utils/response.js'; 7 | 8 | async function main() { 9 | try { 10 | const serverScriptPath = 11 | process.argv[2] || join(process.cwd(), 'dist', 'index.js'); 12 | console.log(`Connecting to server at: ${serverScriptPath}`); 13 | 14 | const mcpClient = new Client({ 15 | name: 'vapi-test-client', 16 | version: '1.0.0', 17 | }); 18 | 19 | const transport = new StdioClientTransport({ 20 | command: 'node', 21 | args: [serverScriptPath], 22 | env: process.env as Record, 23 | }); 24 | 25 | console.log('Connecting to MCP server...'); 26 | await mcpClient.connect(transport); 27 | console.log('Connected successfully'); 28 | 29 | try { 30 | console.log('Fetching available tools...'); 31 | const toolsResult = await mcpClient.listTools(); 32 | console.log('Available tools:'); 33 | toolsResult.tools.forEach((tool) => { 34 | console.log(`- ${tool.name}: ${tool.description}`); 35 | }); 36 | console.log('\nTools List Result:', JSON.stringify(toolsResult, null, 2)); 37 | 38 | console.log('\nCalling list_assistants tool...'); 39 | const assistantsResponse: any = await mcpClient.callTool({ 40 | name: 'list_assistants', 41 | arguments: {}, 42 | }); 43 | 44 | const assistants = parseToolResponse(assistantsResponse); 45 | 46 | console.log('Assistants:'); 47 | if (Array.isArray(assistants) && assistants.length > 0) { 48 | assistants.forEach((assistant) => { 49 | console.log(`- ${assistant.name} (${assistant.id})`); 50 | }); 51 | 52 | const assistantId = assistants[0]?.id; 53 | if (assistantId) { 54 | console.log('\nGet Assistant....'); 55 | const assistantResponse = await mcpClient.callTool({ 56 | name: 'get_assistant', 57 | arguments: { 58 | assistantId: assistantId, 59 | }, 60 | }); 61 | 62 | const assistant = parseToolResponse(assistantResponse); 63 | console.log('\nAssistant:', JSON.stringify(assistant, null, 2)); 64 | } else { 65 | console.log('No assistant ID found'); 66 | } 67 | } else { 68 | console.log('No assistants found'); 69 | } 70 | 71 | console.log('\nCalling create_assistant tool...'); 72 | const createAssistantResponse = await mcpClient.callTool({ 73 | name: 'create_assistant', 74 | arguments: { 75 | name: 'My Assistant', 76 | }, 77 | }); 78 | 79 | const createAssistant = parseToolResponse(createAssistantResponse); 80 | console.log( 81 | '\nCreated Assistant:', 82 | JSON.stringify(createAssistant, null, 2) 83 | ); 84 | 85 | console.log('\nCalling create_assistant with custom configuration...'); 86 | const customAssistantResponse = await mcpClient.callTool({ 87 | name: 'create_assistant', 88 | arguments: { 89 | name: 'Custom Assistant', 90 | llm: { 91 | provider: 'anthropic', 92 | model: 'claude-3-7-sonnet-20250219', 93 | }, 94 | voice: { 95 | provider: '11labs', 96 | voiceId: 'sarah', 97 | }, 98 | }, 99 | }); 100 | 101 | const customAssistant = parseToolResponse(customAssistantResponse); 102 | console.log( 103 | '\nCustom Assistant:', 104 | JSON.stringify(customAssistant, null, 2) 105 | ); 106 | 107 | console.log('\nCalling create_assistant with string-formatted LLM...'); 108 | const stringLLMAssistantResponse = await mcpClient.callTool({ 109 | name: 'create_assistant', 110 | arguments: { 111 | name: 'String LLM Assistant', 112 | llm: JSON.stringify({ 113 | provider: 'openai', 114 | model: 'gpt-4o-mini', 115 | }), 116 | voice: { 117 | provider: 'vapi', 118 | voiceId: 'Elliot', 119 | }, 120 | }, 121 | }); 122 | 123 | const stringLLMAssistant = parseToolResponse(stringLLMAssistantResponse); 124 | console.log( 125 | '\nString LLM Assistant:', 126 | JSON.stringify(stringLLMAssistant, null, 2) 127 | ); 128 | 129 | // List phone numbers 130 | console.log('\nListing phone numbers...'); 131 | const phoneNumbersResponse = await mcpClient.callTool({ 132 | name: 'list_phone_numbers', 133 | arguments: {}, 134 | }); 135 | 136 | const phoneNumbers = parseToolResponse(phoneNumbersResponse); 137 | 138 | if (Array.isArray(phoneNumbers) && phoneNumbers.length > 0) { 139 | console.log('Your phone numbers:'); 140 | phoneNumbers.forEach((phoneNumber: any) => { 141 | console.log(`- ${phoneNumber.phoneNumber} (${phoneNumber.id})`); 142 | }); 143 | } else { 144 | console.log('No phone numbers found. Please add a phone number in the Vapi dashboard first.'); 145 | } 146 | 147 | // List calls 148 | console.log('\nListing calls...'); 149 | const callsResponse = await mcpClient.callTool({ 150 | name: 'list_calls', 151 | arguments: {}, 152 | }); 153 | 154 | const calls = parseToolResponse(callsResponse); 155 | 156 | if (Array.isArray(calls) && calls.length > 0) { 157 | console.log('Your calls:'); 158 | calls.forEach((call: any) => { 159 | const createdAt = call.createdAt ? new Date(call.createdAt).toLocaleString() : 'N/A'; 160 | const customerPhone = call.customer?.phoneNumber || 'N/A'; 161 | const endedReason = call.endedReason || 'N/A'; 162 | 163 | console.log(`- ID: ${call.id} | Status: ${call.status} | Created: ${createdAt} | Customer: ${customerPhone} | Ended reason: ${endedReason}`); 164 | }); 165 | } else { 166 | console.log('No calls found'); 167 | } 168 | 169 | // Create a call 170 | console.log('\nCreating a call...'); 171 | 172 | if (Array.isArray(assistants) && assistants.length > 0 && 173 | Array.isArray(phoneNumbers) && phoneNumbers.length > 0) { 174 | 175 | const phoneNumberId = phoneNumbers[0].id; 176 | const assistantId = assistants[0].id; 177 | 178 | console.log(`Creating a call using assistant (${assistantId}) and phone number (${phoneNumberId})...`); 179 | 180 | const createCallResponse = await mcpClient.callTool({ 181 | name: 'create_call', 182 | arguments: { 183 | assistantId: assistantId, 184 | phoneNumberId: phoneNumberId, 185 | customer: { 186 | // phoneNumber: '+1234567890', // Replace with actual customer phone number 187 | }, 188 | // Optional: schedule a call for the future 189 | // scheduledAt: "2025-04-15T15:30:00Z" 190 | }, 191 | }); 192 | 193 | const createdCall = parseToolResponse(createCallResponse); 194 | console.log('\nCall created:', JSON.stringify(createdCall, null, 2)); 195 | 196 | // Get call details if we have the call ID 197 | if (createdCall && createdCall.id) { 198 | console.log(`\nGetting details for call ${createdCall.id}...`); 199 | const callDetailsResponse = await mcpClient.callTool({ 200 | name: 'get_call', 201 | arguments: { 202 | callId: createdCall.id 203 | }, 204 | }); 205 | 206 | const callDetails = parseToolResponse(callDetailsResponse); 207 | console.log('\nCall details:', JSON.stringify(callDetails, null, 2)); 208 | } 209 | } else { 210 | console.log('Cannot create call: Need both assistants and phone numbers.'); 211 | } 212 | } finally { 213 | console.log('\nDisconnecting from server...'); 214 | await mcpClient.close(); 215 | console.log('Disconnected'); 216 | } 217 | } catch (error) { 218 | console.error('Error:', error); 219 | process.exit(1); 220 | } 221 | } 222 | 223 | main(); 224 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: 'ts-jest/presets/default-esm', 4 | testEnvironment: 'node', 5 | transform: { 6 | '^.+\\.tsx?$': [ 7 | 'ts-jest', 8 | { 9 | useESM: true, 10 | isolatedModules: true, 11 | }, 12 | ], 13 | }, 14 | testRegex: '.*\\.test\\.tsx?$', 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 16 | testPathIgnorePatterns: ['/node_modules/', '/dist/', '\\.d\\.ts$', '.*\\.mock\\.ts$'], 17 | extensionsToTreatAsEsm: ['.ts'], 18 | moduleNameMapper: { 19 | '^(\\.{1,2}/.*)\\.js$': '$1', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vapi-ai/mcp-server", 3 | "description": "Vapi MCP Server", 4 | "version": "0.0.6", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "type": "module", 8 | "author": { 9 | "name": "Vapi AI", 10 | "url": "https://vapi.ai" 11 | }, 12 | "contributors": [ 13 | { 14 | "name": "ramsrib", 15 | "url": "https://github.com/ramsrib" 16 | } 17 | ], 18 | "homepage": "https://github.com/VapiAI/mcp-server#readme", 19 | "repository": "https://github.com/VapiAI/mcp-server", 20 | "bugs": { 21 | "url": "https://github.com/VapiAI/mcp-server/issues" 22 | }, 23 | "keywords": [ 24 | "vapi", 25 | "mcp", 26 | "model-context-protocol", 27 | "ai", 28 | "claude", 29 | "voice-api", 30 | "tool-calling" 31 | ], 32 | "bin": { 33 | "@vapi-ai/mcp-server": "dist/index.js" 34 | }, 35 | "scripts": { 36 | "prepare": "npm run build", 37 | "build": "tsc && shx chmod +x dist/*.js", 38 | "start": "node dist/index.js", 39 | "dev": "tsx watch src/index.ts", 40 | "dev:stdio-example": "tsx examples/stdio-client.ts", 41 | "dev:sse-example": "tsx examples/sse-client.ts", 42 | "inspector": "mcp-inspector", 43 | "test": "NODE_OPTIONS=--experimental-vm-modules jest", 44 | "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest src/tests/mcp-server-mock.test.ts", 45 | "test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest src/tests/mcp-server-e2e.test.ts" 46 | }, 47 | "files": [ 48 | "dist", 49 | "dist/**/*.d.ts", 50 | "dist/**/*.d.ts.map" 51 | ], 52 | "dependencies": { 53 | "@modelcontextprotocol/sdk": "^1.11.0", 54 | "@vapi-ai/server-sdk": "^0.5.2", 55 | "dotenv": "^16.4.7", 56 | "zod": "^3.24.2", 57 | "zod-to-json-schema": "^3.24.5" 58 | }, 59 | "devDependencies": { 60 | "@modelcontextprotocol/inspector": "^0.8.2", 61 | "@types/jest": "^29.5.14", 62 | "@types/node": "^22.14.0", 63 | "jest": "^29.7.0", 64 | "shx": "^0.4.0", 65 | "ts-jest": "^29.3.1", 66 | "tsx": "^4.19.3", 67 | "typescript": "^5.8.3" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - VAPI_TOKEN 10 | properties: 11 | VAPI_TOKEN: 12 | type: string 13 | default: "" 14 | description: Vapi API token used for making API calls 15 | commandFunction: 16 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 17 | |- 18 | (config) => ({ 19 | command: 'node', 20 | args: ['dist/index.js'], 21 | env: { VAPI_TOKEN: config.VAPI_TOKEN } 22 | }) 23 | exampleConfig: 24 | VAPI_TOKEN: dummy_vapi_token_123 25 | -------------------------------------------------------------------------------- /src/__tests__/mcp-server-e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 2 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; 3 | import { join } from 'path'; 4 | import { jest } from '@jest/globals'; 5 | import dotenv from 'dotenv'; 6 | import { parseToolResponse } from '../utils/response.js'; 7 | 8 | dotenv.config(); 9 | 10 | jest.setTimeout(15000); 11 | 12 | describe('MCP Server E2E Test', () => { 13 | let mcpClient: Client; 14 | let transport: StdioClientTransport; 15 | 16 | beforeAll(async () => { 17 | const serverScriptPath = join(process.cwd(), 'src', 'index.ts'); 18 | console.log(`Using server source at: ${serverScriptPath}`); 19 | 20 | mcpClient = new Client({ name: 'vapi-e2e-test-client', version: '1.0.0' }); 21 | 22 | transport = new StdioClientTransport({ 23 | command: 'npx', 24 | args: ['tsx', serverScriptPath], 25 | env: { 26 | ...process.env, 27 | VAPI_TOKEN: process.env.VAPI_TOKEN || 'test-token', 28 | }, 29 | }); 30 | 31 | console.log('Connecting to MCP server...'); 32 | await mcpClient.connect(transport); 33 | console.log('Connected to server successfully'); 34 | }); 35 | 36 | afterAll(async () => { 37 | console.log('Disconnecting from server...'); 38 | await mcpClient.close(); 39 | console.log('Disconnected from server'); 40 | }); 41 | 42 | test('should list available tools', async () => { 43 | console.log('Requesting available tools...'); 44 | const toolsResult = await mcpClient.listTools(); 45 | 46 | expect(toolsResult.tools).toBeDefined(); 47 | expect(Array.isArray(toolsResult.tools)).toBe(true); 48 | 49 | const toolNames = toolsResult.tools.map((tool) => tool.name); 50 | console.log('Available tools:', toolNames); 51 | 52 | expect(toolNames).toContain('list_assistants'); 53 | expect(toolNames).toContain('create_assistant'); 54 | expect(toolNames).toContain('get_assistant'); 55 | 56 | expect(toolNames).toContain('list_phone_numbers'); 57 | expect(toolNames).toContain('get_phone_number'); 58 | 59 | expect(toolNames).toContain('list_calls'); 60 | expect(toolNames).toContain('create_call'); 61 | expect(toolNames).toContain('get_call'); 62 | }); 63 | 64 | describe('Assistant Tools', () => { 65 | test('should list all assistants', async () => { 66 | console.log('Calling list_assistants tool...'); 67 | const rawResult = await mcpClient.callTool({ 68 | name: 'list_assistants', 69 | arguments: {}, 70 | }); 71 | 72 | const result = parseToolResponse(rawResult); 73 | expect(result).toBeDefined(); 74 | if (Array.isArray(result)) { 75 | console.log(`Received ${result.length} assistants in array form`); 76 | } else { 77 | console.log(`Received assistants in non-array form:`, result); 78 | } 79 | }); 80 | 81 | test('should create a new assistant', async () => { 82 | console.log('Calling create_assistant tool...'); 83 | const assistantData = { 84 | name: `Test Assistant ${Date.now()}`, 85 | instructions: 'You are a helpful test assistant', 86 | llm: { 87 | provider: 'openai', 88 | model: 'gpt-4o', 89 | }, 90 | transcriber: { 91 | provider: 'deepgram', 92 | model: 'nova-3', 93 | }, 94 | voice: { 95 | provider: '11labs', 96 | model: 'eleven_turbo_v2_5', 97 | voiceId: 'sarah', 98 | }, 99 | firstMessage: "Hello, I'm your vapi assistant.", 100 | }; 101 | 102 | const rawResult = await mcpClient.callTool({ 103 | name: 'create_assistant', 104 | arguments: assistantData, 105 | }); 106 | 107 | const result = parseToolResponse(rawResult); 108 | expect(result).toBeDefined(); 109 | expect(result.id).toBeDefined(); 110 | expect(result.name).toBe(assistantData.name); 111 | expect(result.llm.provider).toBe(assistantData.llm.provider); 112 | expect(result.llm.model).toBe(assistantData.llm.model); 113 | 114 | console.log(`Successfully created assistant with ID: ${result.id}`); 115 | }); 116 | 117 | test('should get an assistant by ID', async () => { 118 | console.log('Listing assistants to get the first one...'); 119 | const rawListResult = await mcpClient.callTool({ 120 | name: 'list_assistants', 121 | arguments: {}, 122 | }); 123 | 124 | const listResult = parseToolResponse(rawListResult); 125 | expect(listResult).toBeDefined(); 126 | expect(Array.isArray(listResult)).toBe(true); 127 | 128 | if (listResult.length === 0) { 129 | console.log('No assistants found, skipping get_assistant test.'); 130 | // Optionally, you could fail the test here or create an assistant first 131 | return; 132 | } 133 | 134 | const firstAssistant = listResult[0]; 135 | expect(firstAssistant.id).toBeDefined(); 136 | const assistantId = firstAssistant.id; 137 | const assistantName = firstAssistant.name; 138 | 139 | console.log( 140 | `Attempting to fetch assistant with ID: ${assistantId} (Name: ${assistantName})` 141 | ); 142 | 143 | const rawGetResult = await mcpClient.callTool({ 144 | name: 'get_assistant', 145 | arguments: { assistantId: assistantId }, 146 | }); 147 | 148 | const getResult = parseToolResponse(rawGetResult); 149 | expect(getResult).toBeDefined(); 150 | expect(getResult.id).toBe(assistantId); 151 | expect(getResult.name).toBe(assistantName); // Verify name matches too 152 | 153 | console.log(`Successfully fetched assistant with ID: ${getResult.id}`); 154 | }); 155 | 156 | test('should handle invalid assistant ID', async () => { 157 | const invalidId = 'non-existent-assistant-id-' + Date.now(); 158 | console.log( 159 | `Testing error handling with invalid assistant ID: ${invalidId}` 160 | ); 161 | 162 | try { 163 | const rawResult = await mcpClient.callTool({ 164 | name: 'get_assistant', 165 | arguments: { assistantId: invalidId }, 166 | }); 167 | 168 | const result = parseToolResponse(rawResult); 169 | // Check for error in the result 170 | if ( 171 | result.error || 172 | (typeof result === 'string' && 173 | (result.includes('Error') || result.includes('error'))) 174 | ) { 175 | // Test passed - we got an error response 176 | expect(true).toBe(true); 177 | } else { 178 | // If we somehow got a successful result, fail the test 179 | expect(result).toContain('error'); 180 | } 181 | } catch (error) { 182 | // This is also acceptable - the API might throw instead of returning error 183 | expect(error).toBeDefined(); 184 | } 185 | }); 186 | }); 187 | 188 | describe('Phone Number Tools', () => { 189 | test('should list all phone numbers', async () => { 190 | console.log('Calling list_phone_numbers tool...'); 191 | const rawResult = await mcpClient.callTool({ 192 | name: 'list_phone_numbers', 193 | arguments: {}, 194 | }); 195 | 196 | const result = parseToolResponse(rawResult); 197 | expect(result).toBeDefined(); 198 | if (Array.isArray(result)) { 199 | console.log(`Received ${result.length} phone numbers in array form`); 200 | } else { 201 | console.log(`Received phone numbers in non-array form:`, result); 202 | } 203 | }); 204 | 205 | test('should handle invalid phone number ID', async () => { 206 | const invalidId = 'non-existent-phone-number-id-' + Date.now(); 207 | console.log( 208 | `Testing error handling with invalid phone number ID: ${invalidId}` 209 | ); 210 | 211 | try { 212 | const rawResult = await mcpClient.callTool({ 213 | name: 'get_phone_number', 214 | arguments: { phoneNumberId: invalidId }, 215 | }); 216 | 217 | const result = parseToolResponse(rawResult); 218 | // Check for error in the result 219 | if ( 220 | result.error || 221 | (typeof result === 'string' && 222 | (result.includes('Error') || result.includes('error'))) 223 | ) { 224 | // Test passed - we got an error response 225 | expect(true).toBe(true); 226 | } else { 227 | // If we somehow got a successful result, fail the test 228 | expect(result).toContain('error'); 229 | } 230 | } catch (error) { 231 | // This is also acceptable - the API might throw instead of returning error 232 | expect(error).toBeDefined(); 233 | } 234 | }); 235 | }); 236 | 237 | describe('Call Tools', () => { 238 | test('should list all calls', async () => { 239 | console.log('Calling list_calls tool...'); 240 | const rawResult = await mcpClient.callTool({ 241 | name: 'list_calls', 242 | arguments: {}, 243 | }); 244 | 245 | const result = parseToolResponse(rawResult); 246 | expect(result).toBeDefined(); 247 | if (Array.isArray(result)) { 248 | console.log(`Received ${result.length} calls in array form`); 249 | } else { 250 | console.log(`Received calls in non-array form:`, result); 251 | } 252 | }); 253 | 254 | test('should handle invalid call ID', async () => { 255 | const invalidId = 'non-existent-call-id-' + Date.now(); 256 | console.log(`Testing error handling with invalid call ID: ${invalidId}`); 257 | 258 | try { 259 | const rawResult = await mcpClient.callTool({ 260 | name: 'get_call', 261 | arguments: { callId: invalidId }, 262 | }); 263 | 264 | const result = parseToolResponse(rawResult); 265 | // Check for error in the result 266 | if ( 267 | result.error || 268 | (typeof result === 'string' && 269 | (result.includes('Error') || result.includes('error'))) 270 | ) { 271 | // Test passed - we got an error response 272 | expect(true).toBe(true); 273 | } else { 274 | // If we somehow got a successful result, fail the test 275 | expect(result).toContain('error'); 276 | } 277 | } catch (error) { 278 | // This is also acceptable - the API might throw instead of returning error 279 | expect(error).toBeDefined(); 280 | } 281 | }); 282 | }); 283 | }); 284 | -------------------------------------------------------------------------------- /src/__tests__/mcp-server-mock.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import { createMcpServer } from '../index.js'; 3 | import { createMockVapiClient } from './mocks/vapi-client.mock.js'; 4 | 5 | jest.mock('../client.js', () => ({ 6 | createVapiClient: jest.fn(() => createMockVapiClient()), 7 | })); 8 | 9 | process.env.VAPI_TOKEN = 'test-mock-token'; 10 | 11 | describe('MCP Server Unit Test (with mocks)', () => { 12 | let mcpServer: any; 13 | 14 | beforeAll(() => { 15 | console.log('Creating MCP server with mocked Vapi client...'); 16 | mcpServer = createMcpServer(); 17 | console.log('MCP server created successfully'); 18 | }); 19 | 20 | test('server should be initialized correctly', () => { 21 | expect(mcpServer).toBeDefined(); 22 | expect(typeof mcpServer).toBe('object'); 23 | expect(mcpServer.constructor.name).toBe('McpServer'); 24 | 25 | expect(mcpServer.connect).toBeDefined(); 26 | expect(typeof mcpServer.connect).toBe('function'); 27 | }); 28 | 29 | test('server should have required internal structures', () => { 30 | expect(mcpServer._registeredTools).toBeDefined(); 31 | expect(typeof mcpServer._registeredTools).toBe('object'); 32 | }); 33 | 34 | test('server should have registered Vapi tools', () => { 35 | const registeredTools = Object.keys(mcpServer._registeredTools); 36 | expect(registeredTools.length).toBeGreaterThan(0); 37 | 38 | console.log('Found registered tools:', registeredTools); 39 | 40 | expect(registeredTools).toContain('list_assistants'); 41 | expect(registeredTools).toContain('create_assistant'); 42 | expect(registeredTools).toContain('get_assistant'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/__tests__/mocks/vapi-client.mock.ts: -------------------------------------------------------------------------------- 1 | export class MockVapiClient { 2 | assistants = { 3 | list: jest.fn().mockResolvedValue([ 4 | { 5 | id: 'mock-assistant-id-1', 6 | name: 'Mock Assistant 1', 7 | model: 'gpt-4', 8 | instructions: 'Example instructions', 9 | createdAt: new Date().toISOString(), 10 | updatedAt: new Date().toISOString(), 11 | }, 12 | { 13 | id: 'mock-assistant-id-2', 14 | name: 'Mock Assistant 2', 15 | model: 'claude-3-opus', 16 | instructions: 'Another example', 17 | createdAt: new Date().toISOString(), 18 | updatedAt: new Date().toISOString(), 19 | }, 20 | ]), 21 | 22 | get: jest.fn().mockImplementation((id) => { 23 | return Promise.resolve({ 24 | id, 25 | name: `Mock Assistant ${id}`, 26 | model: 'gpt-4', 27 | instructions: 'Example instructions', 28 | createdAt: new Date().toISOString(), 29 | updatedAt: new Date().toISOString(), 30 | }); 31 | }), 32 | 33 | create: jest.fn().mockImplementation((data) => { 34 | return Promise.resolve({ 35 | id: 'new-mock-assistant-id', 36 | ...data, 37 | createdAt: new Date().toISOString(), 38 | updatedAt: new Date().toISOString(), 39 | }); 40 | }), 41 | }; 42 | } 43 | 44 | export const createMockVapiClient = () => { 45 | return new MockVapiClient(); 46 | }; 47 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { VapiClient, Vapi } from '@vapi-ai/server-sdk'; 2 | 3 | export const createVapiClient = (token: string): VapiClient => { 4 | if (!token) { 5 | throw new Error('No Vapi token available'); 6 | } 7 | return new VapiClient({ token }); 8 | }; 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 5 | import { registerAllTools } from './tools/index.js'; 6 | import { createVapiClient } from './client.js'; 7 | 8 | import dotenv from 'dotenv'; 9 | dotenv.config(); 10 | 11 | function createMcpServer() { 12 | const vapiToken = process.env.VAPI_TOKEN; 13 | if (!vapiToken) { 14 | throw new Error('VAPI_TOKEN environment variable is required'); 15 | } 16 | 17 | const vapiClient = createVapiClient(vapiToken); 18 | 19 | const mcpServer = new McpServer({ 20 | name: 'Vapi MCP', 21 | version: '0.1.0', 22 | capabilities: [], 23 | }); 24 | 25 | registerAllTools(mcpServer, vapiClient); 26 | 27 | return mcpServer; 28 | } 29 | 30 | async function main() { 31 | try { 32 | const mcpServer = createMcpServer(); 33 | 34 | const transport = new StdioServerTransport(); 35 | await mcpServer.connect(transport); 36 | 37 | setupShutdownHandler(mcpServer); 38 | } catch (err) { 39 | process.exit(1); 40 | } 41 | } 42 | 43 | function setupShutdownHandler(mcpServer: McpServer) { 44 | process.on('SIGINT', async () => { 45 | try { 46 | await mcpServer.close(); 47 | process.exit(0); 48 | } catch (err) { 49 | process.exit(1); 50 | } 51 | }); 52 | } 53 | 54 | main().catch((err) => { 55 | process.exit(1); 56 | }); 57 | 58 | export { createMcpServer }; 59 | -------------------------------------------------------------------------------- /src/schemas/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // ===== Model Provider and Models ===== 4 | 5 | export const ModelProvider = { 6 | OpenAI: 'openai', 7 | Anthropic: 'anthropic', 8 | GoogleAI: 'google', 9 | } as const; 10 | 11 | export const OpenAIModels = { 12 | GPT4o: 'gpt-4o', 13 | GPT4oMini: 'gpt-4o-mini', 14 | } as const; 15 | 16 | export const AnthropicModels = { 17 | Claude3Sonnet: 'claude-3-7-sonnet-20250219', 18 | Claude3Haiku: 'claude-3-5-haiku-20241022', 19 | } as const; 20 | 21 | export const GoogleModels = { 22 | GeminiPro: 'gemini-1.5-pro', 23 | GeminiFlash: 'gemini-1.5-flash', 24 | Gemini2Flash: 'gemini-2.0-flash', 25 | Gemini2Pro: 'gemini-2.0-pro', 26 | } as const; 27 | 28 | export type ModelProviderType = 29 | (typeof ModelProvider)[keyof typeof ModelProvider]; 30 | export type OpenAIModelType = (typeof OpenAIModels)[keyof typeof OpenAIModels]; 31 | export type AnthropicModelType = 32 | (typeof AnthropicModels)[keyof typeof AnthropicModels]; 33 | export type GoogleModelType = (typeof GoogleModels)[keyof typeof GoogleModels]; 34 | 35 | const OpenAILLMSchema = z.object({ 36 | provider: z.literal(ModelProvider.OpenAI), 37 | model: z.enum([OpenAIModels.GPT4o, OpenAIModels.GPT4oMini] as const), 38 | }); 39 | 40 | const AnthropicLLMSchema = z.object({ 41 | provider: z.literal(ModelProvider.Anthropic), 42 | model: z.enum([ 43 | AnthropicModels.Claude3Sonnet, 44 | AnthropicModels.Claude3Haiku, 45 | ] as const), 46 | }); 47 | 48 | const GoogleLLMSchema = z.object({ 49 | provider: z.literal(ModelProvider.GoogleAI), 50 | model: z.enum([ 51 | GoogleModels.GeminiPro, 52 | GoogleModels.GeminiFlash, 53 | GoogleModels.Gemini2Flash, 54 | GoogleModels.Gemini2Pro, 55 | ] as const), 56 | }); 57 | 58 | const GenericLLMSchema = z.object({ 59 | provider: z.string(), 60 | model: z.string(), 61 | }); 62 | 63 | const LLMSchema = z.union([ 64 | OpenAILLMSchema, 65 | AnthropicLLMSchema, 66 | GoogleLLMSchema, 67 | GenericLLMSchema, 68 | ]); 69 | 70 | export const DEFAULT_LLM = { 71 | provider: ModelProvider.OpenAI, 72 | model: OpenAIModels.GPT4o, 73 | }; 74 | 75 | const VoiceProviderSchema = z.enum([ 76 | 'vapi', 77 | '11labs', 78 | 'azure', 79 | 'cartesia', 80 | 'custom-voice', 81 | 'deepgram', 82 | '11labs', 83 | 'hume', 84 | 'lmnt', 85 | 'neuphonic', 86 | 'openai', 87 | 'playht', 88 | 'rime-ai', 89 | 'smallest-ai', 90 | 'tavus', 91 | 'sesame', 92 | ]); 93 | 94 | export type VoiceProviderType = z.infer; 95 | 96 | export const DEFAULT_VOICE = { 97 | provider: '11labs' as VoiceProviderType, 98 | voiceId: 'sarah', 99 | }; 100 | 101 | export const DEFAULT_TRANSCRIBER = { 102 | provider: 'deepgram', 103 | model: 'nova-3', 104 | }; 105 | 106 | export const ElevenLabsVoiceIds = { 107 | Sarah: 'sarah', 108 | Phillip: 'phillip', 109 | Steve: 'steve', 110 | Joseph: 'joseph', 111 | Myra: 'myra', 112 | } as const; 113 | 114 | // ===== Common Schemas ===== 115 | 116 | export const BaseResponseSchema = z.object({ 117 | id: z.string(), 118 | createdAt: z.string(), 119 | updatedAt: z.string(), 120 | }); 121 | 122 | // ===== Assistant Schemas ===== 123 | 124 | export const CreateAssistantInputSchema = z.object({ 125 | name: z.string().describe('Name of the assistant'), 126 | instructions: z 127 | .string() 128 | .optional() 129 | .default('You are a helpful assistant.') 130 | .describe('Instructions for the assistant'), 131 | llm: z 132 | .union([ 133 | LLMSchema, 134 | z.string().transform((str) => { 135 | try { 136 | return JSON.parse(str); 137 | } catch (e) { 138 | throw new Error(`Invalid LLM JSON string: ${str}`); 139 | } 140 | }), 141 | ]) 142 | .default(DEFAULT_LLM) 143 | .describe('LLM configuration'), 144 | toolIds: z 145 | .array(z.string()) 146 | .optional() 147 | .describe('IDs of tools to use with this assistant'), 148 | transcriber: z 149 | .object({ 150 | provider: z.string().describe('Provider to use for transcription'), 151 | model: z.string().describe('Transcription model to use'), 152 | }) 153 | .default(DEFAULT_TRANSCRIBER) 154 | .describe('Transcription configuration'), 155 | voice: z 156 | .object({ 157 | provider: VoiceProviderSchema.describe('Provider to use for voice'), 158 | voiceId: z.string().describe('Voice ID to use'), 159 | model: z.string().optional().describe('Voice model to use'), 160 | }) 161 | .default(DEFAULT_VOICE) 162 | .describe('Voice configuration'), 163 | firstMessage: z 164 | .string() 165 | .optional() 166 | .default('Hello, how can I help you today?') 167 | .describe('First message to say to the user'), 168 | firstMessageMode: z 169 | .enum([ 170 | 'assistant-speaks-first', 171 | 'assistant-waits-for-user', 172 | 'assistant-speaks-first-with-model-generated-message', 173 | ]) 174 | .default('assistant-speaks-first') 175 | .optional() 176 | .describe('This determines who speaks first, either assistant or user'), 177 | }); 178 | 179 | export const AssistantOutputSchema = BaseResponseSchema.extend({ 180 | name: z.string(), 181 | llm: z.object({ 182 | provider: z.string(), 183 | model: z.string(), 184 | }), 185 | voice: z.object({ 186 | provider: z.string(), 187 | voiceId: z.string(), 188 | model: z.string().optional(), 189 | }), 190 | transcriber: z.object({ 191 | provider: z.string(), 192 | model: z.string(), 193 | }), 194 | toolIds: z.array(z.string()).optional(), 195 | }); 196 | 197 | export const GetAssistantInputSchema = z.object({ 198 | assistantId: z.string().describe('ID of the assistant to get'), 199 | }); 200 | 201 | export const UpdateAssistantInputSchema = z.object({ 202 | assistantId: z.string().describe('ID of the assistant to update'), 203 | name: z.string().optional().describe('New name for the assistant'), 204 | instructions: z 205 | .string() 206 | .optional() 207 | .describe('New instructions for the assistant'), 208 | llm: z 209 | .union([ 210 | LLMSchema, 211 | z.string().transform((str) => { 212 | try { 213 | return JSON.parse(str); 214 | } catch (e) { 215 | throw new Error(`Invalid LLM JSON string: ${str}`); 216 | } 217 | }), 218 | ]) 219 | .optional() 220 | .describe('New LLM configuration'), 221 | toolIds: z 222 | .array(z.string()) 223 | .optional() 224 | .describe('New IDs of tools to use with this assistant'), 225 | transcriber: z 226 | .object({ 227 | provider: z.string().describe('Provider to use for transcription'), 228 | model: z.string().describe('Transcription model to use'), 229 | }) 230 | .optional() 231 | .describe('New transcription configuration'), 232 | voice: z 233 | .object({ 234 | provider: VoiceProviderSchema.describe('Provider to use for voice'), 235 | voiceId: z.string().describe('Voice ID to use'), 236 | model: z.string().optional().describe('Voice model to use'), 237 | }) 238 | .optional() 239 | .describe('New voice configuration'), 240 | firstMessage: z 241 | .string() 242 | .optional() 243 | .describe('First message to say to the user'), 244 | firstMessageMode: z 245 | .enum([ 246 | 'assistant-speaks-first', 247 | 'assistant-waits-for-user', 248 | 'assistant-speaks-first-with-model-generated-message', 249 | ]) 250 | .optional() 251 | .describe('This determines who speaks first, either assistant or user'), 252 | }); 253 | 254 | // ===== Call Schemas ===== 255 | 256 | export const CallInputSchema = z.object({ 257 | assistantId: z 258 | .string() 259 | .optional() 260 | .describe('ID of the assistant to use for the call'), 261 | phoneNumberId: z 262 | .string() 263 | .optional() 264 | .describe('ID of the phone number to use for the call'), 265 | customer: z 266 | .object({ 267 | phoneNumber: z.string().describe('Customer phone number'), 268 | }) 269 | .optional() 270 | .describe('Customer information'), 271 | scheduledAt: z 272 | .string() 273 | .optional() 274 | .describe( 275 | 'ISO datetime string for when the call should be scheduled (e.g. "2025-03-25T22:39:27.771Z")' 276 | ), 277 | }); 278 | 279 | export const CallOutputSchema = BaseResponseSchema.extend({ 280 | status: z.string(), 281 | endedReason: z.string().optional(), 282 | assistantId: z.string().optional(), 283 | phoneNumberId: z.string().optional(), 284 | customer: z 285 | .object({ 286 | phoneNumber: z.string(), 287 | }) 288 | .optional(), 289 | scheduledAt: z.string().optional(), 290 | }); 291 | 292 | export const GetCallInputSchema = z.object({ 293 | callId: z.string().describe('ID of the call to get'), 294 | }); 295 | 296 | // ===== Phone Number Schemas ===== 297 | 298 | export const GetPhoneNumberInputSchema = z.object({ 299 | phoneNumberId: z.string().describe('ID of the phone number to get'), 300 | }); 301 | 302 | export const PhoneNumberOutputSchema = BaseResponseSchema.extend({ 303 | name: z.string().optional(), 304 | phoneNumber: z.string(), 305 | status: z.string(), 306 | capabilities: z 307 | .object({ 308 | sms: z.boolean().optional(), 309 | voice: z.boolean().optional(), 310 | }) 311 | .optional(), 312 | }); 313 | 314 | // ===== Tool Schemas ===== 315 | 316 | export const GetToolInputSchema = z.object({ 317 | toolId: z.string().describe('ID of the tool to get'), 318 | }); 319 | 320 | export const ToolOutputSchema = BaseResponseSchema.extend({ 321 | type: z 322 | .string() 323 | .describe('Type of the tool (dtmf, function, mcp, query, etc.)'), 324 | name: z.string().describe('Name of the tool'), 325 | description: z.string().describe('Description of the tool'), 326 | }); 327 | -------------------------------------------------------------------------------- /src/tools/assistant.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { VapiClient, Vapi } from '@vapi-ai/server-sdk'; 3 | import { 4 | CreateAssistantInputSchema, 5 | GetAssistantInputSchema, 6 | UpdateAssistantInputSchema, 7 | } from '../schemas/index.js'; 8 | import { 9 | transformAssistantInput, 10 | transformAssistantOutput, 11 | transformUpdateAssistantInput, 12 | } from '../transformers/index.js'; 13 | import { createToolHandler } from './utils.js'; 14 | 15 | export const registerAssistantTools = ( 16 | server: McpServer, 17 | vapiClient: VapiClient 18 | ) => { 19 | server.tool( 20 | 'list_assistants', 21 | 'Lists all Vapi assistants', 22 | {}, 23 | createToolHandler(async () => { 24 | // console.log('list_assistants'); 25 | const assistants = await vapiClient.assistants.list({ limit: 10 }); 26 | // console.log('assistants', assistants); 27 | return assistants.map(transformAssistantOutput); 28 | }) 29 | ); 30 | 31 | server.tool( 32 | 'create_assistant', 33 | 'Creates a new Vapi assistant', 34 | CreateAssistantInputSchema.shape, 35 | createToolHandler(async (data) => { 36 | // console.log('create_assistant', data); 37 | const createAssistantDto = transformAssistantInput(data); 38 | const assistant = await vapiClient.assistants.create(createAssistantDto); 39 | return transformAssistantOutput(assistant); 40 | }) 41 | ); 42 | 43 | server.tool( 44 | 'get_assistant', 45 | 'Gets a Vapi assistant by ID', 46 | GetAssistantInputSchema.shape, 47 | createToolHandler(async (data) => { 48 | // console.log('get_assistant', data); 49 | const assistantId = data.assistantId; 50 | try { 51 | const assistant = await vapiClient.assistants.get(assistantId); 52 | if (!assistant) { 53 | throw new Error(`Assistant with ID ${assistantId} not found`); 54 | } 55 | return transformAssistantOutput(assistant); 56 | } catch (error: any) { 57 | console.error(`Error getting assistant: ${error.message}`); 58 | throw error; 59 | } 60 | }) 61 | ); 62 | 63 | server.tool( 64 | 'update_assistant', 65 | 'Updates an existing Vapi assistant', 66 | UpdateAssistantInputSchema.shape, 67 | createToolHandler(async (data) => { 68 | const assistantId = data.assistantId; 69 | try { 70 | // First check if the assistant exists 71 | const existingAssistant = await vapiClient.assistants.get(assistantId); 72 | if (!existingAssistant) { 73 | throw new Error(`Assistant with ID ${assistantId} not found`); 74 | } 75 | 76 | // Transform the update data 77 | const updateAssistantDto = transformUpdateAssistantInput(data); 78 | 79 | // Update the assistant 80 | const updatedAssistant = await vapiClient.assistants.update( 81 | assistantId, 82 | updateAssistantDto 83 | ); 84 | 85 | return transformAssistantOutput(updatedAssistant); 86 | } catch (error: any) { 87 | console.error(`Error updating assistant: ${error.message}`); 88 | throw error; 89 | } 90 | }) 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /src/tools/call.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { VapiClient, Vapi } from '@vapi-ai/server-sdk'; 3 | 4 | import { CallInputSchema, GetCallInputSchema } from '../schemas/index.js'; 5 | import { 6 | transformCallInput, 7 | transformCallOutput, 8 | } from '../transformers/index.js'; 9 | import { createToolHandler } from './utils.js'; 10 | 11 | export const registerCallTools = ( 12 | server: McpServer, 13 | vapiClient: VapiClient 14 | ) => { 15 | server.tool( 16 | 'list_calls', 17 | 'Lists all Vapi calls', 18 | {}, 19 | createToolHandler(async () => { 20 | const calls = await vapiClient.calls.list({ limit: 10 }); 21 | return calls.map(transformCallOutput); 22 | }) 23 | ); 24 | 25 | server.tool( 26 | 'create_call', 27 | 'Creates a outbound call', 28 | CallInputSchema.shape, 29 | createToolHandler(async (data) => { 30 | const createCallDto = transformCallInput(data); 31 | const call = await vapiClient.calls.create(createCallDto); 32 | return transformCallOutput(call as unknown as Vapi.Call); 33 | }) 34 | ); 35 | 36 | server.tool( 37 | 'get_call', 38 | 'Gets details of a specific call', 39 | GetCallInputSchema.shape, 40 | createToolHandler(async (data) => { 41 | const call = await vapiClient.calls.get(data.callId); 42 | return transformCallOutput(call); 43 | }) 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { VapiClient } from '@vapi-ai/server-sdk'; 3 | 4 | import { registerAssistantTools } from './assistant.js'; 5 | import { registerCallTools } from './call.js'; 6 | import { registerPhoneNumberTools } from './phone-number.js'; 7 | import { registerToolTools } from './tool.js'; 8 | 9 | export const registerAllTools = (server: McpServer, vapiClient: VapiClient) => { 10 | registerAssistantTools(server, vapiClient); 11 | registerCallTools(server, vapiClient); 12 | registerPhoneNumberTools(server, vapiClient); 13 | registerToolTools(server, vapiClient); 14 | }; 15 | -------------------------------------------------------------------------------- /src/tools/phone-number.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { VapiClient } from '@vapi-ai/server-sdk'; 3 | 4 | import { transformPhoneNumberOutput } from '../transformers/index.js'; 5 | import { createToolHandler } from './utils.js'; 6 | import { GetPhoneNumberInputSchema } from '../schemas/index.js'; 7 | 8 | export const registerPhoneNumberTools = ( 9 | server: McpServer, 10 | vapiClient: VapiClient 11 | ) => { 12 | server.tool( 13 | 'list_phone_numbers', 14 | 'Lists all Vapi phone numbers', 15 | {}, 16 | createToolHandler(async () => { 17 | const phoneNumbers = await vapiClient.phoneNumbers.list({ limit: 10 }); 18 | return phoneNumbers.map(transformPhoneNumberOutput); 19 | }) 20 | ); 21 | 22 | server.tool( 23 | 'get_phone_number', 24 | 'Gets details of a specific phone number', 25 | GetPhoneNumberInputSchema.shape, 26 | createToolHandler(async (data) => { 27 | const phoneNumberId = data.phoneNumberId; 28 | const phoneNumber = await vapiClient.phoneNumbers.get(phoneNumberId); 29 | return transformPhoneNumberOutput(phoneNumber); 30 | }) 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/tools/tool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { VapiClient, Vapi } from '@vapi-ai/server-sdk'; 3 | 4 | import { GetToolInputSchema } from '../schemas/index.js'; 5 | import { transformToolOutput } from '../transformers/index.js'; 6 | import { createToolHandler } from './utils.js'; 7 | 8 | export const registerToolTools = ( 9 | server: McpServer, 10 | vapiClient: VapiClient 11 | ) => { 12 | server.tool( 13 | 'list_tools', 14 | 'Lists all Vapi tools', 15 | {}, 16 | createToolHandler(async () => { 17 | const tools = await vapiClient.tools.list({ limit: 10 }); 18 | return tools.map(transformToolOutput); 19 | }) 20 | ); 21 | 22 | server.tool( 23 | 'get_tool', 24 | 'Gets details of a specific tool', 25 | GetToolInputSchema.shape, 26 | createToolHandler(async (data) => { 27 | const tool = await vapiClient.tools.get(data.toolId); 28 | return transformToolOutput(tool); 29 | }) 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/tools/utils.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export type ToolResponse = { 4 | content: Array<{ type: 'text'; text: string }>; 5 | }; 6 | 7 | export function createSuccessResponse(data: any): ToolResponse { 8 | return { 9 | content: [ 10 | { 11 | type: 'text' as const, 12 | text: typeof data === 'string' ? data : JSON.stringify(data), 13 | }, 14 | ], 15 | }; 16 | } 17 | 18 | export function createErrorResponse(error: any): ToolResponse { 19 | const errorMessage = error?.message || String(error); 20 | return { 21 | content: [ 22 | { 23 | type: 'text' as const, 24 | text: `Error: ${errorMessage}`, 25 | }, 26 | ], 27 | }; 28 | } 29 | 30 | export function createToolHandler( 31 | handler: (params: T) => Promise 32 | ): (params: T) => Promise { 33 | return async (params: T) => { 34 | try { 35 | const result = await handler(params); 36 | return createSuccessResponse(result); 37 | } catch (error) { 38 | return createErrorResponse(error); 39 | } 40 | }; 41 | } 42 | 43 | export function createParamsSchema(schema: z.ZodType): any { 44 | return { 45 | properties: { 46 | params: (schema as any).shape || {}, 47 | }, 48 | required: ['params'], 49 | }; 50 | } 51 | 52 | export function createIdParamSchema( 53 | paramName: string, 54 | description: string 55 | ): any { 56 | return { 57 | properties: { 58 | [paramName]: { type: 'string', description }, 59 | }, 60 | required: [paramName], 61 | }; 62 | } 63 | 64 | export const withErrorHandling = ( 65 | handler: () => Promise 66 | ): Promise => { 67 | return handler() 68 | .then((result) => createSuccessResponse(result)) 69 | .catch((error) => createErrorResponse(error)); 70 | }; 71 | 72 | export const filterResponseWithSchema = ( 73 | data: any, 74 | schema: z.ZodType 75 | ): T => { 76 | if (Array.isArray(data)) { 77 | return data.map((item) => schema.parse(item)) as unknown as T; 78 | } 79 | return schema.parse(data); 80 | }; 81 | 82 | export const withSchemaFiltering = ( 83 | handler: () => Promise, 84 | schema: z.ZodType 85 | ): Promise => { 86 | return handler() 87 | .then((result) => { 88 | const filtered = filterResponseWithSchema(result, schema); 89 | return createSuccessResponse(filtered); 90 | }) 91 | .catch((error) => createErrorResponse(error)); 92 | }; 93 | -------------------------------------------------------------------------------- /src/transformers/index.ts: -------------------------------------------------------------------------------- 1 | import { Vapi } from '@vapi-ai/server-sdk'; 2 | import { z } from 'zod'; 3 | import { 4 | CreateAssistantInputSchema, 5 | CallInputSchema, 6 | AssistantOutputSchema, 7 | CallOutputSchema, 8 | PhoneNumberOutputSchema, 9 | ToolOutputSchema, 10 | UpdateAssistantInputSchema, 11 | } from '../schemas/index.js'; 12 | 13 | // ===== Assistant Transformers ===== 14 | 15 | export function transformAssistantInput( 16 | input: z.infer 17 | ): Vapi.CreateAssistantDto { 18 | const assistantDto: any = { 19 | name: input.name, 20 | }; 21 | 22 | assistantDto.model = { 23 | provider: input.llm.provider as any, 24 | model: input.llm.model, 25 | }; 26 | 27 | if (input.toolIds && input.toolIds.length > 0) { 28 | assistantDto.model.toolIds = input.toolIds; 29 | } 30 | 31 | if (input.instructions) { 32 | assistantDto.model.messages = [ 33 | { 34 | role: 'system', 35 | content: input.instructions, 36 | }, 37 | ]; 38 | } 39 | 40 | assistantDto.transcriber = { 41 | provider: input.transcriber.provider, 42 | ...(input.transcriber.model ? { model: input.transcriber.model } : {}), 43 | }; 44 | 45 | assistantDto.voice = { 46 | provider: input.voice.provider as any, 47 | voiceId: input.voice.voiceId, 48 | ...(input.voice.model ? { model: input.voice.model } : {}), 49 | }; 50 | 51 | if (input.firstMessage) { 52 | assistantDto.firstMessage = input.firstMessage; 53 | } 54 | 55 | if (input.firstMessageMode) { 56 | assistantDto.firstMessageMode = input.firstMessageMode; 57 | } 58 | 59 | return assistantDto as Vapi.CreateAssistantDto; 60 | } 61 | 62 | export function transformUpdateAssistantInput( 63 | input: z.infer 64 | ): Vapi.UpdateAssistantDto { 65 | const updateDto: any = {}; 66 | 67 | if (input.name) { 68 | updateDto.name = input.name; 69 | } 70 | 71 | if (input.llm) { 72 | updateDto.model = { 73 | provider: input.llm.provider as any, 74 | model: input.llm.model, 75 | }; 76 | 77 | if (input.toolIds && input.toolIds.length > 0) { 78 | updateDto.model.toolIds = input.toolIds; 79 | } 80 | 81 | if (input.instructions) { 82 | updateDto.model.messages = [ 83 | { 84 | role: 'system', 85 | content: input.instructions, 86 | }, 87 | ]; 88 | } 89 | } else { 90 | if (input.toolIds && input.toolIds.length > 0) { 91 | updateDto.model = { toolIds: input.toolIds }; 92 | } 93 | 94 | if (input.instructions) { 95 | if (!updateDto.model) updateDto.model = {}; 96 | updateDto.model.messages = [ 97 | { 98 | role: 'system', 99 | content: input.instructions, 100 | }, 101 | ]; 102 | } 103 | } 104 | 105 | if (input.transcriber) { 106 | updateDto.transcriber = { 107 | provider: input.transcriber.provider, 108 | ...(input.transcriber.model ? { model: input.transcriber.model } : {}), 109 | }; 110 | } 111 | 112 | if (input.voice) { 113 | updateDto.voice = { 114 | provider: input.voice.provider as any, 115 | voiceId: input.voice.voiceId, 116 | ...(input.voice.model ? { model: input.voice.model } : {}), 117 | }; 118 | } 119 | 120 | if (input.firstMessage) { 121 | updateDto.firstMessage = input.firstMessage; 122 | } 123 | 124 | if (input.firstMessageMode) { 125 | updateDto.firstMessageMode = input.firstMessageMode; 126 | } 127 | 128 | return updateDto as Vapi.UpdateAssistantDto; 129 | } 130 | 131 | export function transformAssistantOutput( 132 | assistant: Vapi.Assistant 133 | ): z.infer { 134 | return { 135 | id: assistant.id, 136 | createdAt: assistant.createdAt, 137 | updatedAt: assistant.updatedAt, 138 | name: assistant.name || 'Vapi Assistant', 139 | llm: { 140 | provider: assistant.model?.provider || 'openai', 141 | model: assistant.model?.model || 'gpt-4o-mini', 142 | }, 143 | voice: { 144 | provider: assistant.voice?.provider || '11labs', 145 | voiceId: getAssistantVoiceId(assistant.voice), 146 | model: getAssistantVoiceModel(assistant.voice) || 'eleven_turbo_v2_5', 147 | }, 148 | transcriber: { 149 | provider: assistant.transcriber?.provider || 'deepgram', 150 | model: getAssistantTranscriberModel(assistant.transcriber) || 'nova-3', 151 | }, 152 | toolIds: assistant.model?.toolIds || [], 153 | }; 154 | } 155 | 156 | function getAssistantVoiceId(voice?: Vapi.AssistantVoice): string { 157 | if (!voice) return ''; 158 | 159 | const voiceAny = voice as any; 160 | return voiceAny.voiceId || voiceAny.voice || ''; 161 | } 162 | 163 | function getAssistantVoiceModel(voice?: Vapi.AssistantVoice): string { 164 | if (!voice) return ''; 165 | 166 | const voiceAny = voice as any; 167 | return voiceAny.model || ''; 168 | } 169 | 170 | function getAssistantTranscriberModel( 171 | transcriber?: Vapi.AssistantTranscriber 172 | ): string { 173 | if (!transcriber) return ''; 174 | 175 | const transcriberAny = transcriber as any; 176 | return transcriberAny.model || transcriberAny.transcriber || ''; 177 | } 178 | 179 | // ===== Call Transformers ===== 180 | 181 | export function transformCallInput( 182 | input: z.infer 183 | ): Vapi.CreateCallDto { 184 | return { 185 | ...(input.assistantId ? { assistantId: input.assistantId } : {}), 186 | ...(input.phoneNumberId ? { phoneNumberId: input.phoneNumberId } : {}), 187 | ...(input.customer 188 | ? { 189 | customer: { 190 | number: input.customer.phoneNumber, 191 | }, 192 | } 193 | : {}), 194 | ...(input.scheduledAt 195 | ? { 196 | schedulePlan: { 197 | earliestAt: input.scheduledAt, 198 | }, 199 | } 200 | : {}), 201 | }; 202 | } 203 | 204 | export function transformCallOutput( 205 | call: Vapi.Call 206 | ): z.infer { 207 | return { 208 | id: call.id, 209 | createdAt: call.createdAt, 210 | updatedAt: call.updatedAt, 211 | status: call.status || '', 212 | endedReason: call.endedReason, 213 | assistantId: call.assistantId, 214 | phoneNumberId: call.phoneNumberId, 215 | customer: call.customer 216 | ? { 217 | phoneNumber: call.customer.number || '', 218 | } 219 | : undefined, 220 | scheduledAt: call.schedulePlan?.earliestAt, 221 | }; 222 | } 223 | 224 | // ===== Phone Number Transformers ===== 225 | 226 | export function transformPhoneNumberOutput( 227 | phoneNumber: any 228 | ): z.infer { 229 | return { 230 | id: phoneNumber.id, 231 | name: phoneNumber.name, 232 | createdAt: phoneNumber.createdAt, 233 | updatedAt: phoneNumber.updatedAt, 234 | phoneNumber: phoneNumber.number, 235 | status: phoneNumber.status, 236 | }; 237 | } 238 | 239 | // ===== Tool Transformers ===== 240 | 241 | export function transformToolOutput( 242 | tool: any 243 | ): z.infer { 244 | return { 245 | id: tool.id, 246 | createdAt: tool.createdAt, 247 | updatedAt: tool.updatedAt, 248 | type: tool.type || '', 249 | name: tool.function?.name || '', 250 | description: tool.function?.description || '', 251 | }; 252 | } 253 | -------------------------------------------------------------------------------- /src/utils/response.ts: -------------------------------------------------------------------------------- 1 | export function parseToolResponse(response: any): any { 2 | // If no response or it's a primitive, return as is 3 | if (!response || typeof response !== 'object') return response; 4 | 5 | // Check for content array with text items 6 | if (response.content && Array.isArray(response.content)) { 7 | const textContent = response.content.find( 8 | (item: { type: string; text?: string }) => item.type === 'text' 9 | )?.text; 10 | 11 | if (textContent) { 12 | // Check if it's an error message first - only if it starts with 'Error:' 13 | if (textContent.startsWith('Error:')) { 14 | return { error: textContent }; 15 | } 16 | 17 | try { 18 | return JSON.parse(textContent); 19 | } catch (e) { 20 | // Don't warn on expected error messages 21 | if (!textContent.startsWith('Error:')) { 22 | console.warn('Failed to parse text content as JSON:', e); 23 | } 24 | return textContent; 25 | } 26 | } 27 | } 28 | 29 | return response; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "declaration": true, 13 | "declarationMap": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | --------------------------------------------------------------------------------