Skip to main content
Glama
loader.ts7.93 kB
import { readFile } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import yaml from 'yaml'; import { configSchema, type Config, type PartialConfig } from './schema.js'; import { log } from '../utils/logger.js'; /** * CLI arguments that map to config */ export interface CLIArgs { specUrl?: string; specFile?: string; format?: string; upstreamUrl?: string; port?: number; host?: string; config?: string; logLevel?: string; logFormat?: string; includeTools?: string; excludeTools?: string; excludeMethods?: string; includeTags?: string; toolWarning?: boolean; } /** * Environment variables that map to config */ const ENV_MAPPING: Record<string, string> = { SPEC_URL: 'spec.url', SPEC_FILE: 'spec.file', SPEC_FORMAT: 'format', UPSTREAM_URL: 'upstream.baseUrl', PORT: 'server.port', HOST: 'server.host', LOG_LEVEL: 'logging.level', LOG_FORMAT: 'logging.format', AUTH_TYPE: 'auth.type', AUTH_NAME: 'auth.name', AUTH_VALUE: 'auth.value', // Tool filtering TOOLS_INCLUDE: 'tools.include', TOOLS_EXCLUDE: 'tools.exclude', METHODS_EXCLUDE: 'tools.autoDisable.methods', TAGS_INCLUDE: 'tools.includeTags', TOOL_WARNING: 'tools.warnings.enabled', }; /** * Set a nested value in an object using dot notation path */ function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current)) { current[part] = {}; } current = current[part] as Record<string, unknown>; } current[parts[parts.length - 1]] = value; } /** * Interpolate environment variables in string values * Supports ${VAR_NAME} syntax */ function interpolateEnvVars(value: string): string { return value.replace(/\$\{([^}]+)\}/g, (_, envVar: string) => { const envValue = process.env[envVar]; if (envValue === undefined) { log.warn(`Environment variable ${envVar} not found, using empty string`); return ''; } return envValue; }); } /** * Recursively interpolate env vars in an object */ function interpolateConfig(obj: unknown): unknown { if (typeof obj === 'string') { return interpolateEnvVars(obj); } if (Array.isArray(obj)) { return obj.map(interpolateConfig); } if (obj !== null && typeof obj === 'object') { const result: Record<string, unknown> = {}; for (const [key, value] of Object.entries(obj)) { result[key] = interpolateConfig(value); } return result; } return obj; } /** * Load config from a YAML/JSON file */ async function loadConfigFile(filePath: string): Promise<PartialConfig> { if (!existsSync(filePath)) { throw new Error(`Config file not found: ${filePath}`); } const content = await readFile(filePath, 'utf-8'); // Try YAML first (also handles JSON) try { const parsed = yaml.parse(content) as PartialConfig; return parsed; } catch { throw new Error(`Failed to parse config file: ${filePath}`); } } /** * ENV vars that should be parsed as comma-separated arrays */ const ARRAY_ENV_VARS = new Set([ 'TOOLS_INCLUDE', 'TOOLS_EXCLUDE', 'METHODS_EXCLUDE', 'TAGS_INCLUDE', ]); /** * Load config from environment variables */ function loadEnvConfig(): PartialConfig { const config: Record<string, unknown> = {}; for (const [envVar, path] of Object.entries(ENV_MAPPING)) { const value = process.env[envVar]; if (value !== undefined) { let finalValue: unknown = value; // Convert numeric values if (envVar === 'PORT') { const numValue = Number(value); if (!isNaN(numValue)) { finalValue = numValue; } } // Convert boolean values else if (envVar === 'TOOL_WARNING') { finalValue = value.toLowerCase() !== 'false' && value !== '0'; } // Convert comma-separated values to arrays else if (ARRAY_ENV_VARS.has(envVar)) { finalValue = value.split(',').map((s) => s.trim()).filter((s) => s.length > 0); } setNestedValue(config, path, finalValue); } } return config as PartialConfig; } /** * Convert CLI args to config */ function cliArgsToConfig(args: CLIArgs): PartialConfig { const config: PartialConfig = { spec: {}, upstream: {} as PartialConfig['upstream'], server: {}, logging: {}, tools: {}, }; if (args.specUrl) { config.spec.url = args.specUrl; } if (args.specFile) { config.spec.file = args.specFile; } if (args.format) { config.format = args.format as 'openapi' | 'postman'; } if (args.upstreamUrl) { config.upstream = { baseUrl: args.upstreamUrl }; } if (args.port) { config.server!.port = args.port; } if (args.host) { config.server!.host = args.host; } if (args.logLevel) { config.logging!.level = args.logLevel as 'debug' | 'info' | 'warn' | 'error'; } if (args.logFormat) { config.logging!.format = args.logFormat as 'json' | 'pretty'; } // Tool filtering from CLI if (args.includeTools) { config.tools!.include = args.includeTools.split(',').map((s) => s.trim()); } if (args.excludeTools) { config.tools!.exclude = args.excludeTools.split(',').map((s) => s.trim()); } if (args.excludeMethods) { config.tools!.autoDisable = { methods: args.excludeMethods.split(',').map((s) => s.trim().toUpperCase()) as Array<'DELETE' | 'PUT' | 'PATCH' | 'POST'>, }; } if (args.includeTags) { config.tools!.includeTags = args.includeTags.split(',').map((s) => s.trim()); } if (args.toolWarning === false) { config.tools!.warnings = { enabled: false }; } return config; } /** * Deep merge objects (later values override earlier) */ function deepMerge<T extends Record<string, unknown>>(...objects: Partial<T>[]): T { const result: Record<string, unknown> = {}; for (const obj of objects) { for (const [key, value] of Object.entries(obj)) { if (value === undefined) continue; if ( typeof value === 'object' && value !== null && !Array.isArray(value) && typeof result[key] === 'object' && result[key] !== null ) { result[key] = deepMerge(result[key] as Record<string, unknown>, value as Record<string, unknown>); } else { result[key] = value; } } } return result as T; } /** * Load and merge configuration from all sources * Priority: CLI args > ENV vars > config file > defaults */ export async function loadConfig(args: CLIArgs): Promise<Config> { const configs: PartialConfig[] = []; // 1. Load config file if specified if (args.config) { const fileConfig = await loadConfigFile(args.config); configs.push(fileConfig); log.debug('Loaded config from file', { file: args.config }); } // 2. Load from environment const envConfig = loadEnvConfig(); configs.push(envConfig); // 3. Convert CLI args const cliConfig = cliArgsToConfig(args); configs.push(cliConfig); // 4. Merge all configs const merged = deepMerge(...configs); // 5. Interpolate environment variables const interpolated = interpolateConfig(merged) as PartialConfig; // 6. Validate with Zod const result = configSchema.safeParse(interpolated); if (!result.success) { const errors = result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`); throw new Error(`Configuration validation failed:\n${errors.join('\n')}`); } return result.data; } /** * Validate a partial config */ export function validateConfig(config: unknown): Config { const result = configSchema.safeParse(config); if (!result.success) { const errors = result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`); throw new Error(`Configuration validation failed:\n${errors.join('\n')}`); } return result.data; }

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/procoders/openapi-mcp-ts'

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