Skip to main content
Glama
parser.ts7.83 kB
/** * WP Navigator Role Parser * * Phase B3: YAML parsing and validation for role definitions. * Supports both YAML and JSON formats for flexibility. * * @package WP_Navigator_MCP * @since 1.2.0 */ import * as fs from 'fs'; import * as path from 'path'; import { parse as yamlParse } from 'yaml'; import { Role, RoleSource, LoadedRole, RoleLoadResult, RoleValidationError, RoleSchemaVersionError, ROLE_SCHEMA_VERSION, } from './types.js'; // ============================================================================= // YAML Parser (using 'yaml' library) // ============================================================================= /** * Parse a YAML string into an object. * Uses the 'yaml' library for full YAML 1.2 compliance. */ export function parseYaml(content: string): Record<string, unknown> { const result = yamlParse(content); return result ?? {}; } // ============================================================================= // Role Validation // ============================================================================= /** * Validate role name format (slug) */ function isValidRoleName(name: string): boolean { return /^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) || /^[a-z]$/.test(name); } /** * Validate a parsed role object */ export function validateRole(data: unknown, filePath: string): Role { if (!data || typeof data !== 'object') { throw new RoleValidationError('Role must be a YAML/JSON object', filePath); } const obj = data as Record<string, unknown>; // Validate schema_version if present if (obj.schema_version !== undefined) { if (typeof obj.schema_version !== 'number' || !Number.isInteger(obj.schema_version)) { throw new RoleSchemaVersionError( `Invalid schema_version: expected integer, got ${typeof obj.schema_version}`, filePath, 'Set "schema_version: 1" (must be an integer).' ); } if (obj.schema_version > ROLE_SCHEMA_VERSION) { throw new RoleSchemaVersionError( `Unsupported role schema_version: ${obj.schema_version}`, filePath, `This version of wpnav only understands role schema_version ${ROLE_SCHEMA_VERSION}.\n\nUpgrade wpnav to use this role.` ); } } // Validate name (required) if (!obj.name || typeof obj.name !== 'string') { throw new RoleValidationError('Missing or invalid "name" field (required string)', filePath, 'name'); } if (!isValidRoleName(obj.name)) { throw new RoleValidationError( `Invalid role name "${obj.name}": must be lowercase slug format (e.g., "content-editor")`, filePath, 'name' ); } // Validate description (required) if (!obj.description || typeof obj.description !== 'string') { throw new RoleValidationError('Missing or invalid "description" field (required string)', filePath, 'description'); } // Validate context (required) if (!obj.context || typeof obj.context !== 'string') { throw new RoleValidationError('Missing or invalid "context" field (required string)', filePath, 'context'); } // Validate focus_areas (optional, array of strings) if (obj.focus_areas !== undefined) { if (!Array.isArray(obj.focus_areas)) { throw new RoleValidationError('focus_areas must be an array', filePath, 'focus_areas'); } for (let i = 0; i < obj.focus_areas.length; i++) { if (typeof obj.focus_areas[i] !== 'string') { throw new RoleValidationError(`focus_areas[${i}] must be a string`, filePath, `focus_areas[${i}]`); } } } // Validate avoid (optional, array of strings) if (obj.avoid !== undefined) { if (!Array.isArray(obj.avoid)) { throw new RoleValidationError('avoid must be an array', filePath, 'avoid'); } for (let i = 0; i < obj.avoid.length; i++) { if (typeof obj.avoid[i] !== 'string') { throw new RoleValidationError(`avoid[${i}] must be a string`, filePath, `avoid[${i}]`); } } } // Validate tools (optional, object with allowed/denied arrays) if (obj.tools !== undefined) { if (typeof obj.tools !== 'object' || obj.tools === null) { throw new RoleValidationError('tools must be an object', filePath, 'tools'); } const tools = obj.tools as Record<string, unknown>; if (tools.allowed !== undefined) { if (!Array.isArray(tools.allowed)) { throw new RoleValidationError('tools.allowed must be an array', filePath, 'tools.allowed'); } for (let i = 0; i < tools.allowed.length; i++) { if (typeof tools.allowed[i] !== 'string') { throw new RoleValidationError(`tools.allowed[${i}] must be a string`, filePath, `tools.allowed[${i}]`); } } } if (tools.denied !== undefined) { if (!Array.isArray(tools.denied)) { throw new RoleValidationError('tools.denied must be an array', filePath, 'tools.denied'); } for (let i = 0; i < tools.denied.length; i++) { if (typeof tools.denied[i] !== 'string') { throw new RoleValidationError(`tools.denied[${i}] must be a string`, filePath, `tools.denied[${i}]`); } } } } // Validate priority (optional, number) if (obj.priority !== undefined && typeof obj.priority !== 'number') { throw new RoleValidationError('priority must be a number', filePath, 'priority'); } // Validate version (optional, string) if (obj.version !== undefined && typeof obj.version !== 'string') { throw new RoleValidationError('version must be a string', filePath, 'version'); } // Validate tags (optional, array of strings) if (obj.tags !== undefined) { if (!Array.isArray(obj.tags)) { throw new RoleValidationError('tags must be an array', filePath, 'tags'); } for (let i = 0; i < obj.tags.length; i++) { if (typeof obj.tags[i] !== 'string') { throw new RoleValidationError(`tags[${i}] must be a string`, filePath, `tags[${i}]`); } } } // Validate author (optional, string) if (obj.author !== undefined && typeof obj.author !== 'string') { throw new RoleValidationError('author must be a string', filePath, 'author'); } return obj as unknown as Role; } // ============================================================================= // Role File Loading // ============================================================================= /** * Load a role from a YAML or JSON file */ export function loadRoleFile(filePath: string, source: RoleSource): RoleLoadResult { const ext = path.extname(filePath).toLowerCase(); // Read file let content: string; try { content = fs.readFileSync(filePath, 'utf8'); } catch (error) { return { success: false, error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`, path: filePath, }; } // Parse based on extension let parsed: unknown; try { if (ext === '.json') { parsed = JSON.parse(content); } else if (ext === '.yaml' || ext === '.yml') { parsed = parseYaml(content); } else { return { success: false, error: `Unsupported file extension: ${ext} (use .yaml, .yml, or .json)`, path: filePath, }; } } catch (error) { return { success: false, error: `Failed to parse ${ext}: ${error instanceof Error ? error.message : String(error)}`, path: filePath, }; } // Validate try { const role = validateRole(parsed, filePath); const loadedRole: LoadedRole = { ...role, source, sourcePath: filePath, }; return { success: true, role: loadedRole, path: filePath, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), path: filePath, }; } }

Latest Blog Posts

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/littlebearapps/wp-navigator-mcp'

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