UNPKG

@probelabs/probe

Version:

Node.js wrapper for the probe code search tool

313 lines (281 loc) 9.01 kB
/** * MCP Configuration Manager * Handles loading and parsing MCP server configurations similar to Claude */ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs'; import { join, dirname } from 'path'; import { homedir } from 'os'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Default MCP configuration structure */ const DEFAULT_CONFIG = { mcpServers: { // Example probe server configuration 'probe-local': { command: 'node', args: [join(__dirname, '../../../examples/chat/mcpServer.js')], transport: 'stdio', enabled: false }, 'probe-npm': { command: 'npx', args: ['-y', '@probelabs/probe@latest', 'mcp'], transport: 'stdio', enabled: false } } }; /** * Load MCP configuration from a specific file path * @param {string} configPath - Path to MCP configuration file * @returns {Object} Configuration object * @throws {Error} If file doesn't exist or is invalid */ export function loadMCPConfigurationFromPath(configPath) { if (!configPath) { throw new Error('Config path is required'); } if (!existsSync(configPath)) { throw new Error(`MCP configuration file not found: ${configPath}`); } try { const content = readFileSync(configPath, 'utf8'); const config = JSON.parse(content); if (process.env.DEBUG === '1' || process.env.DEBUG_MCP === '1') { console.error(`[MCP DEBUG] Loaded configuration from: ${configPath}`); } // Merge with environment variable overrides return mergeWithEnvironment(config); } catch (error) { throw new Error(`Failed to parse MCP config from ${configPath}: ${error.message}`); } } /** * Load MCP configuration from various sources (DEPRECATED - use loadMCPConfigurationFromPath for explicit paths) * Priority order: * 1. Environment variable MCP_CONFIG_PATH * 2. Local project .mcp/config.json * 3. Home directory ~/.config/probe/mcp.json * 4. Home directory ~/.mcp/config.json (Claude compatible) * 5. Default configuration * @deprecated Use loadMCPConfigurationFromPath for explicit path loading or pass config directly */ export function loadMCPConfiguration() { const configPaths = [ // Environment variable path process.env.MCP_CONFIG_PATH, // Local project paths join(process.cwd(), '.mcp', 'config.json'), join(process.cwd(), 'mcp.config.json'), // Home directory paths join(homedir(), '.config', 'probe', 'mcp.json'), join(homedir(), '.mcp', 'config.json'), // Claude-style config location join(homedir(), 'Library', 'Application Support', 'Claude', 'mcp_config.json'), ].filter(Boolean); let config = null; // Try to load configuration from paths for (const configPath of configPaths) { if (existsSync(configPath)) { try { const content = readFileSync(configPath, 'utf8'); config = JSON.parse(content); if (process.env.DEBUG === '1' || process.env.DEBUG_MCP === '1') { console.error(`[MCP DEBUG] Loaded configuration from: ${configPath}`); } break; } catch (error) { console.error(`[MCP ERROR] Failed to parse config from ${configPath}:`, error.message); } } } // Merge with environment variable overrides config = mergeWithEnvironment(config || DEFAULT_CONFIG); return config; } /** * Merge configuration with environment variables * Supports: * - MCP_SERVERS_<NAME>_COMMAND: Command for server * - MCP_SERVERS_<NAME>_ARGS: Comma-separated args * - MCP_SERVERS_<NAME>_TRANSPORT: Transport type * - MCP_SERVERS_<NAME>_URL: URL for HTTP/WebSocket transports * - MCP_SERVERS_<NAME>_ENABLED: Enable/disable server */ function mergeWithEnvironment(config) { const serverPattern = /^MCP_SERVERS_([A-Z0-9_]+)_(.+)$/; for (const [key, value] of Object.entries(process.env)) { const match = key.match(serverPattern); if (match) { const [, serverName, property] = match; const normalizedName = serverName.toLowerCase().replace(/_/g, '-'); if (!config.mcpServers) { config.mcpServers = {}; } if (!config.mcpServers[normalizedName]) { config.mcpServers[normalizedName] = {}; } switch (property) { case 'COMMAND': config.mcpServers[normalizedName].command = value; break; case 'ARGS': config.mcpServers[normalizedName].args = value.split(',').map(arg => arg.trim()); break; case 'TRANSPORT': config.mcpServers[normalizedName].transport = value.toLowerCase(); break; case 'URL': config.mcpServers[normalizedName].url = value; break; case 'ENABLED': config.mcpServers[normalizedName].enabled = value === 'true' || value === '1'; break; case 'ENV': // Support custom environment variables for the server try { config.mcpServers[normalizedName].env = JSON.parse(value); } catch { config.mcpServers[normalizedName].env = { [property]: value }; } break; } } } return config; } /** * Parse MCP server configuration to extract enabled servers * @param {Object} config - Full MCP configuration * @returns {Array} Array of server configurations ready for connection */ export function parseEnabledServers(config) { const servers = []; if (!config || !config.mcpServers) { return servers; } for (const [name, serverConfig] of Object.entries(config.mcpServers)) { // Skip disabled servers if (serverConfig.enabled === false) { continue; } const server = { name, ...serverConfig }; // Set default transport if not specified if (!server.transport) { if (server.url) { // Infer transport from URL if (server.url.startsWith('ws://') || server.url.startsWith('wss://')) { server.transport = 'websocket'; } else if (server.url.includes('/sse')) { server.transport = 'sse'; } else { server.transport = 'http'; } } else { server.transport = 'stdio'; } } // Validate required fields based on transport if (server.transport === 'stdio') { if (!server.command) { console.error(`[MCP ERROR] Server ${name} missing required 'command' for stdio transport`); continue; } } else if (['websocket', 'sse', 'http'].includes(server.transport)) { if (!server.url) { console.error(`[MCP ERROR] Server ${name} missing required 'url' for ${server.transport} transport`); continue; } } servers.push(server); } return servers; } /** * Create a sample MCP configuration file */ export function createSampleConfig() { return { mcpServers: { 'probe': { command: 'npx', args: ['-y', '@probelabs/probe@latest', 'mcp'], transport: 'stdio', enabled: true, description: 'Probe code search MCP server' }, 'filesystem': { command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', process.cwd()], transport: 'stdio', enabled: false, description: 'Filesystem operations MCP server' }, 'github': { command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'], transport: 'stdio', enabled: false, description: 'GitHub API MCP server', env: { GITHUB_TOKEN: 'your-github-token' } }, 'postgres': { command: 'npx', args: ['-y', '@modelcontextprotocol/server-postgres'], transport: 'stdio', enabled: false, description: 'PostgreSQL database MCP server', env: { DATABASE_URL: 'postgresql://user:pass@localhost/db' } }, 'custom-http': { url: 'http://localhost:3000/mcp', transport: 'http', enabled: false, description: 'Custom HTTP MCP server' }, 'custom-websocket': { url: 'ws://localhost:8080', transport: 'websocket', enabled: false, description: 'Custom WebSocket MCP server' } }, // Global settings settings: { timeout: 30000, retryCount: 3, debug: false } }; } /** * Save configuration to file * @param {Object} config - Configuration to save * @param {string} path - Path to save to */ export function saveConfig(config, path) { const dir = dirname(path); // Create directory if it doesn't exist if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(path, JSON.stringify(config, null, 2), 'utf8'); console.error(`[MCP INFO] Configuration saved to: ${path}`); } export default { loadMCPConfiguration, loadMCPConfigurationFromPath, parseEnabledServers, createSampleConfig, saveConfig };