Skip to main content
Glama
IBM
by IBM
configParser.ts12.6 kB
/** * @fileoverview YAML configuration parser with validation and environment variable interpolation * Handles parsing, validation, and processing of YAML tool configurations * * @module src/utils/yaml/yamlParser */ import { readFileSync, existsSync } from "fs"; import { resolve } from "path"; import { load as yamlLoad } from "js-yaml"; import { ProcessedSQLTool } from "./types.js"; import { ParsingResult, SqlToolsConfig } from "../../schemas/index.js"; import { SqlToolParameter } from "../../schemas/index.js"; import { ErrorHandler, logger } from "@/utils/internal/index.js"; import { requestContextService, RequestContext, } from "@/utils/internal/requestContext.js"; // Import schemas from centralized location import { SqlToolsConfigSchema, SqlToolParameterSchema, } from "@/ibmi-mcp-server/schemas/index.js"; import { JsonRpcErrorCode, McpError } from "@/types-global/errors.js"; /** * YAML configuration parser with validation and environment variable interpolation */ export class ConfigParser { /** * Parse and validate a YAML tools configuration file * @param filePath - Path to the YAML configuration file * @param context - Request context for logging * @returns Parsing result with validation information */ static async parseYamlFile( filePath: string, context?: RequestContext, ): Promise<ParsingResult> { const operationContext = context || requestContextService.createRequestContext({ operation: "ParseYamlFile", filePath, }); return ErrorHandler.tryCatch( async () => { logger.info( { ...operationContext, filePath, }, "Parsing YAML configuration file", ); // Check if file exists const resolvedPath = resolve(filePath); if (!existsSync(resolvedPath)) { throw new McpError( JsonRpcErrorCode.ValidationError, `YAML configuration file not found: ${resolvedPath}`, ); } // Read file content const fileContent = readFileSync(resolvedPath, "utf8"); logger.debug( { ...operationContext, contentLength: fileContent.length, }, "YAML file content loaded", ); // Interpolate environment variables at startup // TODO: In the future, this should use client-provided environment variables // instead of server-side environment variables const interpolatedContent = this.interpolateEnvironmentVariables( fileContent, operationContext, ); // Parse YAML const parsedYaml = yamlLoad(interpolatedContent); // Validate against schema const validationResult = SqlToolsConfigSchema.safeParse(parsedYaml); if (!validationResult.success) { const errors = validationResult.error.errors.map( (err) => `${err.path.join(".")}: ${err.message}`, ); logger.error( { ...operationContext, errors, }, "YAML validation failed", ); return { success: false, errors, }; } const config = validationResult.data as SqlToolsConfig; // Additional validation - check tool source references const sourceValidationErrors = this.validateToolSourceReferences(config); if (sourceValidationErrors.length > 0) { logger.error( { ...operationContext, errors: sourceValidationErrors, }, "Source reference validation failed", ); return { success: false, errors: sourceValidationErrors, }; } // Note: Toolset validation is intentionally deferred to post-merge validation // in YamlConfigBuilder to support cross-file tool references with YAML_MERGE_ARRAYS // Additional validation - check tool-specific requirements const toolValidationErrors = this.validateToolRequirements(config); if (toolValidationErrors.length > 0) { logger.error( { ...operationContext, errors: toolValidationErrors, }, "Tool requirements validation failed", ); return { success: false, errors: toolValidationErrors, }; } // Process tools const processedTools = this.processTools(config); // Count disabled tools const totalTools = config.tools ? Object.keys(config.tools).length : 0; const disabledTools = config.tools ? Object.values(config.tools).filter((tool) => tool.enabled === false) .length : 0; const enabledTools = totalTools - disabledTools; // Generate statistics const stats = { sourceCount: config.sources ? Object.keys(config.sources).length : 0, toolCount: totalTools, enabledToolCount: enabledTools, disabledToolCount: disabledTools, toolsetCount: config.toolsets ? Object.keys(config.toolsets).length : 0, totalParameterCount: config.tools ? Object.values(config.tools).reduce( (sum, tool) => sum + (tool.parameters?.length || 0), 0, ) : 0, }; logger.info( { ...operationContext, stats, }, "YAML configuration parsed successfully", ); return { success: true, config, processedTools, stats, }; }, { operation: "ParseYamlFile", context: operationContext, errorCode: JsonRpcErrorCode.ConfigurationError, }, ); } /** * Interpolate environment variables in YAML content * Supports ${VAR_NAME} syntax * @param content - YAML content string * @param context - Request context for logging * @returns Content with environment variables interpolated * @private */ private static interpolateEnvironmentVariables( content: string, context: RequestContext, ): string { return content.replace(/\$\{([^}]+)\}/g, (match, varName) => { const envValue = process.env[varName]; if (envValue === undefined) { logger.debug( { ...context, varName, }, `Environment variable ${varName} not found, keeping placeholder`, ); return match; } logger.debug( { ...context, varName, envValue: envValue.substring(0, 10) + "...", // Only show first 10 chars for security }, `Environment variable ${varName} found and substituted`, ); return envValue; }); } /** * Interpolate environment variables using client-provided environment * Supports ${VAR_NAME} syntax * * NOT USED CURRENTLY * @param content - Content string with environment variable placeholders * @param clientEnvironment - Environment variables provided by the client * @param context - Request context for logging * @returns Content with environment variables interpolated */ static interpolateClientEnvironmentVariables( content: string, clientEnvironment: Record<string, string> = {}, context?: RequestContext, ): string { const operationContext = context || requestContextService.createRequestContext({ operation: "InterpolateClientEnvironmentVariables", }); logger.debug( { ...operationContext, contentLength: content.length, availableClientVars: Object.keys(clientEnvironment), }, "Starting client environment variable interpolation", ); return content.replace(/\$\{([^}]+)\}/g, (match, varName) => { const envValue = clientEnvironment[varName]; if (envValue === undefined) { logger.debug( { ...operationContext, match, varName, availableClientVars: Object.keys(clientEnvironment), }, `Client environment variable ${varName} not found, keeping placeholder`, ); return match; } logger.debug( { ...operationContext, varName, envValue: envValue.substring(0, 10) + "...", // Only show first 10 chars for security }, `Client environment variable ${varName} found and substituted`, ); return envValue; }); } /** * Validate that all tool source references exist in the sources section * @param config - Parsed YAML configuration * @returns Array of validation errors * @private */ private static validateToolSourceReferences( config: SqlToolsConfig, ): string[] { const errors: string[] = []; // Skip validation if either section is missing if (!config.sources || !config.tools) { return errors; } const sourceNames = Object.keys(config.sources); Object.entries(config.tools).forEach(([toolName, tool]) => { if (!sourceNames.includes(tool.source)) { errors.push( `Tool '${toolName}' references unknown source '${tool.source}'. Available sources: ${sourceNames.join(", ")}`, ); } }); return errors; } /** * Validate tool-specific requirements * @param config - Parsed YAML configuration * @returns Array of validation errors * @private */ private static validateToolRequirements(config: SqlToolsConfig): string[] { const errors: string[] = []; // Skip validation if tools section is missing if (!config.tools) { return errors; } Object.entries(config.tools).forEach(([toolName, tool]) => { // All tools must have a statement if (!tool.statement || tool.statement.trim().length === 0) { errors.push(`Tool '${toolName}' must have a non-empty statement field`); } }); return errors; } /** * Process tools from YAML configuration into runtime format * @param config - Validated YAML configuration * @returns Array of processed tools * @private */ private static processTools(config: SqlToolsConfig): ProcessedSQLTool[] { const processedTools: ProcessedSQLTool[] = []; // Return empty array if tools section is missing if (!config.tools) { return processedTools; } // Build toolset membership map const toolToToolsets: Record<string, string[]> = {}; if (config.toolsets) { Object.entries(config.toolsets).forEach(([toolsetName, toolset]) => { toolset.tools.forEach((toolName) => { if (!toolToToolsets[toolName]) { toolToToolsets[toolName] = []; } toolToToolsets[toolName].push(toolsetName); }); }); } // Process each tool Object.entries(config.tools).forEach(([toolName, tool]) => { // Skip disabled tools if (tool.enabled === false) { logger.debug( { toolName, enabled: false, }, `Skipping disabled tool: ${toolName}`, ); return; } const source = config.sources?.[tool.source]; const toolsets = toolToToolsets[toolName] || []; processedTools.push({ name: toolName, config: tool, source: source!, toolsets, metadata: { name: toolName, description: tool.description, domain: tool.domain, category: tool.category, toolsets, }, }); }); return processedTools; } /** * Validate a single tool parameter definition * @param parameter - Parameter definition to validate * @returns Validation result */ static validateParameter(parameter: SqlToolParameter): { valid: boolean; errors: string[]; } { const result = SqlToolParameterSchema.safeParse(parameter); if (result.success) { return { valid: true, errors: [] }; } const errors = result.error.errors.map((err) => err.message); return { valid: false, errors }; } /** * Get available parameter types * @returns Array of supported parameter types */ static getAvailableParameterTypes(): string[] { return ["string", "number", "boolean", "integer"]; } }

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/IBM/ibmi-mcp'

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