From 77cbf127882ca798f07e7b3f133580f6d2847224 Mon Sep 17 00:00:00 2001 From: stevenbdf Date: Wed, 18 Jun 2025 23:33:44 -0700 Subject: [PATCH 1/4] feat: add create_tool --- src/schemas/index.ts | 72 +++++++++++++++++++++++++++++++++++++++ src/tools/tool.ts | 15 ++++++-- src/transformers/index.ts | 67 ++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 2 deletions(-) diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 43d9690..e965c96 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -328,6 +328,78 @@ export const GetToolInputSchema = z.object({ toolId: z.string().describe('ID of the tool to get'), }); +const TransferCallDestinationSchema = z.object({ + type: z.literal('number'), + number: z.string().describe('Phone number to transfer to (e.g., "+16054440129"). It can be any phone number in E.164 format.'), + extension: z.string().optional().describe('Extension number if applicable'), + callerId: z.string().optional().describe('Caller ID to use for the transfer'), + description: z.string().optional().describe('Description of the transfer destination'), +}); + +// Generic custom tool schemas +const JsonSchemaProperty = z.object({ + type: z.string(), + description: z.string().optional(), + enum: z.array(z.string()).optional(), + items: z.any().optional(), + properties: z.record(z.any()).optional(), + required: z.array(z.string()).optional(), +}); + +const JsonSchema = z.object({ + type: z.literal('object'), + properties: z.record(JsonSchemaProperty), + required: z.array(z.string()).optional(), +}); + +const ServerSchema = z.object({ + url: z.string().url().describe('Server URL where the function will be called'), + headers: z.record(z.string()).optional().describe('Headers to send with the request'), +}); + +const BackoffPlanSchema = z.object({ + type: z.enum(['fixed', 'exponential']).default('fixed'), + maxRetries: z.number().default(3).describe('Maximum number of retries'), + baseDelaySeconds: z.number().default(1).describe('Base delay between retries in seconds'), +}); + +export const CreateToolInputSchema = z.object({ + type: z.enum(['sms', 'transferCall', 'function', 'apiRequest']) + .describe('Type of the tool to create'), + + // Common fields for all tools + name: z.string().optional().describe('Name of the function/tool'), + description: z.string().optional().describe('Description of what the function/tool does'), + + // SMS tool configuration + sms: z.object({ + metadata: z.object({ + from: z.string().describe('Phone number to send SMS from (e.g., "+15551234567"). It must be a twilio number in E.164 format.'), + }).describe('SMS configuration metadata'), + }).optional().describe('SMS tool configuration - to send text messages'), + + // Transfer call tool configuration + transferCall: z.object({ + destinations: z.array(TransferCallDestinationSchema).describe('Array of possible transfer destinations'), + }).optional().describe('Transfer call tool configuration - to transfer calls to destinations'), + + // Function tool configuration (custom functions with parameters) + function: z.object({ + parameters: JsonSchema.describe('JSON schema for function parameters'), + server: ServerSchema.describe('Server configuration with URL where the function will be called'), + }).optional().describe('Custom function tool configuration - for custom server-side functions'), + + // API Request tool configuration + apiRequest: z.object({ + url: z.string().url().describe('URL to make the API request to'), + method: z.enum(['GET', 'POST']).default('POST').describe('HTTP method for the API request'), + headers: z.record(z.string()).optional().describe('Headers to send with the request (key-value pairs)'), + body: JsonSchema.optional().describe('Body schema for the API request in JSON Schema format'), + backoffPlan: BackoffPlanSchema.optional().describe('Retry configuration for failed API requests'), + timeoutSeconds: z.number().default(20).describe('Request timeout in seconds'), + }).optional().describe('API Request tool configuration - for HTTP API integration'), +}); + export const ToolOutputSchema = BaseResponseSchema.extend({ type: z .string() diff --git a/src/tools/tool.ts b/src/tools/tool.ts index f6ba9b7..5eb7ef4 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,8 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { VapiClient, Vapi } from '@vapi-ai/server-sdk'; -import { GetToolInputSchema } from '../schemas/index.js'; -import { transformToolOutput } from '../transformers/index.js'; +import { GetToolInputSchema, CreateToolInputSchema } from '../schemas/index.js'; +import { transformToolInput, transformToolOutput } from '../transformers/index.js'; import { createToolHandler } from './utils.js'; export const registerToolTools = ( @@ -28,4 +28,15 @@ export const registerToolTools = ( return transformToolOutput(tool); }) ); + + server.tool( + 'create_tool', + 'Creates a new Vapi tool', + CreateToolInputSchema.shape, + createToolHandler(async (data) => { + const createToolDto = transformToolInput(data); + const tool = await vapiClient.tools.create(createToolDto); + return transformToolOutput(tool); + }) + ); }; diff --git a/src/transformers/index.ts b/src/transformers/index.ts index 06b65e4..d4b0243 100644 --- a/src/transformers/index.ts +++ b/src/transformers/index.ts @@ -8,6 +8,7 @@ import { PhoneNumberOutputSchema, ToolOutputSchema, UpdateAssistantInputSchema, + CreateToolInputSchema, } from '../schemas/index.js'; // ===== Assistant Transformers ===== @@ -243,6 +244,72 @@ export function transformPhoneNumberOutput( // ===== Tool Transformers ===== +export function transformToolInput( + input: z.infer +): any { + let toolDto: any = { + type: input.type, + }; + + // Add function definition if name and description are provided + if (input.name || input.description) { + toolDto.function = { + ...(input.name && { name: input.name }), + ...(input.description && { description: input.description }), + }; + } + + // Handle different tool types using the new nested structure + switch (input.type) { + case 'sms': + if (input.sms?.metadata) { + toolDto.metadata = input.sms.metadata; + } + break; + + case 'transferCall': + if (input.transferCall?.destinations) { + toolDto.destinations = input.transferCall.destinations; + } + break; + + case 'function': + if (input.function?.parameters && input.function?.server) { + // For function tools, add parameters to the existing function object + if (toolDto.function) { + toolDto.function.parameters = input.function.parameters; + } else { + toolDto.function = { + parameters: input.function.parameters, + }; + } + + toolDto.server = { + url: input.function.server.url, + ...(input.function.server.headers && { headers: input.function.server.headers }), + }; + } + break; + + case 'apiRequest': + if (input.apiRequest?.url) { + toolDto.url = input.apiRequest.url; + toolDto.method = input.apiRequest.method || 'POST'; + + if (input.apiRequest.headers) toolDto.headers = input.apiRequest.headers; + if (input.apiRequest.body) toolDto.body = input.apiRequest.body; + if (input.apiRequest.backoffPlan) toolDto.backoffPlan = input.apiRequest.backoffPlan; + if (input.apiRequest.timeoutSeconds) toolDto.timeoutSeconds = input.apiRequest.timeoutSeconds; + } + break; + + default: + throw new Error(`Unsupported tool type: ${(input as any).type}`); + } + + return toolDto; +} + export function transformToolOutput( tool: any ): z.infer { From 000f4435395ac6c73e8ee77e0560c483b1ef0174 Mon Sep 17 00:00:00 2001 From: stevenbdf Date: Wed, 18 Jun 2025 23:48:23 -0700 Subject: [PATCH 2/4] feat: add update_tool --- src/schemas/index.ts | 17 ++++++++--- src/tools/tool.ts | 15 ++++++++-- src/transformers/index.ts | 61 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/schemas/index.ts b/src/schemas/index.ts index e965c96..2638121 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -363,10 +363,8 @@ const BackoffPlanSchema = z.object({ baseDelaySeconds: z.number().default(1).describe('Base delay between retries in seconds'), }); -export const CreateToolInputSchema = z.object({ - type: z.enum(['sms', 'transferCall', 'function', 'apiRequest']) - .describe('Type of the tool to create'), - +// Base tool configuration schema (reusable for both create and update) +const BaseToolConfigSchema = z.object({ // Common fields for all tools name: z.string().optional().describe('Name of the function/tool'), description: z.string().optional().describe('Description of what the function/tool does'), @@ -400,10 +398,21 @@ export const CreateToolInputSchema = z.object({ }).optional().describe('API Request tool configuration - for HTTP API integration'), }); +export const CreateToolInputSchema = BaseToolConfigSchema.extend({ + type: z.enum(['sms', 'transferCall', 'function', 'apiRequest']) + .describe('Type of the tool to create'), +}); + +export const UpdateToolInputSchema = BaseToolConfigSchema.extend({ + toolId: z.string().describe('ID of the tool to update'), +}); + export const ToolOutputSchema = BaseResponseSchema.extend({ type: z .string() .describe('Type of the tool (dtmf, function, mcp, query, etc.)'), name: z.string().describe('Name of the tool'), description: z.string().describe('Description of the tool'), + parameters: z.record(z.any()).describe('Parameters of the tool'), + server: ServerSchema.describe('Server of the tool'), }); diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 5eb7ef4..d487e1e 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,8 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { VapiClient, Vapi } from '@vapi-ai/server-sdk'; -import { GetToolInputSchema, CreateToolInputSchema } from '../schemas/index.js'; -import { transformToolInput, transformToolOutput } from '../transformers/index.js'; +import { GetToolInputSchema, CreateToolInputSchema, UpdateToolInputSchema } from '../schemas/index.js'; +import { transformToolInput, transformUpdateToolInput, transformToolOutput } from '../transformers/index.js'; import { createToolHandler } from './utils.js'; export const registerToolTools = ( @@ -39,4 +39,15 @@ export const registerToolTools = ( return transformToolOutput(tool); }) ); + + server.tool( + 'update_tool', + 'Updates an existing Vapi tool', + UpdateToolInputSchema.shape, + createToolHandler(async (data) => { + const updateToolDto = transformUpdateToolInput(data); + const tool = await vapiClient.tools.update(data.toolId, updateToolDto); + return transformToolOutput(tool); + }) + ); }; diff --git a/src/transformers/index.ts b/src/transformers/index.ts index d4b0243..53fa800 100644 --- a/src/transformers/index.ts +++ b/src/transformers/index.ts @@ -9,6 +9,7 @@ import { ToolOutputSchema, UpdateAssistantInputSchema, CreateToolInputSchema, + UpdateToolInputSchema, } from '../schemas/index.js'; // ===== Assistant Transformers ===== @@ -310,8 +311,61 @@ export function transformToolInput( return toolDto; } +export function transformUpdateToolInput( + input: z.infer +): any { + let updateDto: any = {}; + + // Add function definition if name and description are provided + if (input.name || input.description) { + updateDto.function = { + ...(input.name && { name: input.name }), + ...(input.description && { description: input.description }), + }; + } + + // Handle SMS tool configuration + if (input.sms?.metadata) { + updateDto.metadata = input.sms.metadata; + } + + // Handle Transfer call tool configuration + if (input.transferCall?.destinations) { + updateDto.destinations = input.transferCall.destinations; + } + + // Handle Function tool configuration + if (input.function?.parameters && input.function?.server) { + // For function tools, add parameters to the existing function object + if (updateDto.function) { + updateDto.function.parameters = input.function.parameters; + } else { + updateDto.function = { + parameters: input.function.parameters, + }; + } + + updateDto.server = { + url: input.function.server.url, + ...(input.function.server.headers && { headers: input.function.server.headers }), + }; + } + + // Handle API Request tool configuration + if (input.apiRequest) { + if (input.apiRequest.url) updateDto.url = input.apiRequest.url; + if (input.apiRequest.method) updateDto.method = input.apiRequest.method; + if (input.apiRequest.headers) updateDto.headers = input.apiRequest.headers; + if (input.apiRequest.body) updateDto.body = input.apiRequest.body; + if (input.apiRequest.backoffPlan) updateDto.backoffPlan = input.apiRequest.backoffPlan; + if (input.apiRequest.timeoutSeconds) updateDto.timeoutSeconds = input.apiRequest.timeoutSeconds; + } + + return updateDto; +} + export function transformToolOutput( - tool: any + tool: Vapi.ToolsGetResponse ): z.infer { return { id: tool.id, @@ -320,5 +374,10 @@ export function transformToolOutput( type: tool.type || '', name: tool.function?.name || '', description: tool.function?.description || '', + parameters: tool.function?.parameters || {}, + server: { + url: tool.server?.url || '', + headers: tool.server?.headers as Record || {}, + } }; } From e0cdfb7cb990918273e7a63519f490ac4430e4b4 Mon Sep 17 00:00:00 2001 From: stevenbdf Date: Wed, 18 Jun 2025 23:57:04 -0700 Subject: [PATCH 3/4] feat: add delete_tool --- src/tools/tool.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tools/tool.ts b/src/tools/tool.ts index d487e1e..6e7423d 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -50,4 +50,14 @@ export const registerToolTools = ( return transformToolOutput(tool); }) ); + + server.tool( + 'delete_tool', + 'Deletes a Vapi tool', + GetToolInputSchema.shape, + createToolHandler(async (data) => { + const tool = await vapiClient.tools.delete(data.toolId); + return transformToolOutput(tool); + }) + ); }; From e8b15e44d3e1cf5f014ecbab59421d2a1b30c3c1 Mon Sep 17 00:00:00 2001 From: stevenbdf Date: Thu, 19 Jun 2025 10:20:43 -0700 Subject: [PATCH 4/4] chore: remove delete_tool --- src/tools/tool.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 6e7423d..d487e1e 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -50,14 +50,4 @@ export const registerToolTools = ( return transformToolOutput(tool); }) ); - - server.tool( - 'delete_tool', - 'Deletes a Vapi tool', - GetToolInputSchema.shape, - createToolHandler(async (data) => { - const tool = await vapiClient.tools.delete(data.toolId); - return transformToolOutput(tool); - }) - ); };