import { z } from 'zod';
import { logger } from '../utils/logger.js';
import type { ToolResult } from '../types/tools.js';
/**
* Base class for all MCP tools
* Provides common functionality for validation, execution, and error handling
*/
export abstract class BaseTool<TSchema extends z.ZodType = z.ZodType> {
abstract readonly name: string;
abstract readonly description: string;
abstract readonly schema: TSchema;
/**
* Validate input parameters against the tool's schema
*/
protected validate(params: unknown): z.infer<TSchema> {
try {
return this.schema.parse(params);
} catch (error) {
if (error instanceof z.ZodError) {
const issues = error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');
throw new Error(`Validation failed: ${issues}`);
}
throw error;
}
}
/**
* Format successful response
*/
protected formatSuccess(data: unknown): ToolResult {
return {
content: [
{
type: 'text',
text: typeof data === 'string' ? data : JSON.stringify(data, null, 2),
},
],
};
}
/**
* Format error response
*/
protected formatError(error: Error): ToolResult {
logger.error(`Tool ${this.name} error:`, { error: error.message });
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
/**
* Execute the tool with given parameters
* This method handles validation and error handling automatically
*/
async run(params: unknown): Promise<ToolResult> {
try {
logger.debug(`Executing tool: ${this.name}`, { params });
const validatedParams = this.validate(params);
const result = await this.execute(validatedParams);
logger.info(`Tool ${this.name} executed successfully`);
return this.formatSuccess(result);
} catch (error) {
return this.formatError(error instanceof Error ? error : new Error(String(error)));
}
}
/**
* Abstract method to be implemented by concrete tools
* This is where the actual tool logic goes
*/
protected abstract execute(params: z.infer<TSchema>): Promise<unknown>;
/**
* Get tool definition for MCP server registration
*/
getDefinition() {
return {
name: this.name,
description: this.description,
inputSchema: {
type: 'object',
properties: this.getSchemaProperties(),
required: this.getRequiredFields(),
},
};
}
/**
* Extract properties from Zod schema for MCP tool definition
*/
private getSchemaProperties(): Record<string, unknown> {
// This is a simplified version - in production you'd want more robust schema conversion
if (!(this.schema instanceof z.ZodObject)) {
return {};
}
const shape = this.schema.shape;
const properties: Record<string, unknown> = {};
for (const [key, value] of Object.entries(shape)) {
if (value instanceof z.ZodString) {
properties[key] = { type: 'string', description: value.description || '' };
} else if (value instanceof z.ZodNumber) {
properties[key] = { type: 'number', description: value.description || '' };
} else if (value instanceof z.ZodBoolean) {
properties[key] = { type: 'boolean', description: value.description || '' };
} else if (value instanceof z.ZodEnum) {
properties[key] = {
type: 'string',
enum: value.options,
description: value.description || '',
};
} else if (value instanceof z.ZodOptional) {
// Handle optional fields recursively
const innerType = value.unwrap();
if (innerType instanceof z.ZodString) {
properties[key] = { type: 'string', description: innerType.description || '' };
} else if (innerType instanceof z.ZodNumber) {
properties[key] = { type: 'number', description: innerType.description || '' };
} else if (innerType instanceof z.ZodBoolean) {
properties[key] = { type: 'boolean', description: innerType.description || '' };
}
}
}
return properties;
}
/**
* Get list of required fields from Zod schema
*/
private getRequiredFields(): string[] {
if (!(this.schema instanceof z.ZodObject)) {
return [];
}
const shape = this.schema.shape;
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
if (!(value instanceof z.ZodOptional)) {
required.push(key);
}
}
return required;
}
}