UNPKG

codemesh

Version:

Execute TypeScript code against multiple MCP servers, weaving them together into powerful workflows

171 lines (170 loc) 6.34 kB
import { z } from 'zod'; import { readFileSync, existsSync } from 'node:fs'; import { resolve, join } from 'node:path'; import { logger } from './logger.js'; // MCP Server Configuration Schema (compatible with VSCode's .vscode/mcp.json) const McpServerConfigSchema = z.object({ id: z.string().describe('Unique identifier for the server'), name: z.string().describe('Human-readable name for the server'), type: z.enum(['stdio', 'http', 'websocket']).describe('Connection type'), // For stdio servers command: z.array(z.string()).optional().describe('Command and arguments to start the server'), cwd: z.string().optional().describe('Working directory for the server process'), env: z.record(z.string()).optional().describe('Environment variables for the server'), // For HTTP/WebSocket servers url: z.string().url().optional().describe('Server URL'), // Optional configuration timeout: z.number().optional().describe('Connection timeout in milliseconds'), retries: z.number().optional().describe('Number of connection retries'), }); const LoggingConfigSchema = z.object({ enabled: z.boolean().default(false).describe('Enable local file logging'), level: z.enum(['debug', 'info', 'warn', 'error']).default('info').describe('Log level'), logDir: z.string().default('.codemesh/logs').describe('Directory for log files'), }).optional(); const McpConfigSchema = z.object({ logging: LoggingConfigSchema, servers: z.array(McpServerConfigSchema).describe('List of MCP servers to connect to'), }); export class ConfigLoader { static instance; config = null; constructor() { } static getInstance() { if (!ConfigLoader.instance) { ConfigLoader.instance = new ConfigLoader(); } return ConfigLoader.instance; } /** * Expand environment variable references in a string value * Supports: ${VAR} and ${VAR:-default} */ expandEnvVar(value) { // Match ${VAR} or ${VAR:-default} return value.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_, varName, defaultValue) => { const envValue = process.env[varName]; if (envValue !== undefined) { return envValue; } if (defaultValue !== undefined) { return defaultValue; } logger.warn(`Environment variable ${varName} not found and no default provided`); return ''; }); } /** * Recursively expand environment variables in config object */ expandEnvVars(obj) { if (typeof obj === 'string') { return this.expandEnvVar(obj); } if (Array.isArray(obj)) { return obj.map(item => this.expandEnvVars(item)); } if (obj && typeof obj === 'object') { const expanded = {}; for (const [key, value] of Object.entries(obj)) { expanded[key] = this.expandEnvVars(value); } return expanded; } return obj; } /** * Auto-discover and load MCP configuration from project root * Looks for .codemesh/config.json in PWD (for stdio servers) */ loadConfigAuto() { // Use PWD if available, otherwise fall back to process.cwd() const pwd = process.env.PWD || process.cwd(); const configPath = join(pwd, '.codemesh', 'config.json'); if (!existsSync(configPath)) { throw new Error(`No .codemesh/config.json found in project root: ${pwd}\n` + `Please create ${configPath} with your MCP server configuration.`); } return this.loadConfig(configPath); } /** * Load MCP configuration from a JSON file */ loadConfig(configPath) { try { const configFile = resolve(configPath); const configData = readFileSync(configFile, 'utf-8'); const parsedConfig = JSON.parse(configData); // Expand environment variables before validation const expandedConfig = this.expandEnvVars(parsedConfig); // Validate the configuration against our schema this.config = McpConfigSchema.parse(expandedConfig); logger.error(`📄 Loaded MCP configuration from ${configFile}`); logger.error(`📡 Found ${this.config.servers.length} MCP server(s) configured`); return this.config; } catch (error) { if (error instanceof z.ZodError) { logger.error('❌ Invalid MCP configuration format:'); error.errors.forEach((err) => { logger.error(` - ${err.path.join('.')}: ${err.message}`); }); throw new Error('Invalid MCP configuration format'); } else if (error instanceof SyntaxError) { throw new Error(`Invalid JSON in MCP configuration: ${error.message}`); } else { throw new Error(`Failed to load MCP configuration: ${error}`); } } } /** * Get the current loaded configuration */ getConfig() { if (!this.config) { throw new Error('No MCP configuration loaded. Call loadConfig() first.'); } return this.config; } /** * Get a specific server configuration by ID */ getServerConfig(serverId) { if (!this.config) { return null; } return this.config.servers.find((server) => server.id === serverId) || null; } /** * Get all HTTP-based servers (easier to connect to for discovery) */ getHttpServers() { if (!this.config) { return []; } return this.config.servers.filter((server) => server.type === 'http'); } /** * Get all stdio-based servers */ getStdioServers() { if (!this.config) { return []; } return this.config.servers.filter((server) => server.type === 'stdio'); } /** * Validate if a configuration is valid without loading it */ static validateConfig(configData) { try { McpConfigSchema.parse(configData); return true; } catch { return false; } } }