@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
341 lines • 13.9 kB
JavaScript
import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { substituteEnvVars } from '../config/env-substitution.js';
import { getConfigPath } from '../config/paths.js';
import { logError } from '../utils/message-queue.js';
/**
* Show deprecation warning for array format MCP configuration
*/
function showArrayFormatDeprecationWarning() {
logError('Warning: Array format for MCP servers is deprecated.');
logError('Please use object format: { "mcpServers": { "serverName": { ... } } }');
}
/**
* Show deprecation warning for MCP servers in agents.config.json
*/
function showAgentsConfigDeprecationWarning() {
const _configPath = join(getConfigPath(), '.mcp.json');
logError('Warning: MCP servers in agents.config.json are deprecated.');
logError(`Please migrate to ${_configPath}. See documentation for details.`);
}
/**
* Parse MCP servers from config object, supporting both array and object formats
* Converts array format to normalized server list with deprecation warning
*/
function parseMCPServers(config) {
if (typeof config !== 'object' || config === null) {
return null;
}
const configObj = config;
let mcpServers = null;
let usedArrayFormat = false;
// Direct array format at root
if (Array.isArray(config)) {
mcpServers = config;
usedArrayFormat = true;
}
else if ('nanocoder' in configObj &&
configObj.nanocoder &&
typeof configObj.nanocoder === 'object' &&
'mcpServers' in configObj.nanocoder &&
Array.isArray(configObj.nanocoder.mcpServers)) {
mcpServers = configObj.nanocoder
.mcpServers;
usedArrayFormat = true;
}
else if ('mcpServers' in configObj && Array.isArray(configObj.mcpServers)) {
mcpServers = configObj.mcpServers;
usedArrayFormat = true;
}
else if ('mcpServers' in configObj &&
configObj.mcpServers &&
typeof configObj.mcpServers === 'object' &&
!Array.isArray(configObj.mcpServers)) {
// Claude Code format: { "serverName": { ...serverConfig } }
mcpServers = Object.entries(configObj.mcpServers).map(([name, serverConfig]) => ({
name,
...serverConfig,
}));
}
// Show deprecation warning if array format was used
if (usedArrayFormat && mcpServers && mcpServers.length > 0) {
showArrayFormatDeprecationWarning();
}
return mcpServers;
}
/**
* 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) {
// Apply environment variable substitution
const processedServers = substituteEnvVars(mcpServers);
return processedServers.map((server) => {
const typedServer = server;
return {
server: {
name: typedServer.name,
transport: typedServer.transport,
command: typedServer.command,
args: typedServer.args,
env: typedServer.env,
url: typedServer.url,
headers: typedServer.headers,
auth: typedServer.auth,
timeout: typedServer.timeout,
reconnect: typedServer.reconnect,
description: typedServer.description,
tags: typedServer.tags,
enabled: typedServer.enabled,
},
source: 'project',
};
});
}
}
catch (error) {
logError(`Failed to load MCP config from ${configPath}: ${String(error)}`);
}
return [];
}
/**
* Load global MCP configuration from ~/.config/nanocoder/.mcp.json
* Falls back to agents.config.json with deprecation warning
*/
export function loadGlobalMCPConfig() {
const configDir = getConfigPath();
const newConfigPath = join(configDir, '.mcp.json');
// First, check the new .mcp.json location
if (existsSync(newConfigPath)) {
try {
const rawData = readFileSync(newConfigPath, '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) => {
const typedServer = server;
return {
server: {
name: typedServer.name,
transport: typedServer.transport,
command: typedServer.command,
args: typedServer.args,
env: typedServer.env,
url: typedServer.url,
headers: typedServer.headers,
auth: typedServer.auth,
timeout: typedServer.timeout,
reconnect: typedServer.reconnect,
description: typedServer.description,
tags: typedServer.tags,
enabled: typedServer.enabled,
},
source: 'global',
};
});
}
}
catch (error) {
logError(`Failed to load MCP config from ${newConfigPath}: ${String(error)}`);
}
return [];
}
// Fallback to legacy agents.config.json with deprecation warning
const homePath = join(homedir(), '.agents.config.json');
if (existsSync(homePath)) {
return loadMCPConfigFromAgentsConfig(homePath);
}
const legacyConfigPath = join(configDir, 'agents.config.json');
if (existsSync(legacyConfigPath)) {
return loadMCPConfigFromAgentsConfig(legacyConfigPath);
}
return [];
}
/**
* Load MCP config from legacy agents.config.json with deprecation warning
*/
function loadMCPConfigFromAgentsConfig(filePath) {
try {
const rawData = readFileSync(filePath, 'utf-8');
const config = JSON.parse(rawData);
let mcpServers = null;
let hasMcpServers = false;
if (config.nanocoder && Array.isArray(config.nanocoder.mcpServers)) {
mcpServers = config.nanocoder.mcpServers;
hasMcpServers = mcpServers !== null && mcpServers.length > 0;
}
else if (config.nanocoder &&
config.nanocoder.mcpServers &&
typeof config.nanocoder.mcpServers === 'object' &&
!Array.isArray(config.nanocoder.mcpServers)) {
// Claude Code format in agents.config.json
mcpServers = Object.entries(config.nanocoder.mcpServers).map(([name, serverConfig]) => ({
name,
...serverConfig,
}));
hasMcpServers = mcpServers && mcpServers.length > 0;
}
// Show deprecation warning if MCP servers found in agents.config.json
if (hasMcpServers) {
showAgentsConfigDeprecationWarning();
}
if (Array.isArray(mcpServers) && mcpServers.length > 0) {
const processedServers = substituteEnvVars(mcpServers);
return processedServers.map((server) => {
const typedServer = server;
return {
server: {
name: typedServer.name,
transport: typedServer.transport,
command: typedServer.command,
args: typedServer.args,
env: typedServer.env,
url: typedServer.url,
headers: typedServer.headers,
auth: typedServer.auth,
timeout: typedServer.timeout,
reconnect: typedServer.reconnect,
description: typedServer.description,
tags: typedServer.tags,
enabled: typedServer.enabled,
},
source: 'global',
};
});
}
}
catch (error) {
logError(`Failed to load MCP config from ${filePath}: ${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) {
const serverMap = new Map();
// Add project servers first (displayed first in UI)
for (const projectServer of projectServers) {
serverMap.set(projectServer.server.name, projectServer);
}
// Add global servers (only if not already added from project)
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();
return mergeMCPConfigs(projectServers, globalServers);
}
/**
* 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();
// Merge providers with project providers taking precedence over global ones
// If a provider with the same name exists in both, project version wins
const providerMap = new Map();
// Add global providers first (lower priority)
for (const provider of globalProviders) {
providerMap.set(provider.name, provider);
}
// Add project providers (higher priority) - they will override global ones
for (const provider of projectProviders) {
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();
// Check the $HOME for a hidden file. This should only be for legacy support
const homePath = join(homedir(), '.agents.config.json');
if (existsSync(homePath)) {
const providers = loadProviderConfigFromFile(homePath);
if (providers.length > 0) {
return providers;
}
}
// Next, lets 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 [];
}
//# sourceMappingURL=mcp-config-loader.js.map