Skip to content

Commit 77cbf12

Browse files
feat: add create_tool
1 parent 076fc42 commit 77cbf12

3 files changed

Lines changed: 152 additions & 2 deletions

File tree

src/schemas/index.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,78 @@ 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+
export const CreateToolInputSchema = z.object({
367+
type: z.enum(['sms', 'transferCall', 'function', 'apiRequest'])
368+
.describe('Type of the tool to create'),
369+
370+
// Common fields for all tools
371+
name: z.string().optional().describe('Name of the function/tool'),
372+
description: z.string().optional().describe('Description of what the function/tool does'),
373+
374+
// SMS tool configuration
375+
sms: z.object({
376+
metadata: z.object({
377+
from: z.string().describe('Phone number to send SMS from (e.g., "+15551234567"). It must be a twilio number in E.164 format.'),
378+
}).describe('SMS configuration metadata'),
379+
}).optional().describe('SMS tool configuration - to send text messages'),
380+
381+
// Transfer call tool configuration
382+
transferCall: z.object({
383+
destinations: z.array(TransferCallDestinationSchema).describe('Array of possible transfer destinations'),
384+
}).optional().describe('Transfer call tool configuration - to transfer calls to destinations'),
385+
386+
// Function tool configuration (custom functions with parameters)
387+
function: z.object({
388+
parameters: JsonSchema.describe('JSON schema for function parameters'),
389+
server: ServerSchema.describe('Server configuration with URL where the function will be called'),
390+
}).optional().describe('Custom function tool configuration - for custom server-side functions'),
391+
392+
// API Request tool configuration
393+
apiRequest: z.object({
394+
url: z.string().url().describe('URL to make the API request to'),
395+
method: z.enum(['GET', 'POST']).default('POST').describe('HTTP method for the API request'),
396+
headers: z.record(z.string()).optional().describe('Headers to send with the request (key-value pairs)'),
397+
body: JsonSchema.optional().describe('Body schema for the API request in JSON Schema format'),
398+
backoffPlan: BackoffPlanSchema.optional().describe('Retry configuration for failed API requests'),
399+
timeoutSeconds: z.number().default(20).describe('Request timeout in seconds'),
400+
}).optional().describe('API Request tool configuration - for HTTP API integration'),
401+
});
402+
331403
export const ToolOutputSchema = BaseResponseSchema.extend({
332404
type: z
333405
.string()

src/tools/tool.ts

Lines changed: 13 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 } from '../schemas/index.js';
5+
import { transformToolInput, transformToolOutput } from '../transformers/index.js';
66
import { createToolHandler } from './utils.js';
77

88
export const registerToolTools = (
@@ -28,4 +28,15 @@ 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+
);
3142
};

src/transformers/index.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
PhoneNumberOutputSchema,
99
ToolOutputSchema,
1010
UpdateAssistantInputSchema,
11+
CreateToolInputSchema,
1112
} from '../schemas/index.js';
1213

1314
// ===== Assistant Transformers =====
@@ -243,6 +244,72 @@ export function transformPhoneNumberOutput(
243244

244245
// ===== Tool Transformers =====
245246

247+
export function transformToolInput(
248+
input: z.infer<typeof CreateToolInputSchema>
249+
): any {
250+
let toolDto: any = {
251+
type: input.type,
252+
};
253+
254+
// Add function definition if name and description are provided
255+
if (input.name || input.description) {
256+
toolDto.function = {
257+
...(input.name && { name: input.name }),
258+
...(input.description && { description: input.description }),
259+
};
260+
}
261+
262+
// Handle different tool types using the new nested structure
263+
switch (input.type) {
264+
case 'sms':
265+
if (input.sms?.metadata) {
266+
toolDto.metadata = input.sms.metadata;
267+
}
268+
break;
269+
270+
case 'transferCall':
271+
if (input.transferCall?.destinations) {
272+
toolDto.destinations = input.transferCall.destinations;
273+
}
274+
break;
275+
276+
case 'function':
277+
if (input.function?.parameters && input.function?.server) {
278+
// For function tools, add parameters to the existing function object
279+
if (toolDto.function) {
280+
toolDto.function.parameters = input.function.parameters;
281+
} else {
282+
toolDto.function = {
283+
parameters: input.function.parameters,
284+
};
285+
}
286+
287+
toolDto.server = {
288+
url: input.function.server.url,
289+
...(input.function.server.headers && { headers: input.function.server.headers }),
290+
};
291+
}
292+
break;
293+
294+
case 'apiRequest':
295+
if (input.apiRequest?.url) {
296+
toolDto.url = input.apiRequest.url;
297+
toolDto.method = input.apiRequest.method || 'POST';
298+
299+
if (input.apiRequest.headers) toolDto.headers = input.apiRequest.headers;
300+
if (input.apiRequest.body) toolDto.body = input.apiRequest.body;
301+
if (input.apiRequest.backoffPlan) toolDto.backoffPlan = input.apiRequest.backoffPlan;
302+
if (input.apiRequest.timeoutSeconds) toolDto.timeoutSeconds = input.apiRequest.timeoutSeconds;
303+
}
304+
break;
305+
306+
default:
307+
throw new Error(`Unsupported tool type: ${(input as any).type}`);
308+
}
309+
310+
return toolDto;
311+
}
312+
246313
export function transformToolOutput(
247314
tool: any
248315
): z.infer<typeof ToolOutputSchema> {

0 commit comments

Comments
 (0)