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