import type { JsonValue, JsonSchemaType, JsonObject } from "./jsonUtils"; import Ajv from "ajv"; import type { ValidateFunction } from "ajv"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; const ajv = new Ajv(); // Cache for compiled validators const toolOutputValidators = new Map(); /** * Compiles and caches output schema validators for a list of tools * Following the same pattern as SDK's Client.cacheToolOutputSchemas * @param tools Array of tools that may have output schemas */ export function cacheToolOutputSchemas(tools: Tool[]): void { toolOutputValidators.clear(); for (const tool of tools) { if (tool.outputSchema) { try { const validator = ajv.compile(tool.outputSchema); toolOutputValidators.set(tool.name, validator); } catch (error) { console.warn( `Failed to compile output schema for tool ${tool.name}:`, error, ); } } } } /** * Gets the cached output schema validator for a tool * Following the same pattern as SDK's Client.getToolOutputValidator * @param toolName Name of the tool * @returns The compiled validator function, or undefined if not found */ export function getToolOutputValidator( toolName: string, ): ValidateFunction | undefined { return toolOutputValidators.get(toolName); } /** * Validates structured content against a tool's output schema * Returns validation result with detailed error messages * @param toolName Name of the tool * @param structuredContent The structured content to validate * @returns An object with isValid boolean and optional error message */ export function validateToolOutput( toolName: string, structuredContent: unknown, ): { isValid: boolean; error?: string } { const validator = getToolOutputValidator(toolName); if (!validator) { return { isValid: true }; // No validator means no schema to validate against } const isValid = validator(structuredContent); if (!isValid) { return { isValid: false, error: ajv.errorsText(validator.errors), }; } return { isValid: true }; } /** * Checks if a tool has an output schema * @param toolName Name of the tool * @returns true if the tool has an output schema */ export function hasOutputSchema(toolName: string): boolean { return toolOutputValidators.has(toolName); } /** * Generates a default value based on a JSON schema type * @param schema The JSON schema definition * @returns A default value matching the schema type, or null for non-required fields */ export function generateDefaultValue(schema: JsonSchemaType): JsonValue { if ("default" in schema) { return schema.default; } if (!schema.required) { if (schema.type === "array") return []; if (schema.type === "object") return {}; return undefined; } switch (schema.type) { case "string": return ""; case "number": case "integer": return 0; case "boolean": return false; case "array": return []; case "object": { if (!schema.properties) return {}; const obj: JsonObject = {}; Object.entries(schema.properties) .filter(([, prop]) => prop.required) .forEach(([key, prop]) => { const value = generateDefaultValue(prop); obj[key] = value; }); return obj; } default: return null; } } /** * Formats a field key into a human-readable label * @param key The field key to format * @returns A formatted label string */ export function formatFieldLabel(key: string): string { return key .replace(/([A-Z])/g, " $1") // Insert space before capital letters .replace(/_/g, " ") // Replace underscores with spaces .replace(/^\w/, (c) => c.toUpperCase()); // Capitalize first letter }