import { z } from "zod"; import "reflect-metadata"; import { TextResponse, ToolResponse } from "./ToolResponse"; import type { PenpotMcpServer, SessionContext } from "./PenpotMcpServer"; import { createLogger } from "./logger"; /** * An empty arguments class for tools that do not require any parameters. */ export class EmptyToolArgs { static schema = {}; } /** * Base class for type-safe tools with automatic schema generation and validation. * * This class provides type safety through automatic validation and strongly-typed * protected methods. All tools should extend this class. * * @template TArgs - The strongly-typed arguments class for this tool */ export abstract class Tool { private readonly logger = createLogger("Tool"); /** monotonically increasing counter for unique tool execution IDs */ private static executionCounter = 0; protected constructor( protected mcpServer: PenpotMcpServer, private inputSchema: z.ZodRawShape ) {} /** * Executes the tool with automatic validation and type safety. * * This method handles the unknown args from the MCP protocol, * delegating to the type-safe implementation. */ async execute(args: unknown): Promise { const executionId = ++Tool.executionCounter; try { let argsInstance: TArgs = args as TArgs; this.logger.info("Tool execution #%d starting: %s", executionId, this.getToolName()); if (this.logger.isLevelEnabled("debug")) { this.logger.debug("Tool execution #%d arguments: %s", executionId, this.formatArgs(argsInstance)); } // execute the actual tool logic let result = await this.executeCore(argsInstance); this.logger.info("Tool execution #%d complete: %s", executionId, this.getToolName()); return result; } catch (error) { this.logger.error("Tool execution #%d failed: %s; error: %s", executionId, this.getToolName(), error); return new TextResponse(`Tool execution failed: ${String(error)}`); } } /** * Formats tool arguments for readable logging. * * Multi-line strings are preserved with proper indentation. */ protected formatArgs(args: TArgs): string { const formatted: string[] = []; for (const [key, value] of Object.entries(args)) { if (typeof value === "string" && value.includes("\n")) { // multi-line string - preserve formatting with indentation const indentedValue = value .split("\n") .map((line, index) => (index === 0 ? line : " " + line)) .join("\n"); formatted.push(` ${key}: ${indentedValue}`); } else if (typeof value === "string") { // single-line string formatted.push(` ${key}: "${value}"`); } else if (value === null || value === undefined) { formatted.push(` ${key}: ${value}`); } else { // other types (numbers, booleans, objects, arrays) const stringified = JSON.stringify(value, null, 2); if (stringified.includes("\n")) { // multi-line JSON - indent it const indented = stringified .split("\n") .map((line, index) => (index === 0 ? line : " " + line)) .join("\n"); formatted.push(` ${key}: ${indented}`); } else { formatted.push(` ${key}: ${stringified}`); } } } return formatted.length > 0 ? "\n" + formatted.join("\n") : "{}"; } /** * Retrieves the current session context. * * @returns The session context for the current request, or undefined if not in a request context */ protected getSessionContext(): SessionContext | undefined { return this.mcpServer.getSessionContext(); } public getInputSchema() { return this.inputSchema; } /** * Returns the tool's unique name. */ public abstract getToolName(): string; /** * Returns the tool's description. */ public abstract getToolDescription(): string; /** * Executes the tool's core logic. * * @param args - The (typed) tool arguments */ protected abstract executeCore(args: TArgs): Promise; }