UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

296 lines 11 kB
import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; import { substituteEnvVars } from '../config/env-substitution.js'; import { getConfigPath } from '../config/paths.js'; import { logError } from '../utils/message-queue.js'; /** * Parse MCP servers from .mcp.json config object. * Only supports object format: { "mcpServers": { "serverName": { ... } } } */ function parseMCPServers(config) { if (typeof config !== 'object' || config === null) { return null; } const configObj = config; if ('mcpServers' in configObj && configObj.mcpServers && typeof configObj.mcpServers === 'object' && !Array.isArray(configObj.mcpServers)) { return Object.entries(configObj.mcpServers).map(([name, serverConfig]) => ({ name, ...serverConfig, })); } return null; } /** * Map a raw parsed server object to MCPServerConfig */ function mapServerConfig(server) { const typedServer = server; return { name: typedServer.name, transport: typedServer.transport, command: typedServer.command, args: typedServer.args, env: typedServer.env, url: typedServer.url, headers: typedServer.headers, timeout: typedServer.timeout, alwaysAllow: typedServer.alwaysAllow, description: typedServer.description, tags: typedServer.tags, enabled: typedServer.enabled, }; } /** * Load project-level MCP configuration from .mcp.json */ export function loadProjectMCPConfig() { const configPath = join(process.cwd(), '.mcp.json'); if (!existsSync(configPath)) { return []; } try { const rawData = readFileSync(configPath, 'utf-8'); const config = JSON.parse(rawData); const mcpServers = parseMCPServers(config); if (Array.isArray(mcpServers) && mcpServers.length > 0) { const processedServers = substituteEnvVars(mcpServers); return processedServers.map((server) => ({ server: mapServerConfig(server), source: 'project', })); } } catch (error) { logError(`Failed to load MCP config from ${configPath}: ${String(error)}`); } return []; } /** * Load global MCP configuration from ~/.config/nanocoder/.mcp.json */ export function loadGlobalMCPConfig() { const configDir = getConfigPath(); const configPath = join(configDir, '.mcp.json'); if (!existsSync(configPath)) { return []; } try { const rawData = readFileSync(configPath, 'utf-8'); const config = JSON.parse(rawData); const mcpServers = parseMCPServers(config); if (Array.isArray(mcpServers) && mcpServers.length > 0) { const processedServers = substituteEnvVars(mcpServers); return processedServers.map((server) => ({ server: mapServerConfig(server), source: 'global', })); } } catch (error) { logError(`Failed to load MCP config from ${configPath}: ${String(error)}`); } return []; } /** * Merge project-level and global MCP configurations * ALL servers from both locations are loaded (project servers shown first) * No overriding - each unique server is preserved */ export function mergeMCPConfigs(projectServers, globalServers, envServers = []) { const serverMap = new Map(); // Add env servers first (highest priority, displayed first in UI) for (const envServer of envServers) { serverMap.set(envServer.server.name, envServer); } // Add project servers next for (const projectServer of projectServers) { if (!serverMap.has(projectServer.server.name)) { serverMap.set(projectServer.server.name, projectServer); } } // Add global servers (only if not already added from project or env) for (const globalServer of globalServers) { if (!serverMap.has(globalServer.server.name)) { serverMap.set(globalServer.server.name, globalServer); } } return Array.from(serverMap.values()); } /** * Load all MCP configurations with proper hierarchy and merging */ export function loadAllMCPConfigs() { const projectServers = loadProjectMCPConfig(); // Skip loading global servers in test environment to allow test isolation const globalServers = process.env.NODE_ENV === 'test' ? [] : loadGlobalMCPConfig(); const envServers = loadEnvMCPConfigs(); return mergeMCPConfigs(projectServers, globalServers, envServers); } /** * Load MCP configuration from environment variables (highest precedence) */ function loadEnvMCPConfigs() { let rawData = process.env.NANOCODER_MCPSERVERS; if (!rawData && process.env.NANOCODER_MCPSERVERS_FILE) { if (existsSync(process.env.NANOCODER_MCPSERVERS_FILE)) { try { rawData = readFileSync(process.env.NANOCODER_MCPSERVERS_FILE, 'utf-8'); } catch (error) { logError(`Failed to read NANOCODER_MCPSERVERS_FILE at ${process.env.NANOCODER_MCPSERVERS_FILE}: ${String(error)}`); } } } if (!rawData) { return []; } try { const config = JSON.parse(rawData); // Accept direct array format (env vars) or standard .mcp.json wrapper format let servers = null; if (Array.isArray(config)) { servers = config; } else { servers = parseMCPServers(config); } if (Array.isArray(servers) && servers.length > 0) { const processedServers = substituteEnvVars(servers); return processedServers.map((server) => ({ server: mapServerConfig(server), source: 'env', })); } } catch (error) { logError(`Failed to parse MCP configs from environment: ${String(error)}`); } return []; } /** * Load provider configurations from all available levels (project and global) * This mirrors the approach used for MCP servers to support hierarchical loading */ export function loadAllProviderConfigs() { const projectProviders = loadProjectProviderConfigs(); // Skip loading global providers in test environment to allow test isolation const globalProviders = process.env.NODE_ENV === 'test' ? [] : loadGlobalProviderConfigs(); const envProviders = loadEnvProviderConfigs(); // Merge providers with env providers taking highest precedence const providerMap = new Map(); // Add global providers first (lowest priority) for (const provider of globalProviders) { providerMap.set(provider.name, provider); } // Add project providers (medium priority) - overrides global ones for (const provider of projectProviders) { providerMap.set(provider.name, provider); } // Add env providers last (highest priority) - overrides all for (const provider of envProviders) { providerMap.set(provider.name, provider); } return Array.from(providerMap.values()); } /** * Load provider configurations from project-level files */ function loadProjectProviderConfigs() { // Try to find provider configs in project-level config files const configPath = join(process.cwd(), 'agents.config.json'); if (existsSync(configPath)) { try { const rawData = readFileSync(configPath, 'utf-8'); const config = JSON.parse(rawData); if (config.nanocoder && Array.isArray(config.nanocoder.providers)) { // Apply environment variable substitution const processedProviders = substituteEnvVars(config.nanocoder.providers); return processedProviders; } else if (Array.isArray(config.providers)) { // Apply environment variable substitution const processedProviders = substituteEnvVars(config.providers); return processedProviders; } } catch (error) { logError(`Failed to load project provider config from ${configPath}: ${String(error)}`); } } return []; } /** * Load provider configurations from global config files using the same path resolution as the original system */ function loadGlobalProviderConfigs() { // Use the same path resolution logic as getClosestConfigFile const configDir = getConfigPath(); // Look for a user level config. const configPath = join(configDir, 'agents.config.json'); if (existsSync(configPath)) { return loadProviderConfigFromFile(configPath); } // Note: We don't check CWD here as that's handled by project-level loading return []; } // Helper function to load provider config from a specific file function loadProviderConfigFromFile(filePath) { try { const rawData = readFileSync(filePath, 'utf-8'); const config = JSON.parse(rawData); if (config.nanocoder && Array.isArray(config.nanocoder.providers)) { // Apply environment variable substitution const processedProviders = substituteEnvVars(config.nanocoder.providers); return processedProviders; } else if (Array.isArray(config.providers)) { // Apply environment variable substitution const processedProviders = substituteEnvVars(config.providers); return processedProviders; } } catch (error) { logError(`Failed to load provider config from ${filePath}: ${String(error)}`); } return []; } /** * Load provider configurations from environment variables */ function loadEnvProviderConfigs() { let rawData = process.env.NANOCODER_PROVIDERS; if (!rawData && process.env.NANOCODER_PROVIDERS_FILE) { if (existsSync(process.env.NANOCODER_PROVIDERS_FILE)) { try { rawData = readFileSync(process.env.NANOCODER_PROVIDERS_FILE, 'utf-8'); } catch (error) { logError(`Failed to read NANOCODER_PROVIDERS_FILE at ${process.env.NANOCODER_PROVIDERS_FILE}: ${String(error)}`); } } } if (!rawData) { return []; } try { const config = JSON.parse(rawData); // Accept direct array format or standard agents.config.json wrapper format if (Array.isArray(config)) { return substituteEnvVars(config); } else if (config.nanocoder && Array.isArray(config.nanocoder.providers)) { return substituteEnvVars(config.nanocoder.providers); } else if (Array.isArray(config.providers)) { return substituteEnvVars(config.providers); } } catch (error) { logError(`Failed to parse provider configs from environment: ${String(error)}`); } return []; } //# sourceMappingURL=mcp-config-loader.js.map