import type { Tool } from '@modelcontextprotocol/server'; import type { MCPPlugin, MCPServer } from '../server/MCPServer.js'; export interface ToolLike { tool: Tool; } /** * Generic base plugin to reduce duplication across tool plugins. * Subclasses should implement creation of tool instances and optionally override * validation, disabling, and per-tool handler wrapping. */ export abstract class BaseToolsPlugin implements MCPPlugin { abstract name: string; protected commands: TTool[] = []; protected logger?: MCPServer['logger']; protected commandMap: Map = new Map(); /** Create tool instances for this plugin */ protected abstract createToolInstances(): TTool[]; /** Optional: validate external dependencies (e.g., CLIs) before registration */ protected async validate(): Promise {} /** Optional: allow disabling a plugin via env var */ protected isDisabled(): boolean { return false; } /** Optional: custom wrapping for tool handlers */ protected getHandlerForTool(_tool: TTool): (params: any) => Promise { // Default: pass params straight to tool.execute if available // Subclasses should override this method to adapt to their tool signature const anyTool = _tool as unknown as { execute?: (params: any) => Promise }; if (!anyTool.execute) { throw new Error('Tool does not implement execute(params)'); } return async (params: any) => { const timeoutMs = this.computeGlobalTimeoutMs(params); const execPromise = anyTool.execute!(params); const label = (_tool as unknown as ToolLike).tool?.name || 'tool'; return this.withTimeout(execPromise, timeoutMs, label); }; } /** Build the internal command map */ protected buildCommandMap(): void { this.commandMap.clear(); for (const command of this.commands) { this.commandMap.set(command.tool.name, command); } } /** Execute a command by name using the plugin's instances */ protected async runCommandByName( commandName: string, params: Record, ): Promise { const command = this.commandMap.get(commandName); if (!command) { throw new Error(`Unknown tool: ${commandName}`); } const handler = this.getHandlerForTool(command); return handler(params); } async initialize(server: MCPServer): Promise { this.logger = server.getLogger(); try { if (this.isDisabled()) { this.logger.info(`${this.name} is disabled via environment variable`); this.commands = []; this.commandMap.clear(); return; } await this.validate(); this.commands = this.createToolInstances(); this.buildCommandMap(); for (const command of this.commands) { server.registerTool(command.tool, async (params: any) => { const handler = this.getHandlerForTool(command); return handler(params); }); } this.logger.info(`${this.constructor.name} initialized with ${this.commands.length} tools.`); } catch (error) { this.logger.error(`Failed to initialize ${this.constructor.name}`, error); throw error; } } getToolFunction(toolName: string): ((params: any) => Promise) | undefined { const command = this.commandMap.get(toolName); if (!command) return undefined; return async (params: any) => { const handler = this.getHandlerForTool(command); return handler(params); }; } async shutdown(): Promise {} /** Determine the global timeout in ms from params or env (TIMEOUT) */ protected computeGlobalTimeoutMs(params: any): number | undefined { const paramTimeout = params && typeof params.timeoutMs === 'number' ? params.timeoutMs : undefined; // Only parse the env var when defined; use base 10 and ignore NaN const envTimeout = process.env.MCP_TIMEOUT !== undefined ? Number.parseInt(process.env.MCP_TIMEOUT, 10) : undefined; const timeoutMs = paramTimeout ?? envTimeout; return Number.isFinite(timeoutMs as number) && (timeoutMs as number) > 0 ? (timeoutMs as number) : undefined; } /** Wrap a promise with a timeout if provided */ protected async withTimeout( promise: Promise, timeoutMs?: number, label?: string, ): Promise { if (!timeoutMs) return promise; return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`${label || 'operation'} timed out after ${timeoutMs}ms`)); }, timeoutMs); promise .then((value) => { clearTimeout(timer); resolve(value); }) .catch((err) => { clearTimeout(timer); reject(err); }); }); } }