@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
JavaScript
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