Skip to main content
Glama

hypertool-mcp

parser.tsโ€ข13.2 kB
/** * YAML parser implementation for persona configuration files * * This module provides comprehensive YAML parsing functionality for persona.yaml/yml files * with robust error handling, line number preservation, and schema validation integration. * * @fileoverview Persona YAML parsing with comprehensive error handling */ import { readFile } from "fs/promises"; import { parse as parseYAML, YAMLParseError } from "yaml"; import { PersonaConfigSchema, validatePersonaConfig, SUPPORTED_PERSONA_FILES, isSupportedPersonaFile, type PersonaConfigData, type SchemaValidationResult, } from "./schemas.js"; import { createYamlParseError, createFileSystemError, createSchemaValidationError, PersonaValidationError, type PersonaError, } from "./errors.js"; import { type PersonaConfig, type ValidationResult, type PersonaValidationErrorInfo, } from "./types.js"; /** * Result of YAML parsing operation */ export interface ParseResult<T = PersonaConfig> { /** Whether parsing was successful */ success: boolean; /** Parsed and validated data if successful */ data?: T; /** Parsing or validation errors */ errors: PersonaValidationErrorInfo[]; /** Parsing or validation warnings */ warnings: PersonaValidationErrorInfo[]; /** Raw YAML parsing errors with line numbers (if any) */ yamlErrors?: YAMLError[]; } /** * YAML parsing error with line number information */ export interface YAMLError { /** Error message from YAML parser */ message: string; /** Line number where error occurred */ line?: number; /** Column number where error occurred */ column?: number; /** Error name/type from YAML parser */ name?: string; /** Original error for debugging */ originalError: Error; } /** * Options for YAML parsing */ export interface ParseOptions { /** Whether to perform schema validation after parsing */ validateSchema?: boolean; /** Whether to include warnings in the result */ includeWarnings?: boolean; /** Custom validation beyond schema (business rules, etc.) */ customValidation?: (data: PersonaConfigData) => ValidationResult; } /** * Parse persona YAML content from string * * @param content - YAML content as string * @param filename - Optional filename for error reporting * @param options - Parse options * @returns Parse result with data, errors, and warnings */ export function parsePersonaYAML( content: string, filename?: string, options: ParseOptions = {} ): ParseResult<PersonaConfig> { const { validateSchema = true, includeWarnings = true } = options; const result: ParseResult<PersonaConfig> = { success: false, errors: [], warnings: [], }; // Step 1: Parse YAML syntax let rawData: unknown; try { rawData = parseYAML(content); } catch (error) { // Handle YAML parsing errors const yamlError = extractYAMLError(error, filename); result.yamlErrors = [yamlError]; result.errors.push({ type: "schema", message: yamlError.message, suggestion: generateYAMLErrorSuggestion(yamlError), severity: "error", }); return result; } // Step 2: Schema validation (if enabled) if (validateSchema) { const schemaResult = validatePersonaConfig(rawData); if (!schemaResult.success) { // Convert schema errors to PersonaValidationErrorInfo result.errors.push( ...schemaResult.errors.map((error) => ({ type: "schema" as const, field: error.path, message: error.message, suggestion: error.suggestion, severity: "error" as const, })) ); // Add warnings from schema validation if (includeWarnings && schemaResult.warnings.length > 0) { result.warnings.push( ...schemaResult.warnings.map((warning) => ({ type: "schema" as const, field: warning.path, message: warning.message, suggestion: warning.suggestion, severity: "warning" as const, })) ); } return result; } // Schema validation successful - assign validated data result.data = schemaResult.data as PersonaConfig; } else { // No schema validation - use raw data (type assertion) result.data = rawData as PersonaConfig; } // Step 3: Custom validation (if provided) if (options.customValidation && result.data) { const customResult = options.customValidation(result.data); // Convert custom validation errors result.errors.push( ...customResult.errors.map((error) => ({ type: "business" as const, message: error.message, severity: "error" as const, })) ); // Add custom warnings if (includeWarnings) { result.warnings.push( ...customResult.warnings.map((warning) => ({ type: "business" as const, message: warning.message, severity: "warning" as const, })) ); } // If custom validation failed, don't mark as success if (!customResult.isValid) { return result; } } // Step 4: Success if no errors result.success = result.errors.length === 0; return result; } /** * Parse persona YAML file from file system * * @param filePath - Path to the YAML file * @param options - Parse options * @returns Parse result with data, errors, and warnings */ export async function parsePersonaYAMLFile( filePath: string, options: ParseOptions = {} ): Promise<ParseResult<PersonaConfig>> { const filename = filePath.split(/[/\\]/).pop() || ""; // Validate file extension first if (!isSupportedPersonaFile(filename)) { return { success: false, errors: [ { type: "schema", message: `Unsupported file type: ${filename}. Expected: ${SUPPORTED_PERSONA_FILES.join(" or ")}`, suggestion: `Rename the file to use one of the supported extensions: ${SUPPORTED_PERSONA_FILES.join(", ")}`, severity: "error", }, ], warnings: [], }; } // Read file content let content: string; try { content = await readFile(filePath, "utf-8"); } catch (error) { const fsError = error as NodeJS.ErrnoException; return { success: false, errors: [ { type: "schema", message: `Failed to read file "${filePath}": ${fsError.message}`, suggestion: fsError.code === "ENOENT" ? `Verify that the file exists at "${filePath}"` : fsError.code === "EACCES" ? `Check file permissions for "${filePath}"` : `Check file accessibility and try again`, severity: "error", }, ], warnings: [], }; } // Parse the content return parsePersonaYAML(content, filename, options); } /** * Check if a file is a persona configuration file * * @param filePath - Path to check * @returns True if the file appears to be a persona config */ export function isPersonaConfigFile(filePath: string): boolean { const filename = filePath.split(/[/\\]/).pop() || ""; return isSupportedPersonaFile(filename); } /** * Get supported persona file extensions * * @returns Array of supported file extensions */ export function getSupportedPersonaFiles(): readonly string[] { return Object.freeze([...SUPPORTED_PERSONA_FILES]); } /** * Extract YAML parsing error information * * @param error - Error from YAML parsing * @param filename - Optional filename for context * @returns Structured YAML error information */ function extractYAMLError(error: unknown, filename?: string): YAMLError { if (error instanceof YAMLParseError) { // YAML parser provides structured error information const linePos = error.linePos?.[0]; return { message: error.message, line: linePos?.line, column: linePos?.col, name: error.name, originalError: error, }; } // Generic error - try to extract line information from message const errorMessage = error instanceof Error ? error.message : String(error); const lineMatch = errorMessage.match(/line (\d+)/i); const columnMatch = errorMessage.match(/column (\d+)/i); return { message: filename ? `YAML parsing error in ${filename}: ${errorMessage}` : `YAML parsing error: ${errorMessage}`, line: lineMatch ? parseInt(lineMatch[1], 10) : undefined, column: columnMatch ? parseInt(columnMatch[1], 10) : undefined, originalError: error instanceof Error ? error : new Error(String(error)), }; } /** * Generate helpful suggestions for YAML parsing errors * * @param yamlError - YAML error information * @returns Suggested fix for the error */ function generateYAMLErrorSuggestion(yamlError: YAMLError): string { const message = yamlError.message.toLowerCase(); // Common YAML error patterns and suggestions if (message.includes("tab") || message.includes("indentation")) { return "Use spaces for indentation, not tabs. YAML requires consistent spacing."; } if (message.includes("expected") && message.includes("found")) { return "Check YAML syntax - ensure proper structure with colons, dashes, and indentation."; } if (message.includes("string")) { return "Wrap string values in quotes if they contain special characters or start with reserved words."; } if (message.includes("duplicate")) { return "Remove or rename duplicate keys - each key in a YAML object must be unique."; } if (message.includes("anchor") || message.includes("alias")) { return "Check YAML anchor (&) and alias (*) syntax for correct format and matching names."; } // Generic suggestions based on line/column information if (yamlError.line && yamlError.column) { return `Check the syntax around line ${yamlError.line}, column ${yamlError.column}. Verify proper indentation and YAML structure.`; } if (yamlError.line) { return `Check the syntax around line ${yamlError.line}. Verify proper indentation and YAML structure.`; } return "Validate YAML syntax using an online YAML validator or editor with YAML support."; } /** * Create a validation result from parsing errors * * @param parseResult - Parse result to convert * @returns ValidationResult compatible with existing patterns */ export function parseResultToValidationResult( parseResult: ParseResult ): ValidationResult { return { isValid: parseResult.success, errors: parseResult.errors, warnings: parseResult.warnings, }; } /** * Parse multiple persona YAML files concurrently * * @param filePaths - Array of file paths to parse * @param options - Parse options * @returns Map of file paths to parse results */ export async function parseMultiplePersonaFiles( filePaths: string[], options: ParseOptions = {} ): Promise<Map<string, ParseResult<PersonaConfig>>> { const results = new Map<string, ParseResult<PersonaConfig>>(); // Parse all files concurrently const parsePromises = filePaths.map(async (filePath) => { try { const result = await parsePersonaYAMLFile(filePath, options); return { filePath, result }; } catch (error) { // Handle unexpected errors during parsing return { filePath, result: { success: false, errors: [ { type: "schema", message: `Failed to parse ${filePath}: ${error instanceof Error ? error.message : String(error)}`, severity: "error", }, ], warnings: [], } as ParseResult<PersonaConfig>, }; } }); const parsedResults = await Promise.allSettled(parsePromises); // Process results parsedResults.forEach((promiseResult, index) => { const filePath = filePaths[index]; if (promiseResult.status === "fulfilled") { results.set(filePath, promiseResult.value.result); } else { // Handle promise rejection results.set(filePath, { success: false, errors: [ { type: "schema", message: `Failed to parse ${filePath}: ${promiseResult.reason}`, severity: "error", }, ], warnings: [], }); } }); return results; } /** * Validate raw YAML content without full parsing * * Quick validation to check if content is valid YAML syntax * without performing schema validation or data processing. * * @param content - YAML content to validate * @returns True if YAML syntax is valid */ export function isValidYAMLSyntax(content: string): boolean { try { parseYAML(content, { strict: false }); return true; } catch { return false; } } /** * Extract persona name from parsed YAML content * * @param content - YAML content * @returns Persona name if found, undefined otherwise */ export function extractPersonaNameFromYAML( content: string ): string | undefined { try { const data = parseYAML(content, { strict: false }); if (data && typeof data === "object" && "name" in data) { return typeof data.name === "string" ? data.name : undefined; } } catch { // Ignore parsing errors for quick extraction } return undefined; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/toolprint/hypertool-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server