Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
});
26 changes: 24 additions & 2 deletions src/tools/tool.ts
Original file line number Diff line number Diff line change
@@ -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 = (
Expand All @@ -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);
})
);
};
128 changes: 127 additions & 1 deletion src/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
PhoneNumberOutputSchema,
ToolOutputSchema,
UpdateAssistantInputSchema,
CreateToolInputSchema,
UpdateToolInputSchema,
} from '../schemas/index.js';

// ===== Assistant Transformers =====
Expand Down Expand Up @@ -243,8 +245,127 @@ export function transformPhoneNumberOutput(

// ===== Tool Transformers =====

export function transformToolInput(
input: z.infer<typeof CreateToolInputSchema>
): 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<typeof UpdateToolInputSchema>
): 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<typeof ToolOutputSchema> {
return {
id: tool.id,
Expand All @@ -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<string, string> || {},
}
};
}