Skip to content

Commit e9baeb4

Browse files
authored
Merge pull request #5 from VapiAI/steven-diaz/vap-7512-mcp-create-update-delete-tools
VAP-7512: add more tool actions
2 parents 076fc42 + e8b15e4 commit e9baeb4

File tree

3 files changed

+232
-3
lines changed

3 files changed

+232
-3
lines changed

src/schemas/index.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,10 +328,91 @@ export const GetToolInputSchema = z.object({
328328
toolId: z.string().describe('ID of the tool to get'),
329329
});
330330

331+
const TransferCallDestinationSchema = z.object({
332+
type: z.literal('number'),
333+
number: z.string().describe('Phone number to transfer to (e.g., "+16054440129"). It can be any phone number in E.164 format.'),
334+
extension: z.string().optional().describe('Extension number if applicable'),
335+
callerId: z.string().optional().describe('Caller ID to use for the transfer'),
336+
description: z.string().optional().describe('Description of the transfer destination'),
337+
});
338+
339+
// Generic custom tool schemas
340+
const JsonSchemaProperty = z.object({
341+
type: z.string(),
342+
description: z.string().optional(),
343+
enum: z.array(z.string()).optional(),
344+
items: z.any().optional(),
345+
properties: z.record(z.any()).optional(),
346+
required: z.array(z.string()).optional(),
347+
});
348+
349+
const JsonSchema = z.object({
350+
type: z.literal('object'),
351+
properties: z.record(JsonSchemaProperty),
352+
required: z.array(z.string()).optional(),
353+
});
354+
355+
const ServerSchema = z.object({
356+
url: z.string().url().describe('Server URL where the function will be called'),
357+
headers: z.record(z.string()).optional().describe('Headers to send with the request'),
358+
});
359+
360+
const BackoffPlanSchema = z.object({
361+
type: z.enum(['fixed', 'exponential']).default('fixed'),
362+
maxRetries: z.number().default(3).describe('Maximum number of retries'),
363+
baseDelaySeconds: z.number().default(1).describe('Base delay between retries in seconds'),
364+
});
365+
366+
// Base tool configuration schema (reusable for both create and update)
367+
const BaseToolConfigSchema = z.object({
368+
// Common fields for all tools
369+
name: z.string().optional().describe('Name of the function/tool'),
370+
description: z.string().optional().describe('Description of what the function/tool does'),
371+
372+
// SMS tool configuration
373+
sms: z.object({
374+
metadata: z.object({
375+
from: z.string().describe('Phone number to send SMS from (e.g., "+15551234567"). It must be a twilio number in E.164 format.'),
376+
}).describe('SMS configuration metadata'),
377+
}).optional().describe('SMS tool configuration - to send text messages'),
378+
379+
// Transfer call tool configuration
380+
transferCall: z.object({
381+
destinations: z.array(TransferCallDestinationSchema).describe('Array of possible transfer destinations'),
382+
}).optional().describe('Transfer call tool configuration - to transfer calls to destinations'),
383+
384+
// Function tool configuration (custom functions with parameters)
385+
function: z.object({
386+
parameters: JsonSchema.describe('JSON schema for function parameters'),
387+
server: ServerSchema.describe('Server configuration with URL where the function will be called'),
388+
}).optional().describe('Custom function tool configuration - for custom server-side functions'),
389+
390+
// API Request tool configuration
391+
apiRequest: z.object({
392+
url: z.string().url().describe('URL to make the API request to'),
393+
method: z.enum(['GET', 'POST']).default('POST').describe('HTTP method for the API request'),
394+
headers: z.record(z.string()).optional().describe('Headers to send with the request (key-value pairs)'),
395+
body: JsonSchema.optional().describe('Body schema for the API request in JSON Schema format'),
396+
backoffPlan: BackoffPlanSchema.optional().describe('Retry configuration for failed API requests'),
397+
timeoutSeconds: z.number().default(20).describe('Request timeout in seconds'),
398+
}).optional().describe('API Request tool configuration - for HTTP API integration'),
399+
});
400+
401+
export const CreateToolInputSchema = BaseToolConfigSchema.extend({
402+
type: z.enum(['sms', 'transferCall', 'function', 'apiRequest'])
403+
.describe('Type of the tool to create'),
404+
});
405+
406+
export const UpdateToolInputSchema = BaseToolConfigSchema.extend({
407+
toolId: z.string().describe('ID of the tool to update'),
408+
});
409+
331410
export const ToolOutputSchema = BaseResponseSchema.extend({
332411
type: z
333412
.string()
334413
.describe('Type of the tool (dtmf, function, mcp, query, etc.)'),
335414
name: z.string().describe('Name of the tool'),
336415
description: z.string().describe('Description of the tool'),
416+
parameters: z.record(z.any()).describe('Parameters of the tool'),
417+
server: ServerSchema.describe('Server of the tool'),
337418
});

src/tools/tool.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
import { VapiClient, Vapi } from '@vapi-ai/server-sdk';
33

4-
import { GetToolInputSchema } from '../schemas/index.js';
5-
import { transformToolOutput } from '../transformers/index.js';
4+
import { GetToolInputSchema, CreateToolInputSchema, UpdateToolInputSchema } from '../schemas/index.js';
5+
import { transformToolInput, transformUpdateToolInput, transformToolOutput } from '../transformers/index.js';
66
import { createToolHandler } from './utils.js';
77

88
export const registerToolTools = (
@@ -28,4 +28,26 @@ export const registerToolTools = (
2828
return transformToolOutput(tool);
2929
})
3030
);
31+
32+
server.tool(
33+
'create_tool',
34+
'Creates a new Vapi tool',
35+
CreateToolInputSchema.shape,
36+
createToolHandler(async (data) => {
37+
const createToolDto = transformToolInput(data);
38+
const tool = await vapiClient.tools.create(createToolDto);
39+
return transformToolOutput(tool);
40+
})
41+
);
42+
43+
server.tool(
44+
'update_tool',
45+
'Updates an existing Vapi tool',
46+
UpdateToolInputSchema.shape,
47+
createToolHandler(async (data) => {
48+
const updateToolDto = transformUpdateToolInput(data);
49+
const tool = await vapiClient.tools.update(data.toolId, updateToolDto);
50+
return transformToolOutput(tool);
51+
})
52+
);
3153
};

src/transformers/index.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
PhoneNumberOutputSchema,
99
ToolOutputSchema,
1010
UpdateAssistantInputSchema,
11+
CreateToolInputSchema,
12+
UpdateToolInputSchema,
1113
} from '../schemas/index.js';
1214

1315
// ===== Assistant Transformers =====
@@ -243,8 +245,127 @@ export function transformPhoneNumberOutput(
243245

244246
// ===== Tool Transformers =====
245247

248+
export function transformToolInput(
249+
input: z.infer<typeof CreateToolInputSchema>
250+
): any {
251+
let toolDto: any = {
252+
type: input.type,
253+
};
254+
255+
// Add function definition if name and description are provided
256+
if (input.name || input.description) {
257+
toolDto.function = {
258+
...(input.name && { name: input.name }),
259+
...(input.description && { description: input.description }),
260+
};
261+
}
262+
263+
// Handle different tool types using the new nested structure
264+
switch (input.type) {
265+
case 'sms':
266+
if (input.sms?.metadata) {
267+
toolDto.metadata = input.sms.metadata;
268+
}
269+
break;
270+
271+
case 'transferCall':
272+
if (input.transferCall?.destinations) {
273+
toolDto.destinations = input.transferCall.destinations;
274+
}
275+
break;
276+
277+
case 'function':
278+
if (input.function?.parameters && input.function?.server) {
279+
// For function tools, add parameters to the existing function object
280+
if (toolDto.function) {
281+
toolDto.function.parameters = input.function.parameters;
282+
} else {
283+
toolDto.function = {
284+
parameters: input.function.parameters,
285+
};
286+
}
287+
288+
toolDto.server = {
289+
url: input.function.server.url,
290+
...(input.function.server.headers && { headers: input.function.server.headers }),
291+
};
292+
}
293+
break;
294+
295+
case 'apiRequest':
296+
if (input.apiRequest?.url) {
297+
toolDto.url = input.apiRequest.url;
298+
toolDto.method = input.apiRequest.method || 'POST';
299+
300+
if (input.apiRequest.headers) toolDto.headers = input.apiRequest.headers;
301+
if (input.apiRequest.body) toolDto.body = input.apiRequest.body;
302+
if (input.apiRequest.backoffPlan) toolDto.backoffPlan = input.apiRequest.backoffPlan;
303+
if (input.apiRequest.timeoutSeconds) toolDto.timeoutSeconds = input.apiRequest.timeoutSeconds;
304+
}
305+
break;
306+
307+
default:
308+
throw new Error(`Unsupported tool type: ${(input as any).type}`);
309+
}
310+
311+
return toolDto;
312+
}
313+
314+
export function transformUpdateToolInput(
315+
input: z.infer<typeof UpdateToolInputSchema>
316+
): any {
317+
let updateDto: any = {};
318+
319+
// Add function definition if name and description are provided
320+
if (input.name || input.description) {
321+
updateDto.function = {
322+
...(input.name && { name: input.name }),
323+
...(input.description && { description: input.description }),
324+
};
325+
}
326+
327+
// Handle SMS tool configuration
328+
if (input.sms?.metadata) {
329+
updateDto.metadata = input.sms.metadata;
330+
}
331+
332+
// Handle Transfer call tool configuration
333+
if (input.transferCall?.destinations) {
334+
updateDto.destinations = input.transferCall.destinations;
335+
}
336+
337+
// Handle Function tool configuration
338+
if (input.function?.parameters && input.function?.server) {
339+
// For function tools, add parameters to the existing function object
340+
if (updateDto.function) {
341+
updateDto.function.parameters = input.function.parameters;
342+
} else {
343+
updateDto.function = {
344+
parameters: input.function.parameters,
345+
};
346+
}
347+
348+
updateDto.server = {
349+
url: input.function.server.url,
350+
...(input.function.server.headers && { headers: input.function.server.headers }),
351+
};
352+
}
353+
354+
// Handle API Request tool configuration
355+
if (input.apiRequest) {
356+
if (input.apiRequest.url) updateDto.url = input.apiRequest.url;
357+
if (input.apiRequest.method) updateDto.method = input.apiRequest.method;
358+
if (input.apiRequest.headers) updateDto.headers = input.apiRequest.headers;
359+
if (input.apiRequest.body) updateDto.body = input.apiRequest.body;
360+
if (input.apiRequest.backoffPlan) updateDto.backoffPlan = input.apiRequest.backoffPlan;
361+
if (input.apiRequest.timeoutSeconds) updateDto.timeoutSeconds = input.apiRequest.timeoutSeconds;
362+
}
363+
364+
return updateDto;
365+
}
366+
246367
export function transformToolOutput(
247-
tool: any
368+
tool: Vapi.ToolsGetResponse
248369
): z.infer<typeof ToolOutputSchema> {
249370
return {
250371
id: tool.id,
@@ -253,5 +374,10 @@ export function transformToolOutput(
253374
type: tool.type || '',
254375
name: tool.function?.name || '',
255376
description: tool.function?.description || '',
377+
parameters: tool.function?.parameters || {},
378+
server: {
379+
url: tool.server?.url || '',
380+
headers: tool.server?.headers as Record<string, string> || {},
381+
}
256382
};
257383
}

0 commit comments

Comments
 (0)