diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 43d9690..2638121 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -328,10 +328,91 @@ 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'), +}); + +// 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'), + + // 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 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 f6ba9b7..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 } from '../schemas/index.js'; -import { 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 = ( @@ -28,4 +28,26 @@ 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); + }) + ); + + 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 06b65e4..53fa800 100644 --- a/src/transformers/index.ts +++ b/src/transformers/index.ts @@ -8,6 +8,8 @@ import { PhoneNumberOutputSchema, ToolOutputSchema, UpdateAssistantInputSchema, + CreateToolInputSchema, + UpdateToolInputSchema, } from '../schemas/index.js'; // ===== Assistant Transformers ===== @@ -243,8 +245,127 @@ 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 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, @@ -253,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 || {}, + } }; }