UNPKG

@toolprint/hypertool-mcp

Version:

HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers

319 lines • 13.3 kB
/** * Validate Persona Tool - Validate a specific persona's structure and configuration with detailed reporting */ import { validatePersona, } from "../../../../persona/validator.js"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { stat } from "fs/promises"; // Define the response schema using Zod const validatePersonaResponseSchema = z.object({ success: z.boolean(), result: z.object({ isValid: z.boolean(), errors: z.array(z.object({ type: z.enum(["schema", "business", "tool-resolution", "mcp-config"]), field: z.string().optional(), message: z.string(), suggestion: z.string().optional(), severity: z.enum(["error", "warning"]), })), warnings: z.array(z.object({ type: z.enum(["schema", "business", "tool-resolution", "mcp-config"]), field: z.string().optional(), message: z.string(), suggestion: z.string().optional(), severity: z.enum(["error", "warning"]), })), }), personaPath: z.string(), summary: z.object({ totalErrors: z.number(), totalWarnings: z.number(), validationLayers: z.array(z.string()), pathType: z.enum(["file", "directory"]), }), error: z.string().optional(), }); export const validatePersonaDefinition = { name: "validate-persona", description: "Validate a specific persona's structure and configuration with comprehensive reporting. Performs multi-layer validation including YAML syntax, schema validation, business rules, tool resolution, and MCP config validation. Supports both persona folder paths and individual persona.yaml/yml file paths. Returns detailed validation results with actionable suggestions for fixing any issues found.", inputSchema: { type: "object", properties: { personaPath: { type: "string", description: "Path to the persona folder or persona.yaml/yml file to validate", }, includeWarnings: { type: "boolean", description: "Whether to include warnings in the validation result (default: true)", default: true, }, checkToolAvailability: { type: "boolean", description: "Whether to perform tool availability validation (default: true)", default: true, }, validateMcpConfig: { type: "boolean", description: "Whether to validate MCP config files if present (default: true)", default: true, }, }, required: ["personaPath"], additionalProperties: false, }, outputSchema: zodToJsonSchema(validatePersonaResponseSchema), annotations: { title: "Validate Persona", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }; /** * Determine if the given path is a file or directory * @param path Path to check * @returns "file" | "directory" | "not-found" */ async function getPathType(path) { try { const stats = await stat(path); return stats.isDirectory() ? "directory" : "file"; } catch { return "not-found"; } } /** * Create validation summary with error categorization * @param result ValidationResult * @param pathType Type of path validated * @returns Summary object */ function createValidationSummary(result, pathType) { return { totalErrors: result.errors.length, totalWarnings: result.warnings.length, validationLayers: [ "YAML Syntax & Schema", "Business Rules", "Tool Resolution", "MCP Configuration", ], pathType: pathType, // Filter out "not-found" }; } /** * Generate actionable suggestions based on validation results * @param result ValidationResult * @param personaPath Original path provided * @returns Array of suggestion strings */ function generateActionableSuggestions(result, personaPath) { const suggestions = []; if (!result.isValid) { suggestions.push(`Fix the ${result.errors.length} error(s) found in "${personaPath}"`); // Group suggestions by error type const errorTypes = [...new Set(result.errors.map((e) => e.type))]; for (const errorType of errorTypes) { switch (errorType) { case "schema": suggestions.push("Check YAML syntax and ensure all required fields are present"); break; case "business": suggestions.push("Review business rules: persona name should match folder name, default toolset should exist"); break; case "tool-resolution": suggestions.push("Verify that all tool IDs reference existing tools from connected MCP servers"); break; case "mcp-config": suggestions.push("Check mcp.json file format and server configuration validity"); break; } } } if (result.warnings.length > 0) { suggestions.push(`Consider addressing ${result.warnings.length} warning(s) to improve persona quality`); } return suggestions; } export const createValidatePersonaModule = (deps) => { return { toolName: "validate-persona", definition: validatePersonaDefinition, handler: async (args) => { try { const { personaPath, includeWarnings = true, checkToolAvailability = true, validateMcpConfig = true, } = args || {}; if (!personaPath || typeof personaPath !== "string") { const errorResponse = { success: false, result: { isValid: false, errors: [ { type: "schema", message: "personaPath parameter is required and must be a string", suggestion: "Provide a valid path to a persona folder or persona.yaml/yml file", severity: "error", }, ], warnings: [], }, personaPath: personaPath || "", summary: { totalErrors: 1, totalWarnings: 0, validationLayers: [ "YAML Syntax & Schema", "Business Rules", "Tool Resolution", "MCP Configuration", ], pathType: "file", }, error: "Invalid personaPath parameter", }; return { content: [ { type: "text", text: JSON.stringify(errorResponse), }, ], structuredContent: errorResponse, isError: true, }; } // Check if path exists and determine type const pathType = await getPathType(personaPath); if (pathType === "not-found") { const errorResponse = { success: false, result: { isValid: false, errors: [ { type: "schema", message: `Path "${personaPath}" does not exist`, suggestion: "Check the path and ensure the persona folder or file exists", severity: "error", }, ], warnings: [], }, personaPath, summary: { totalErrors: 1, totalWarnings: 0, validationLayers: [ "YAML Syntax & Schema", "Business Rules", "Tool Resolution", "MCP Configuration", ], pathType: "file", }, error: `File or directory not found: ${personaPath}`, }; return { content: [ { type: "text", text: JSON.stringify(errorResponse), }, ], structuredContent: errorResponse, isError: true, }; } // Set up validation options const validationOptions = { includeWarnings, checkToolAvailability, validateMcpConfig, stopOnFirstError: false, }; // Perform validation using PersonaValidator const validationResult = await validatePersona(personaPath, deps.discoveryEngine, validationOptions); // Generate actionable suggestions const suggestions = generateActionableSuggestions(validationResult, personaPath); // Add suggestions to errors and warnings if they don't already have them const enrichedErrors = validationResult.errors.map((error) => ({ ...error, suggestion: error.suggestion || suggestions.find((s) => s.includes(error.type)) || "Check the persona configuration and documentation", })); const enrichedWarnings = validationResult.warnings.map((warning) => ({ ...warning, suggestion: warning.suggestion || suggestions.find((s) => s.includes(warning.type)) || "Consider improving the persona configuration", })); const response = { success: true, result: { isValid: validationResult.isValid, errors: enrichedErrors, warnings: enrichedWarnings, }, personaPath, summary: createValidationSummary(validationResult, pathType), }; return { content: [ { type: "text", text: JSON.stringify(response), }, ], structuredContent: response, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorResponse = { success: false, result: { isValid: false, errors: [ { type: "schema", message: `Validation failed: ${errorMessage}`, suggestion: "Check that the path is accessible and the persona configuration is valid", severity: "error", }, ], warnings: [], }, personaPath: args?.personaPath || "", summary: { totalErrors: 1, totalWarnings: 0, validationLayers: [ "YAML Syntax & Schema", "Business Rules", "Tool Resolution", "MCP Configuration", ], pathType: "file", }, error: `Failed to validate persona: ${errorMessage}`, }; return { content: [ { type: "text", text: JSON.stringify(errorResponse), }, ], structuredContent: errorResponse, isError: true, }; } }, }; }; //# sourceMappingURL=validate-persona.js.map