/**
* Tool registry for MCP server
*
* Provides a centralized registry for tool definitions and handlers.
* Handles tool discovery, execution, and error formatting.
*/
import { z } from 'zod';
import { createLogger } from '../shared/logger.js';
import { formatErrorResponse, McpServerError } from '../shared/errors.js';
import type { ToolHandler, ToolResult, RegisteredTool, ResultMeta } from '../types/index.js';
const logger = createLogger('tools');
/**
* Convert Zod schema to JSON Schema for MCP
*/
function zodToJsonSchema(schema: z.ZodType): Record<string, unknown> {
// Handle ZodObject
if (schema instanceof z.ZodObject) {
const shape = schema.shape as Record<string, z.ZodType>;
const properties: Record<string, unknown> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
properties[key] = zodToJsonSchema(value);
// Check if field is required (not optional)
if (!(value instanceof z.ZodOptional)) {
required.push(key);
}
}
return {
type: 'object',
properties,
required: required.length > 0 ? required : undefined,
};
}
// Handle ZodOptional
if (schema instanceof z.ZodOptional) {
return zodToJsonSchema(schema.unwrap());
}
// Handle ZodDefault
if (schema instanceof z.ZodDefault) {
const inner = zodToJsonSchema(schema.removeDefault());
return { ...inner, default: schema._def.defaultValue() };
}
// Handle ZodString
if (schema instanceof z.ZodString) {
const result: Record<string, unknown> = { type: 'string' };
if (schema.description) {
result['description'] = schema.description;
}
return result;
}
// Handle ZodNumber
if (schema instanceof z.ZodNumber) {
const result: Record<string, unknown> = { type: 'number' };
if (schema.description) {
result['description'] = schema.description;
}
return result;
}
// Handle ZodBoolean
if (schema instanceof z.ZodBoolean) {
const result: Record<string, unknown> = { type: 'boolean' };
if (schema.description) {
result['description'] = schema.description;
}
return result;
}
// Handle ZodArray
if (schema instanceof z.ZodArray) {
return {
type: 'array',
items: zodToJsonSchema(schema.element),
};
}
// Handle ZodEnum
if (schema instanceof z.ZodEnum) {
return {
type: 'string',
enum: schema.options,
};
}
// Handle ZodLiteral
if (schema instanceof z.ZodLiteral) {
return {
type: typeof schema.value,
const: schema.value,
};
}
// Handle ZodUnion
if (schema instanceof z.ZodUnion) {
const options = schema.options as z.ZodType[];
return {
oneOf: options.map((opt) => zodToJsonSchema(opt)),
};
}
// Fallback for unknown types
return { type: 'object' };
}
/**
* Tool registry class
*/
export class ToolRegistry {
private readonly tools = new Map<string, RegisteredTool>();
/**
* Register a tool with its definition and handler
*/
register<TInput, TOutput>(
name: string,
description: string,
inputSchema: z.ZodType<TInput>,
handler: ToolHandler<TInput, TOutput>
): void {
if (this.tools.has(name)) {
logger.warning('Overwriting existing tool', { name });
}
const tool: RegisteredTool = {
definition: {
name,
description,
inputSchema,
},
handler: handler as ToolHandler,
};
this.tools.set(name, tool);
logger.debug('Registered tool', { name });
}
/**
* Get a tool by name
*/
get(name: string): RegisteredTool | undefined {
return this.tools.get(name);
}
/**
* Check if a tool exists
*/
has(name: string): boolean {
return this.tools.has(name);
}
/**
* Get all tool definitions for MCP ListToolsRequest
*/
getDefinitions(): Array<{ name: string; description: string; inputSchema: Record<string, unknown> }> {
return Array.from(this.tools.values()).map((tool) => ({
name: tool.definition.name,
description: tool.definition.description,
inputSchema: zodToJsonSchema(tool.definition.inputSchema),
}));
}
/**
* Execute a tool by name with input validation
*/
async execute(name: string, input: unknown): Promise<ToolResult> {
const startTime = Date.now();
const tool = this.tools.get(name);
if (!tool) {
logger.warning('Tool not found', { name });
return {
success: false,
error: {
error: `Tool not found: ${name}`,
code: 'NOT_FOUND',
retryable: false,
},
};
}
try {
// Validate input against schema
const validatedInput = tool.definition.inputSchema.parse(input);
logger.debug('Executing tool', { name, input: validatedInput });
// Execute handler
const result = await tool.handler(validatedInput);
// Add execution metadata
const meta: ResultMeta = {
durationMs: Date.now() - startTime,
cached: false,
timestamp: new Date().toISOString(),
};
logger.debug('Tool executed successfully', { name, durationMs: meta.durationMs });
return {
...result,
meta: { ...result.meta, ...meta },
};
} catch (error) {
const durationMs = Date.now() - startTime;
// Handle Zod validation errors
if (error instanceof z.ZodError) {
logger.warning('Tool input validation failed', { name, errors: error.errors });
return {
success: false,
error: {
error: 'Input validation failed',
code: 'VALIDATION_ERROR',
details: { issues: error.errors },
retryable: false,
},
meta: {
durationMs,
cached: false,
timestamp: new Date().toISOString(),
},
};
}
// Handle MCP server errors
if (error instanceof McpServerError) {
logger.warning('Tool execution failed', { name, error: error.message });
return {
success: false,
error: error.toResponse(),
meta: {
durationMs,
cached: false,
timestamp: new Date().toISOString(),
},
};
}
// Handle unknown errors
logger.error('Unexpected tool error', { name, error: String(error) });
return {
success: false,
error: formatErrorResponse(error),
meta: {
durationMs,
cached: false,
timestamp: new Date().toISOString(),
},
};
}
}
/**
* Get list of registered tool names
*/
getNames(): string[] {
return Array.from(this.tools.keys());
}
/**
* Get count of registered tools
*/
get size(): number {
return this.tools.size;
}
/**
* Clear all registered tools (for testing)
*/
clear(): void {
this.tools.clear();
logger.debug('Cleared all tools');
}
}
// Global tool registry singleton
let globalRegistry: ToolRegistry | null = null;
/**
* Get the global tool registry
*/
export function getToolRegistry(): ToolRegistry {
if (!globalRegistry) {
globalRegistry = new ToolRegistry();
}
return globalRegistry;
}
/**
* Reset the global tool registry (for testing)
*/
export function resetToolRegistry(): void {
globalRegistry = null;
}