@vapi-ai/mcp-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/dist/__tests__/mcp-server-e2e.test.js +239 -0
- package/dist/__tests__/mcp-server-mock.test.js +34 -0
- package/dist/__tests__/mocks/vapi-client.mock.js +43 -0
- package/dist/client.js +7 -0
- package/dist/index.js +47 -0
- package/dist/schemas/index.js +213 -0
- package/dist/tools/assistant.js +32 -0
- package/dist/tools/call.js +18 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/phone-number.js +14 -0
- package/dist/tools/utils.js +67 -0
- package/dist/transformers/index.js +113 -0
- package/dist/utils/response.js +26 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Vapi MCP Server
|
|
2
|
+
|
|
3
|
+
The Vapi [Model Context Protocol](https://modelcontextprotocol.com/) server allows you to integrate with Vapi APIs through function calling.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
To run the Vapi MCP server using npx, use the following command:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# To set up all available tools
|
|
11
|
+
export VAPI_TOKEN=your_vapi_token
|
|
12
|
+
|
|
13
|
+
npx -y @vapi-ai/mcp-server
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Usage with Claude Desktop
|
|
17
|
+
|
|
18
|
+
Add the following to your `claude_desktop_config.json`. See [here](https://modelcontextprotocol.io/quickstart/user) for more details.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
|
|
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": "<your_vapi_token>"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Install dependencies
|
|
43
|
+
npm install
|
|
44
|
+
|
|
45
|
+
# Build the server
|
|
46
|
+
npm run build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Run the server in development mode with hot reloading
|
|
51
|
+
npm run dev
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Testing
|
|
55
|
+
|
|
56
|
+
The project has two types of tests:
|
|
57
|
+
|
|
58
|
+
### Unit Tests
|
|
59
|
+
|
|
60
|
+
Unit tests use mocks to test the MCP server without making actual API calls to Vapi.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Run unit tests
|
|
64
|
+
npm run test:unit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### End-to-End Tests
|
|
68
|
+
|
|
69
|
+
E2E tests run the full MCP server with actual API calls to Vapi.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Set your Vapi API token
|
|
73
|
+
export VAPI_TOKEN=your_token_here
|
|
74
|
+
|
|
75
|
+
# Run E2E tests
|
|
76
|
+
npm run test:e2e
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Note: E2E tests require a valid Vapi API token to be set in the environment.
|
|
80
|
+
|
|
81
|
+
## Running All Tests
|
|
82
|
+
|
|
83
|
+
To run all tests at once:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm test
|
|
87
|
+
```
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
dotenv.config();
|
|
8
|
+
jest.setTimeout(15000);
|
|
9
|
+
describe('MCP Server E2E Test', () => {
|
|
10
|
+
let mcpClient;
|
|
11
|
+
let transport;
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
const serverScriptPath = join(process.cwd(), 'src', 'index.ts');
|
|
14
|
+
console.log(`Using server source at: ${serverScriptPath}`);
|
|
15
|
+
mcpClient = new Client({ name: 'vapi-e2e-test-client', version: '1.0.0' });
|
|
16
|
+
transport = new StdioClientTransport({
|
|
17
|
+
command: 'npx',
|
|
18
|
+
args: ['tsx', serverScriptPath],
|
|
19
|
+
env: {
|
|
20
|
+
...process.env,
|
|
21
|
+
VAPI_TOKEN: process.env.VAPI_TOKEN || 'test-token',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
console.log('Connecting to MCP server...');
|
|
25
|
+
await mcpClient.connect(transport);
|
|
26
|
+
console.log('Connected to server successfully');
|
|
27
|
+
});
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
console.log('Disconnecting from server...');
|
|
30
|
+
await mcpClient.close();
|
|
31
|
+
console.log('Disconnected from server');
|
|
32
|
+
});
|
|
33
|
+
test('should list available tools', async () => {
|
|
34
|
+
console.log('Requesting available tools...');
|
|
35
|
+
const toolsResult = await mcpClient.listTools();
|
|
36
|
+
expect(toolsResult.tools).toBeDefined();
|
|
37
|
+
expect(Array.isArray(toolsResult.tools)).toBe(true);
|
|
38
|
+
const toolNames = toolsResult.tools.map((tool) => tool.name);
|
|
39
|
+
console.log('Available tools:', toolNames);
|
|
40
|
+
expect(toolNames).toContain('list_assistants');
|
|
41
|
+
expect(toolNames).toContain('create_assistant');
|
|
42
|
+
expect(toolNames).toContain('get_assistant');
|
|
43
|
+
expect(toolNames).toContain('list_phone_numbers');
|
|
44
|
+
expect(toolNames).toContain('get_phone_number');
|
|
45
|
+
expect(toolNames).toContain('list_calls');
|
|
46
|
+
expect(toolNames).toContain('create_call');
|
|
47
|
+
expect(toolNames).toContain('get_call');
|
|
48
|
+
});
|
|
49
|
+
describe('Assistant Tools', () => {
|
|
50
|
+
test('should list all assistants', async () => {
|
|
51
|
+
console.log('Calling list_assistants tool...');
|
|
52
|
+
const rawResult = await mcpClient.callTool({
|
|
53
|
+
name: 'list_assistants',
|
|
54
|
+
arguments: {},
|
|
55
|
+
});
|
|
56
|
+
const result = parseToolResponse(rawResult);
|
|
57
|
+
expect(result).toBeDefined();
|
|
58
|
+
if (Array.isArray(result)) {
|
|
59
|
+
console.log(`Received ${result.length} assistants in array form`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.log(`Received assistants in non-array form:`, result);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
test('should create a new assistant', async () => {
|
|
66
|
+
console.log('Calling create_assistant tool...');
|
|
67
|
+
const assistantData = {
|
|
68
|
+
name: `Test Assistant ${Date.now()}`,
|
|
69
|
+
instructions: 'You are a helpful test assistant',
|
|
70
|
+
llm: {
|
|
71
|
+
provider: 'openai',
|
|
72
|
+
model: 'gpt-4o',
|
|
73
|
+
},
|
|
74
|
+
transcriber: {
|
|
75
|
+
provider: 'deepgram',
|
|
76
|
+
model: 'nova-3',
|
|
77
|
+
},
|
|
78
|
+
voice: {
|
|
79
|
+
provider: '11labs',
|
|
80
|
+
model: 'eleven_turbo_v2_5',
|
|
81
|
+
voiceId: 'sarah',
|
|
82
|
+
},
|
|
83
|
+
firstMessage: "Hello, I'm your vapi assistant.",
|
|
84
|
+
};
|
|
85
|
+
const rawResult = await mcpClient.callTool({
|
|
86
|
+
name: 'create_assistant',
|
|
87
|
+
arguments: assistantData,
|
|
88
|
+
});
|
|
89
|
+
const result = parseToolResponse(rawResult);
|
|
90
|
+
expect(result).toBeDefined();
|
|
91
|
+
expect(result.id).toBeDefined();
|
|
92
|
+
expect(result.name).toBe(assistantData.name);
|
|
93
|
+
expect(result.llm.provider).toBe(assistantData.llm.provider);
|
|
94
|
+
expect(result.llm.model).toBe(assistantData.llm.model);
|
|
95
|
+
console.log(`Successfully created assistant with ID: ${result.id}`);
|
|
96
|
+
});
|
|
97
|
+
test('should get an assistant by ID', async () => {
|
|
98
|
+
console.log('Listing assistants to get the first one...');
|
|
99
|
+
const rawListResult = await mcpClient.callTool({
|
|
100
|
+
name: 'list_assistants',
|
|
101
|
+
arguments: {},
|
|
102
|
+
});
|
|
103
|
+
const listResult = parseToolResponse(rawListResult);
|
|
104
|
+
expect(listResult).toBeDefined();
|
|
105
|
+
expect(Array.isArray(listResult)).toBe(true);
|
|
106
|
+
if (listResult.length === 0) {
|
|
107
|
+
console.log('No assistants found, skipping get_assistant test.');
|
|
108
|
+
// Optionally, you could fail the test here or create an assistant first
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const firstAssistant = listResult[0];
|
|
112
|
+
expect(firstAssistant.id).toBeDefined();
|
|
113
|
+
const assistantId = firstAssistant.id;
|
|
114
|
+
const assistantName = firstAssistant.name;
|
|
115
|
+
console.log(`Attempting to fetch assistant with ID: ${assistantId} (Name: ${assistantName})`);
|
|
116
|
+
const rawGetResult = await mcpClient.callTool({
|
|
117
|
+
name: 'get_assistant',
|
|
118
|
+
arguments: { assistantId: assistantId },
|
|
119
|
+
});
|
|
120
|
+
const getResult = parseToolResponse(rawGetResult);
|
|
121
|
+
expect(getResult).toBeDefined();
|
|
122
|
+
expect(getResult.id).toBe(assistantId);
|
|
123
|
+
expect(getResult.name).toBe(assistantName); // Verify name matches too
|
|
124
|
+
console.log(`Successfully fetched assistant with ID: ${getResult.id}`);
|
|
125
|
+
});
|
|
126
|
+
test('should handle invalid assistant ID', async () => {
|
|
127
|
+
const invalidId = 'non-existent-assistant-id-' + Date.now();
|
|
128
|
+
console.log(`Testing error handling with invalid assistant ID: ${invalidId}`);
|
|
129
|
+
try {
|
|
130
|
+
const rawResult = await mcpClient.callTool({
|
|
131
|
+
name: 'get_assistant',
|
|
132
|
+
arguments: { assistantId: invalidId },
|
|
133
|
+
});
|
|
134
|
+
const result = parseToolResponse(rawResult);
|
|
135
|
+
// Check for error in the result
|
|
136
|
+
if (result.error ||
|
|
137
|
+
(typeof result === 'string' &&
|
|
138
|
+
(result.includes('Error') || result.includes('error')))) {
|
|
139
|
+
// Test passed - we got an error response
|
|
140
|
+
expect(true).toBe(true);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// If we somehow got a successful result, fail the test
|
|
144
|
+
expect(result).toContain('error');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
// This is also acceptable - the API might throw instead of returning error
|
|
149
|
+
expect(error).toBeDefined();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('Phone Number Tools', () => {
|
|
154
|
+
test('should list all phone numbers', async () => {
|
|
155
|
+
console.log('Calling list_phone_numbers tool...');
|
|
156
|
+
const rawResult = await mcpClient.callTool({
|
|
157
|
+
name: 'list_phone_numbers',
|
|
158
|
+
arguments: {},
|
|
159
|
+
});
|
|
160
|
+
const result = parseToolResponse(rawResult);
|
|
161
|
+
expect(result).toBeDefined();
|
|
162
|
+
if (Array.isArray(result)) {
|
|
163
|
+
console.log(`Received ${result.length} phone numbers in array form`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.log(`Received phone numbers in non-array form:`, result);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
test('should handle invalid phone number ID', async () => {
|
|
170
|
+
const invalidId = 'non-existent-phone-number-id-' + Date.now();
|
|
171
|
+
console.log(`Testing error handling with invalid phone number ID: ${invalidId}`);
|
|
172
|
+
try {
|
|
173
|
+
const rawResult = await mcpClient.callTool({
|
|
174
|
+
name: 'get_phone_number',
|
|
175
|
+
arguments: { phoneNumberId: invalidId },
|
|
176
|
+
});
|
|
177
|
+
const result = parseToolResponse(rawResult);
|
|
178
|
+
// Check for error in the result
|
|
179
|
+
if (result.error ||
|
|
180
|
+
(typeof result === 'string' &&
|
|
181
|
+
(result.includes('Error') || result.includes('error')))) {
|
|
182
|
+
// Test passed - we got an error response
|
|
183
|
+
expect(true).toBe(true);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// If we somehow got a successful result, fail the test
|
|
187
|
+
expect(result).toContain('error');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
// This is also acceptable - the API might throw instead of returning error
|
|
192
|
+
expect(error).toBeDefined();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('Call Tools', () => {
|
|
197
|
+
test('should list all calls', async () => {
|
|
198
|
+
console.log('Calling list_calls tool...');
|
|
199
|
+
const rawResult = await mcpClient.callTool({
|
|
200
|
+
name: 'list_calls',
|
|
201
|
+
arguments: {},
|
|
202
|
+
});
|
|
203
|
+
const result = parseToolResponse(rawResult);
|
|
204
|
+
expect(result).toBeDefined();
|
|
205
|
+
if (Array.isArray(result)) {
|
|
206
|
+
console.log(`Received ${result.length} calls in array form`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.log(`Received calls in non-array form:`, result);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
test('should handle invalid call ID', async () => {
|
|
213
|
+
const invalidId = 'non-existent-call-id-' + Date.now();
|
|
214
|
+
console.log(`Testing error handling with invalid call ID: ${invalidId}`);
|
|
215
|
+
try {
|
|
216
|
+
const rawResult = await mcpClient.callTool({
|
|
217
|
+
name: 'get_call',
|
|
218
|
+
arguments: { callId: invalidId },
|
|
219
|
+
});
|
|
220
|
+
const result = parseToolResponse(rawResult);
|
|
221
|
+
// Check for error in the result
|
|
222
|
+
if (result.error ||
|
|
223
|
+
(typeof result === 'string' &&
|
|
224
|
+
(result.includes('Error') || result.includes('error')))) {
|
|
225
|
+
// Test passed - we got an error response
|
|
226
|
+
expect(true).toBe(true);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// If we somehow got a successful result, fail the test
|
|
230
|
+
expect(result).toContain('error');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
// This is also acceptable - the API might throw instead of returning error
|
|
235
|
+
expect(error).toBeDefined();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { createMcpServer } from '../index.js';
|
|
3
|
+
import { createMockVapiClient } from './mocks/vapi-client.mock.js';
|
|
4
|
+
jest.mock('../client.js', () => ({
|
|
5
|
+
createVapiClient: jest.fn(() => createMockVapiClient()),
|
|
6
|
+
}));
|
|
7
|
+
process.env.VAPI_TOKEN = 'test-mock-token';
|
|
8
|
+
describe('MCP Server Unit Test (with mocks)', () => {
|
|
9
|
+
let mcpServer;
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
console.log('Creating MCP server with mocked Vapi client...');
|
|
12
|
+
mcpServer = createMcpServer();
|
|
13
|
+
console.log('MCP server created successfully');
|
|
14
|
+
});
|
|
15
|
+
test('server should be initialized correctly', () => {
|
|
16
|
+
expect(mcpServer).toBeDefined();
|
|
17
|
+
expect(typeof mcpServer).toBe('object');
|
|
18
|
+
expect(mcpServer.constructor.name).toBe('McpServer');
|
|
19
|
+
expect(mcpServer.connect).toBeDefined();
|
|
20
|
+
expect(typeof mcpServer.connect).toBe('function');
|
|
21
|
+
});
|
|
22
|
+
test('server should have required internal structures', () => {
|
|
23
|
+
expect(mcpServer._registeredTools).toBeDefined();
|
|
24
|
+
expect(typeof mcpServer._registeredTools).toBe('object');
|
|
25
|
+
});
|
|
26
|
+
test('server should have registered Vapi tools', () => {
|
|
27
|
+
const registeredTools = Object.keys(mcpServer._registeredTools);
|
|
28
|
+
expect(registeredTools.length).toBeGreaterThan(0);
|
|
29
|
+
console.log('Found registered tools:', registeredTools);
|
|
30
|
+
expect(registeredTools).toContain('list_assistants');
|
|
31
|
+
expect(registeredTools).toContain('create_assistant');
|
|
32
|
+
expect(registeredTools).toContain('get_assistant');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
get: jest.fn().mockImplementation((id) => {
|
|
22
|
+
return Promise.resolve({
|
|
23
|
+
id,
|
|
24
|
+
name: `Mock Assistant ${id}`,
|
|
25
|
+
model: 'gpt-4',
|
|
26
|
+
instructions: 'Example instructions',
|
|
27
|
+
createdAt: new Date().toISOString(),
|
|
28
|
+
updatedAt: new Date().toISOString(),
|
|
29
|
+
});
|
|
30
|
+
}),
|
|
31
|
+
create: jest.fn().mockImplementation((data) => {
|
|
32
|
+
return Promise.resolve({
|
|
33
|
+
id: 'new-mock-assistant-id',
|
|
34
|
+
...data,
|
|
35
|
+
createdAt: new Date().toISOString(),
|
|
36
|
+
updatedAt: new Date().toISOString(),
|
|
37
|
+
});
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export const createMockVapiClient = () => {
|
|
42
|
+
return new MockVapiClient();
|
|
43
|
+
};
|
package/dist/client.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { registerAllTools } from './tools/index.js';
|
|
5
|
+
import { createVapiClient } from './client.js';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
dotenv.config();
|
|
8
|
+
function createMcpServer() {
|
|
9
|
+
const vapiToken = process.env.VAPI_TOKEN;
|
|
10
|
+
if (!vapiToken) {
|
|
11
|
+
throw new Error('VAPI_TOKEN environment variable is required');
|
|
12
|
+
}
|
|
13
|
+
const vapiClient = createVapiClient(vapiToken);
|
|
14
|
+
const mcpServer = new McpServer({
|
|
15
|
+
name: 'Vapi MCP',
|
|
16
|
+
version: '0.1.0',
|
|
17
|
+
capabilities: [],
|
|
18
|
+
});
|
|
19
|
+
registerAllTools(mcpServer, vapiClient);
|
|
20
|
+
return mcpServer;
|
|
21
|
+
}
|
|
22
|
+
async function main() {
|
|
23
|
+
try {
|
|
24
|
+
const mcpServer = createMcpServer();
|
|
25
|
+
const transport = new StdioServerTransport();
|
|
26
|
+
await mcpServer.connect(transport);
|
|
27
|
+
setupShutdownHandler(mcpServer);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function setupShutdownHandler(mcpServer) {
|
|
34
|
+
process.on('SIGINT', async () => {
|
|
35
|
+
try {
|
|
36
|
+
await mcpServer.close();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
main().catch((err) => {
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
export { createMcpServer };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// ===== Model Provider and Models =====
|
|
3
|
+
export const ModelProvider = {
|
|
4
|
+
OpenAI: 'openai',
|
|
5
|
+
Anthropic: 'anthropic',
|
|
6
|
+
GoogleAI: 'google',
|
|
7
|
+
};
|
|
8
|
+
export const OpenAIModels = {
|
|
9
|
+
GPT4o: 'gpt-4o',
|
|
10
|
+
GPT4oMini: 'gpt-4o-mini',
|
|
11
|
+
};
|
|
12
|
+
export const AnthropicModels = {
|
|
13
|
+
Claude3Sonnet: 'claude-3-7-sonnet-20250219',
|
|
14
|
+
Claude3Haiku: 'claude-3-5-haiku-20241022',
|
|
15
|
+
};
|
|
16
|
+
export const GoogleModels = {
|
|
17
|
+
GeminiPro: 'gemini-1.5-pro',
|
|
18
|
+
GeminiFlash: 'gemini-1.5-flash',
|
|
19
|
+
Gemini2Flash: 'gemini-2.0-flash',
|
|
20
|
+
Gemini2Pro: 'gemini-2.0-pro',
|
|
21
|
+
};
|
|
22
|
+
const OpenAILLMSchema = z.object({
|
|
23
|
+
provider: z.literal(ModelProvider.OpenAI),
|
|
24
|
+
model: z.enum([OpenAIModels.GPT4o, OpenAIModels.GPT4oMini]),
|
|
25
|
+
});
|
|
26
|
+
const AnthropicLLMSchema = z.object({
|
|
27
|
+
provider: z.literal(ModelProvider.Anthropic),
|
|
28
|
+
model: z.enum([
|
|
29
|
+
AnthropicModels.Claude3Sonnet,
|
|
30
|
+
AnthropicModels.Claude3Haiku,
|
|
31
|
+
]),
|
|
32
|
+
});
|
|
33
|
+
const GoogleLLMSchema = z.object({
|
|
34
|
+
provider: z.literal(ModelProvider.GoogleAI),
|
|
35
|
+
model: z.enum([
|
|
36
|
+
GoogleModels.GeminiPro,
|
|
37
|
+
GoogleModels.GeminiFlash,
|
|
38
|
+
GoogleModels.Gemini2Flash,
|
|
39
|
+
GoogleModels.Gemini2Pro,
|
|
40
|
+
]),
|
|
41
|
+
});
|
|
42
|
+
const GenericLLMSchema = z.object({
|
|
43
|
+
provider: z.string(),
|
|
44
|
+
model: z.string(),
|
|
45
|
+
});
|
|
46
|
+
const LLMSchema = z.union([
|
|
47
|
+
OpenAILLMSchema,
|
|
48
|
+
AnthropicLLMSchema,
|
|
49
|
+
GoogleLLMSchema,
|
|
50
|
+
GenericLLMSchema,
|
|
51
|
+
]);
|
|
52
|
+
export const DEFAULT_LLM = {
|
|
53
|
+
provider: ModelProvider.OpenAI,
|
|
54
|
+
model: OpenAIModels.GPT4o,
|
|
55
|
+
};
|
|
56
|
+
const VoiceProviderSchema = z.enum([
|
|
57
|
+
'vapi',
|
|
58
|
+
'11labs',
|
|
59
|
+
'azure',
|
|
60
|
+
'cartesia',
|
|
61
|
+
'custom-voice',
|
|
62
|
+
'deepgram',
|
|
63
|
+
'11labs',
|
|
64
|
+
'hume',
|
|
65
|
+
'lmnt',
|
|
66
|
+
'neuphonic',
|
|
67
|
+
'openai',
|
|
68
|
+
'playht',
|
|
69
|
+
'rime-ai',
|
|
70
|
+
'smallest-ai',
|
|
71
|
+
'tavus',
|
|
72
|
+
'sesame',
|
|
73
|
+
]);
|
|
74
|
+
export const DEFAULT_VOICE = {
|
|
75
|
+
provider: '11labs',
|
|
76
|
+
voiceId: 'sarah',
|
|
77
|
+
};
|
|
78
|
+
export const DEFAULT_TRANSCRIBER = {
|
|
79
|
+
provider: 'deepgram',
|
|
80
|
+
model: 'nova-3',
|
|
81
|
+
};
|
|
82
|
+
export const ElevenLabsVoiceIds = {
|
|
83
|
+
Sarah: 'sarah',
|
|
84
|
+
Phillip: 'phillip',
|
|
85
|
+
Steve: 'steve',
|
|
86
|
+
Joseph: 'joseph',
|
|
87
|
+
Myra: 'myra',
|
|
88
|
+
};
|
|
89
|
+
// ===== Common Schemas =====
|
|
90
|
+
export const BaseResponseSchema = z.object({
|
|
91
|
+
id: z.string(),
|
|
92
|
+
createdAt: z.string(),
|
|
93
|
+
updatedAt: z.string(),
|
|
94
|
+
});
|
|
95
|
+
// ===== Assistant Schemas =====
|
|
96
|
+
export const CreateAssistantInputSchema = z.object({
|
|
97
|
+
name: z.string().describe('Name of the assistant'),
|
|
98
|
+
instructions: z
|
|
99
|
+
.string()
|
|
100
|
+
.optional()
|
|
101
|
+
.default('You are a helpful assistant.')
|
|
102
|
+
.describe('Instructions for the assistant'),
|
|
103
|
+
llm: z
|
|
104
|
+
.union([
|
|
105
|
+
LLMSchema,
|
|
106
|
+
z.string().transform((str) => {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(str);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
throw new Error(`Invalid LLM JSON string: ${str}`);
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
])
|
|
115
|
+
.default(DEFAULT_LLM)
|
|
116
|
+
.describe('LLM configuration'),
|
|
117
|
+
transcriber: z
|
|
118
|
+
.object({
|
|
119
|
+
provider: z.string().describe('Provider to use for transcription'),
|
|
120
|
+
model: z.string().describe('Transcription model to use'),
|
|
121
|
+
})
|
|
122
|
+
.default(DEFAULT_TRANSCRIBER)
|
|
123
|
+
.describe('Transcription configuration'),
|
|
124
|
+
voice: z
|
|
125
|
+
.object({
|
|
126
|
+
provider: VoiceProviderSchema.describe('Provider to use for voice'),
|
|
127
|
+
voiceId: z.string().describe('Voice ID to use'),
|
|
128
|
+
model: z.string().optional().describe('Voice model to use'),
|
|
129
|
+
})
|
|
130
|
+
.default(DEFAULT_VOICE)
|
|
131
|
+
.describe('Voice configuration'),
|
|
132
|
+
firstMessage: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.default('Hello, how can I help you today?')
|
|
136
|
+
.describe('First message to send'),
|
|
137
|
+
firstMessageMode: z
|
|
138
|
+
.enum([
|
|
139
|
+
'assistant-speaks-first',
|
|
140
|
+
'assistant-waits-for-user',
|
|
141
|
+
'assistant-speaks-first-with-model-generated-message',
|
|
142
|
+
])
|
|
143
|
+
.default('assistant-speaks-first')
|
|
144
|
+
.optional()
|
|
145
|
+
.describe('First message mode'),
|
|
146
|
+
});
|
|
147
|
+
export const AssistantOutputSchema = BaseResponseSchema.extend({
|
|
148
|
+
name: z.string(),
|
|
149
|
+
llm: z.object({
|
|
150
|
+
provider: z.string(),
|
|
151
|
+
model: z.string(),
|
|
152
|
+
}),
|
|
153
|
+
voice: z.object({
|
|
154
|
+
provider: z.string(),
|
|
155
|
+
voiceId: z.string(),
|
|
156
|
+
model: z.string().optional(),
|
|
157
|
+
}),
|
|
158
|
+
transcriber: z.object({
|
|
159
|
+
provider: z.string(),
|
|
160
|
+
model: z.string(),
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
export const GetAssistantInputSchema = z.object({
|
|
164
|
+
assistantId: z.string().describe('ID of the assistant to get'),
|
|
165
|
+
});
|
|
166
|
+
// ===== Call Schemas =====
|
|
167
|
+
export const CallInputSchema = z.object({
|
|
168
|
+
assistantId: z
|
|
169
|
+
.string()
|
|
170
|
+
.optional()
|
|
171
|
+
.describe('ID of the assistant to use for the call'),
|
|
172
|
+
phoneNumberId: z
|
|
173
|
+
.string()
|
|
174
|
+
.optional()
|
|
175
|
+
.describe('ID of the phone number to use for the call'),
|
|
176
|
+
customer: z
|
|
177
|
+
.object({
|
|
178
|
+
phoneNumber: z.string().describe('Customer phone number'),
|
|
179
|
+
})
|
|
180
|
+
.optional()
|
|
181
|
+
.describe('Customer information'),
|
|
182
|
+
});
|
|
183
|
+
export const CallOutputSchema = BaseResponseSchema.extend({
|
|
184
|
+
status: z.string(),
|
|
185
|
+
endedReason: z.string().optional(),
|
|
186
|
+
assistantId: z.string().optional(),
|
|
187
|
+
phoneNumberId: z.string().optional(),
|
|
188
|
+
customer: z
|
|
189
|
+
.object({
|
|
190
|
+
phoneNumber: z.string(),
|
|
191
|
+
})
|
|
192
|
+
.optional(),
|
|
193
|
+
});
|
|
194
|
+
export const GetCallInputSchema = z.object({
|
|
195
|
+
callId: z.string().describe('ID of the call to get'),
|
|
196
|
+
});
|
|
197
|
+
// ===== Phone Number Schemas =====
|
|
198
|
+
export const PhoneNumberInputSchema = z.object({
|
|
199
|
+
areaCode: z.string().describe('Area code to search for'),
|
|
200
|
+
});
|
|
201
|
+
export const PhoneNumberOutputSchema = BaseResponseSchema.extend({
|
|
202
|
+
phoneNumber: z.string(),
|
|
203
|
+
status: z.string(),
|
|
204
|
+
capabilities: z
|
|
205
|
+
.object({
|
|
206
|
+
sms: z.boolean().optional(),
|
|
207
|
+
voice: z.boolean().optional(),
|
|
208
|
+
})
|
|
209
|
+
.optional(),
|
|
210
|
+
});
|
|
211
|
+
export const GetPhoneNumberInputSchema = z.object({
|
|
212
|
+
phoneNumberId: z.string().describe('ID of the phone number to get'),
|
|
213
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CreateAssistantInputSchema, GetAssistantInputSchema, } from '../schemas/index.js';
|
|
2
|
+
import { transformAssistantInput, transformAssistantOutput, } from '../transformers/index.js';
|
|
3
|
+
import { createToolHandler } from './utils.js';
|
|
4
|
+
export const registerAssistantTools = (server, vapiClient) => {
|
|
5
|
+
server.tool('list_assistants', 'Lists all Vapi assistants', {}, createToolHandler(async () => {
|
|
6
|
+
// console.log('list_assistants');
|
|
7
|
+
const assistants = await vapiClient.assistants.list({ limit: 10 });
|
|
8
|
+
// console.log('assistants', assistants);
|
|
9
|
+
return assistants.map(transformAssistantOutput);
|
|
10
|
+
}));
|
|
11
|
+
server.tool('create_assistant', 'Creates a new Vapi assistant', CreateAssistantInputSchema.shape, createToolHandler(async (data) => {
|
|
12
|
+
// console.log('create_assistant', data);
|
|
13
|
+
const createAssistantDto = transformAssistantInput(data);
|
|
14
|
+
const assistant = await vapiClient.assistants.create(createAssistantDto);
|
|
15
|
+
return transformAssistantOutput(assistant);
|
|
16
|
+
}));
|
|
17
|
+
server.tool('get_assistant', 'Gets a Vapi assistant by ID', GetAssistantInputSchema.shape, createToolHandler(async (data) => {
|
|
18
|
+
// console.log('get_assistant', data);
|
|
19
|
+
const assistantId = data.assistantId;
|
|
20
|
+
try {
|
|
21
|
+
const assistant = await vapiClient.assistants.get(assistantId);
|
|
22
|
+
if (!assistant) {
|
|
23
|
+
throw new Error(`Assistant with ID ${assistantId} not found`);
|
|
24
|
+
}
|
|
25
|
+
return transformAssistantOutput(assistant);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(`Error getting assistant: ${error.message}`);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CallInputSchema, GetCallInputSchema } from '../schemas/index.js';
|
|
2
|
+
import { transformCallInput, transformCallOutput, } from '../transformers/index.js';
|
|
3
|
+
import { createToolHandler } from './utils.js';
|
|
4
|
+
export const registerCallTools = (server, vapiClient) => {
|
|
5
|
+
server.tool('list_calls', 'Lists all Vapi calls', {}, createToolHandler(async () => {
|
|
6
|
+
const calls = await vapiClient.calls.list({ limit: 10 });
|
|
7
|
+
return calls.map(transformCallOutput);
|
|
8
|
+
}));
|
|
9
|
+
server.tool('create_call', 'Creates a outbound call', CallInputSchema.shape, createToolHandler(async (data) => {
|
|
10
|
+
const createCallDto = transformCallInput(data);
|
|
11
|
+
const call = await vapiClient.calls.create(createCallDto);
|
|
12
|
+
return transformCallOutput(call);
|
|
13
|
+
}));
|
|
14
|
+
server.tool('get_call', 'Gets details of a specific call', GetCallInputSchema.shape, createToolHandler(async (data) => {
|
|
15
|
+
const call = await vapiClient.calls.get(data.callId);
|
|
16
|
+
return transformCallOutput(call);
|
|
17
|
+
}));
|
|
18
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { registerAssistantTools } from './assistant.js';
|
|
2
|
+
import { registerCallTools } from './call.js';
|
|
3
|
+
import { registerPhoneNumberTools } from './phone-number.js';
|
|
4
|
+
export const registerAllTools = (server, vapiClient) => {
|
|
5
|
+
registerAssistantTools(server, vapiClient);
|
|
6
|
+
registerCallTools(server, vapiClient);
|
|
7
|
+
registerPhoneNumberTools(server, vapiClient);
|
|
8
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { transformPhoneNumberOutput } from '../transformers/index.js';
|
|
2
|
+
import { createToolHandler, } from './utils.js';
|
|
3
|
+
import { GetPhoneNumberInputSchema } from '../schemas/index.js';
|
|
4
|
+
export const registerPhoneNumberTools = (server, vapiClient) => {
|
|
5
|
+
server.tool('list_phone_numbers', 'Lists all Vapi phone numbers', {}, createToolHandler(async () => {
|
|
6
|
+
const phoneNumbers = await vapiClient.phoneNumbers.list({ limit: 10 });
|
|
7
|
+
return phoneNumbers.map(transformPhoneNumberOutput);
|
|
8
|
+
}));
|
|
9
|
+
server.tool('get_phone_number', 'Gets details of a specific phone number', GetPhoneNumberInputSchema.shape, createToolHandler(async (data) => {
|
|
10
|
+
const phoneNumberId = data.phoneNumberId;
|
|
11
|
+
const phoneNumber = await vapiClient.phoneNumbers.get(phoneNumberId);
|
|
12
|
+
return transformPhoneNumberOutput(phoneNumber);
|
|
13
|
+
}));
|
|
14
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function createSuccessResponse(data) {
|
|
2
|
+
return {
|
|
3
|
+
content: [
|
|
4
|
+
{
|
|
5
|
+
type: 'text',
|
|
6
|
+
text: typeof data === 'string' ? data : JSON.stringify(data),
|
|
7
|
+
},
|
|
8
|
+
],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function createErrorResponse(error) {
|
|
12
|
+
const errorMessage = error?.message || String(error);
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: `Error: ${errorMessage}`,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createToolHandler(handler) {
|
|
23
|
+
return async (params) => {
|
|
24
|
+
try {
|
|
25
|
+
const result = await handler(params);
|
|
26
|
+
return createSuccessResponse(result);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return createErrorResponse(error);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function createParamsSchema(schema) {
|
|
34
|
+
return {
|
|
35
|
+
properties: {
|
|
36
|
+
params: schema.shape || {},
|
|
37
|
+
},
|
|
38
|
+
required: ['params'],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function createIdParamSchema(paramName, description) {
|
|
42
|
+
return {
|
|
43
|
+
properties: {
|
|
44
|
+
[paramName]: { type: 'string', description },
|
|
45
|
+
},
|
|
46
|
+
required: [paramName],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export const withErrorHandling = (handler) => {
|
|
50
|
+
return handler()
|
|
51
|
+
.then((result) => createSuccessResponse(result))
|
|
52
|
+
.catch((error) => createErrorResponse(error));
|
|
53
|
+
};
|
|
54
|
+
export const filterResponseWithSchema = (data, schema) => {
|
|
55
|
+
if (Array.isArray(data)) {
|
|
56
|
+
return data.map((item) => schema.parse(item));
|
|
57
|
+
}
|
|
58
|
+
return schema.parse(data);
|
|
59
|
+
};
|
|
60
|
+
export const withSchemaFiltering = (handler, schema) => {
|
|
61
|
+
return handler()
|
|
62
|
+
.then((result) => {
|
|
63
|
+
const filtered = filterResponseWithSchema(result, schema);
|
|
64
|
+
return createSuccessResponse(filtered);
|
|
65
|
+
})
|
|
66
|
+
.catch((error) => createErrorResponse(error));
|
|
67
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ===== Assistant Transformers =====
|
|
2
|
+
export function transformAssistantInput(input) {
|
|
3
|
+
const assistantDto = {
|
|
4
|
+
name: input.name,
|
|
5
|
+
};
|
|
6
|
+
assistantDto.model = {
|
|
7
|
+
provider: input.llm.provider,
|
|
8
|
+
model: input.llm.model,
|
|
9
|
+
};
|
|
10
|
+
if (input.instructions) {
|
|
11
|
+
assistantDto.model.messages = [
|
|
12
|
+
{
|
|
13
|
+
role: 'system',
|
|
14
|
+
content: input.instructions,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
assistantDto.transcriber = {
|
|
19
|
+
provider: input.transcriber.provider,
|
|
20
|
+
...(input.transcriber.model ? { model: input.transcriber.model } : {}),
|
|
21
|
+
};
|
|
22
|
+
assistantDto.voice = {
|
|
23
|
+
provider: input.voice.provider,
|
|
24
|
+
voiceId: input.voice.voiceId,
|
|
25
|
+
...(input.voice.model ? { model: input.voice.model } : {}),
|
|
26
|
+
};
|
|
27
|
+
if (input.firstMessage) {
|
|
28
|
+
assistantDto.firstMessage = input.firstMessage;
|
|
29
|
+
}
|
|
30
|
+
if (input.firstMessageMode) {
|
|
31
|
+
assistantDto.firstMessageMode = input.firstMessageMode;
|
|
32
|
+
}
|
|
33
|
+
return assistantDto;
|
|
34
|
+
}
|
|
35
|
+
export function transformAssistantOutput(assistant) {
|
|
36
|
+
return {
|
|
37
|
+
id: assistant.id,
|
|
38
|
+
createdAt: assistant.createdAt,
|
|
39
|
+
updatedAt: assistant.updatedAt,
|
|
40
|
+
name: assistant.name || 'Vapi Assistant',
|
|
41
|
+
llm: {
|
|
42
|
+
provider: assistant.model?.provider || 'openai',
|
|
43
|
+
model: assistant.model?.model || 'gpt-4o-mini',
|
|
44
|
+
},
|
|
45
|
+
voice: {
|
|
46
|
+
provider: assistant.voice?.provider || '11labs',
|
|
47
|
+
voiceId: getAssistantVoiceId(assistant.voice),
|
|
48
|
+
model: getAssistantVoiceModel(assistant.voice) || 'eleven_turbo_v2_5',
|
|
49
|
+
},
|
|
50
|
+
transcriber: {
|
|
51
|
+
provider: assistant.transcriber?.provider || 'deepgram',
|
|
52
|
+
model: getAssistantTranscriberModel(assistant.transcriber) || 'nova-3',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function getAssistantVoiceId(voice) {
|
|
57
|
+
if (!voice)
|
|
58
|
+
return '';
|
|
59
|
+
const voiceAny = voice;
|
|
60
|
+
return voiceAny.voiceId || voiceAny.voice || '';
|
|
61
|
+
}
|
|
62
|
+
function getAssistantVoiceModel(voice) {
|
|
63
|
+
if (!voice)
|
|
64
|
+
return '';
|
|
65
|
+
const voiceAny = voice;
|
|
66
|
+
return voiceAny.model || '';
|
|
67
|
+
}
|
|
68
|
+
function getAssistantTranscriberModel(transcriber) {
|
|
69
|
+
if (!transcriber)
|
|
70
|
+
return '';
|
|
71
|
+
const transcriberAny = transcriber;
|
|
72
|
+
return transcriberAny.model || transcriberAny.transcriber || '';
|
|
73
|
+
}
|
|
74
|
+
// ===== Call Transformers =====
|
|
75
|
+
export function transformCallInput(input) {
|
|
76
|
+
return {
|
|
77
|
+
...(input.assistantId ? { assistantId: input.assistantId } : {}),
|
|
78
|
+
...(input.phoneNumberId ? { phoneNumberId: input.phoneNumberId } : {}),
|
|
79
|
+
...(input.customer
|
|
80
|
+
? {
|
|
81
|
+
customer: {
|
|
82
|
+
number: input.customer.phoneNumber,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function transformCallOutput(call) {
|
|
89
|
+
return {
|
|
90
|
+
id: call.id,
|
|
91
|
+
createdAt: call.createdAt,
|
|
92
|
+
updatedAt: call.updatedAt,
|
|
93
|
+
status: call.status || '',
|
|
94
|
+
endedReason: call.endedReason,
|
|
95
|
+
assistantId: call.assistantId,
|
|
96
|
+
phoneNumberId: call.phoneNumberId,
|
|
97
|
+
customer: call.customer
|
|
98
|
+
? {
|
|
99
|
+
phoneNumber: call.customer.number || '',
|
|
100
|
+
}
|
|
101
|
+
: undefined,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// ===== Phone Number Transformers =====
|
|
105
|
+
export function transformPhoneNumberOutput(phoneNumber) {
|
|
106
|
+
return {
|
|
107
|
+
id: phoneNumber.id,
|
|
108
|
+
createdAt: phoneNumber.createdAt,
|
|
109
|
+
updatedAt: phoneNumber.updatedAt,
|
|
110
|
+
phoneNumber: phoneNumber.phoneNumber,
|
|
111
|
+
status: phoneNumber.status,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function parseToolResponse(response) {
|
|
2
|
+
// If no response or it's a primitive, return as is
|
|
3
|
+
if (!response || typeof response !== 'object')
|
|
4
|
+
return response;
|
|
5
|
+
// Check for content array with text items
|
|
6
|
+
if (response.content && Array.isArray(response.content)) {
|
|
7
|
+
const textContent = response.content.find((item) => item.type === 'text')?.text;
|
|
8
|
+
if (textContent) {
|
|
9
|
+
// Check if it's an error message first
|
|
10
|
+
if (textContent.startsWith('Error:') || textContent.includes('error')) {
|
|
11
|
+
return { error: textContent };
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(textContent);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
// Don't warn on expected error messages
|
|
18
|
+
if (!textContent.startsWith('Error:')) {
|
|
19
|
+
console.warn('Failed to parse text content as JSON:', e);
|
|
20
|
+
}
|
|
21
|
+
return textContent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return response;
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vapi-ai/mcp-server",
|
|
3
|
+
"description": "Vapi MCP Server",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "ramsrib",
|
|
8
|
+
"keywords": [],
|
|
9
|
+
"repository": "https://github.com/VapiAI/mcp-server",
|
|
10
|
+
"bin": {
|
|
11
|
+
"mcp-server": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"prepare": "npm run build",
|
|
15
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"dev": "tsx watch src/index.ts",
|
|
18
|
+
"dev:example": "tsx examples/client.ts",
|
|
19
|
+
"inspector": "mcp-inspector",
|
|
20
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
21
|
+
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest src/tests/mcp-server-mock.test.ts",
|
|
22
|
+
"test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest src/tests/mcp-server-e2e.test.ts"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
29
|
+
"@vapi-ai/server-sdk": "^0.5.2",
|
|
30
|
+
"dotenv": "^16.4.7",
|
|
31
|
+
"zod": "^3.24.2",
|
|
32
|
+
"zod-to-json-schema": "^3.24.5"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@modelcontextprotocol/inspector": "^0.8.2",
|
|
36
|
+
"@types/jest": "^29.5.14",
|
|
37
|
+
"@types/node": "^22.14.0",
|
|
38
|
+
"jest": "^29.7.0",
|
|
39
|
+
"shx": "^0.4.0",
|
|
40
|
+
"ts-jest": "^29.3.1",
|
|
41
|
+
"tsx": "^4.19.3",
|
|
42
|
+
"typescript": "^5.8.3"
|
|
43
|
+
}
|
|
44
|
+
}
|