UNPKG

@build000r/mcp-mcp

Version:

Fast CLI tool to scan, view, park (JIT), and interactively manage MCP server configurations across all Claude projects

248 lines (214 loc) 8.26 kB
const path = require('path'); const os = require('os'); const { promisify } = require('util'); const fs = require('fs'); const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); const mkdir = promisify(fs.mkdir); const ColorUtils = require('../utils/ColorUtils'); const ConfigParser = require('../scanner/ConfigParser'); // Lazy load inquirer only when needed let inquirer = null; class InteractiveManager { static async runInteractiveMode(configs) { // Lazy load inquirer if (!inquirer) { inquirer = require('inquirer').default || require('inquirer'); } if (configs.length === 0) { console.log(ColorUtils.colorText('No MCP configurations found', 'yellow')); return; } // Load parked servers to check status let parkedServers = {}; const jitDir = path.join(os.homedir(), '.mcp-jit'); const parkedFile = path.join(jitDir, 'parked.json'); try { const content = await readFile(parkedFile, 'utf8'); parkedServers = JSON.parse(content); } catch (error) { // No parked servers } // Collect all servers with their locations const servers = []; configs.forEach(config => { const configServers = ConfigParser.getServersFromConfig(config.config); configServers.forEach(server => { const isParked = !!parkedServers[server.name]; servers.push({ name: server.name, type: server.type, path: config.path, scope: config.scope, configType: config.configType, configPath: config.configPath, env: server.env, fullConfig: server, config: config.config, isParked: isParked }); }); }); if (servers.length === 0) { console.log(ColorUtils.colorText('No MCP servers found', 'yellow')); return; } // Create choices for inquirer const choices = servers.map(server => { const envInfo = Object.keys(server.env).length > 0 ? ` [${Object.keys(server.env).join(', ')}]` : ''; const parkedStatus = server.isParked ? ColorUtils.colorText(' [PARKED]', 'red') : ''; return { name: `${server.name} (${server.type}) - ${server.path} (${server.scope})${envInfo}${parkedStatus}`, value: server, short: server.name }; }); // Add separator and exit option properly if (choices.length > 0) { choices.push(new inquirer.Separator()); choices.push({ name: 'Exit', value: null }); } console.log(ColorUtils.colorText('\nMCP Server Manager - Interactive Mode\n', 'bold')); console.log('Use arrow keys to navigate, press Enter to select servers to park.\n'); console.log('Parked servers are temporarily removed to lighten Claude\'s load.\n'); const { selectedServers } = await inquirer.prompt([ { type: 'checkbox', name: 'selectedServers', message: 'Select servers to park:', choices: choices, pageSize: Math.min(15, choices.length), loop: false } ]); if (!selectedServers || selectedServers.length === 0) { console.log('No servers selected.'); return; } // Confirm park const { confirm } = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: `Are you sure you want to park ${selectedServers.length} server(s)?`, default: false } ]); if (!confirm) { console.log('Parking cancelled.'); return; } // Park selected servers await this.parkSelectedServers(selectedServers, parkedServers, jitDir, parkedFile); } static async parkSelectedServers(selectedServers, existingParked, jitDir, parkedFile) { // Ensure JIT directory exists try { await mkdir(jitDir, { recursive: true }); } catch (error) { // Directory might already exist } let parkedCount = 0; const configsToUpdate = new Map(); for (const server of selectedServers) { if (!server || server.isParked) continue; const scope = this.determineScope(server.path, server.configType); const reinstallCmd = this.generateReinstallCommand(server.fullConfig, scope); // Add to parked servers existingParked[server.name] = { originalPath: server.configPath, originalConfig: server.fullConfig, reinstallCmd: reinstallCmd, scope: scope, configType: server.configType }; // Track which config files need to be updated if (!configsToUpdate.has(server.configPath)) { configsToUpdate.set(server.configPath, { configType: server.configType, path: server.path, serversToRemove: [] }); } configsToUpdate.get(server.configPath).serversToRemove.push(server.name); console.log(` Parking ${ColorUtils.colorText(server.name, 'cyan')} (${scope} scope)`); parkedCount++; } // Save parked servers await writeFile(parkedFile, JSON.stringify(existingParked, null, 2)); // Remove servers from their original config files for (const [configPath, updateInfo] of configsToUpdate) { await this.removeServersFromConfigFile(configPath, updateInfo); } console.log(ColorUtils.colorText(`\n✓ Parked ${parkedCount} MCP servers`, 'green')); console.log(` Use --unpark <server> to restore individual servers`); console.log(` Use --unpark-all to restore all servers`); } static async removeServersFromConfigFile(configPath, updateInfo) { try { const content = await readFile(configPath, 'utf8'); const config = JSON.parse(content); if (updateInfo.configType === '.claude.json (project)') { // Handle project-specific configs in main .claude.json if (config.projects && config.projects[updateInfo.path]) { if (config.projects[updateInfo.path].mcpServers) { for (const serverName of updateInfo.serversToRemove) { delete config.projects[updateInfo.path].mcpServers[serverName]; } if (Object.keys(config.projects[updateInfo.path].mcpServers).length === 0) { delete config.projects[updateInfo.path].mcpServers; } } } } else if (updateInfo.configType === '.claude.json') { // Handle regular .claude.json if (config.mcpServers) { for (const serverName of updateInfo.serversToRemove) { delete config.mcpServers[serverName]; } if (Object.keys(config.mcpServers).length === 0) { delete config.mcpServers; } } } else if (updateInfo.configType === '.mcp.json') { // Handle .mcp.json if (config.servers) { for (const serverName of updateInfo.serversToRemove) { delete config.servers[serverName]; } if (Object.keys(config.servers).length === 0) { delete config.servers; } } } await writeFile(configPath, JSON.stringify(config, null, 2)); console.log(` Updated: ${configPath}`); } catch (error) { console.error(` Failed to update ${configPath}: ${error.message}`); } } static determineScope(configPath, configType) { const homeDir = os.homedir(); if (configType === '.claude.json (project)') { return 'local'; } else if (configPath === homeDir) { return 'user'; } else { return 'local'; } } static generateReinstallCommand(server, scope) { // Generate add command based on server config if (server.command && Array.isArray(server.command)) { const commandStr = server.command.join(' '); return `claude mcp add --scope ${scope} "${server.name}" "${commandStr}"`; } else if (server.command) { return `claude mcp add --scope ${scope} "${server.name}" "${server.command}"`; } else { // Fallback - might need manual intervention return `# Manual restore needed for ${server.name} - original config stored in parked.json`; } } } module.exports = InteractiveManager;