UNPKG

@poai/mcpm-aider

Version:

Your MCP CLI Manager for aider(forked from @mcpm/cli). Helps you install/remove/search your MCP projects. You can also manage your MCP in Claude App with natural language.

495 lines (494 loc) • 17.3 kB
/** * Copyright (C) 2024 PoAI (Lutz Leonhardt) * This file is part of mcpm, based on work by MCP Club * Licensed under the GNU AGPL v3.0 * See LICENSE file for details */ /* eslint-disable no-process-exit */ /* eslint-disable @typescript-eslint/no-unsafe-call */ import { Command } from 'commander'; import prompts from 'prompts'; import { HostService, HostType, MCPServerConfigSource, RegistryService, startMCPServer, stringifyServerToTitle, } from '@mcpm/sdk'; import { generateToolPrompt } from './mcp-proxy/toolprompt.js'; import { version } from './utils/version.js'; import { formatMCPServers } from './utils/formatter.js'; import { startBridge } from './mcp-bridge/index.js'; const program = new Command(); const claudeSrv = HostService.getInstanceByType(HostType.CLAUDE); const registrySrv = new RegistryService(); program .version(version) .name('mcpm') .option('-d, --debug', 'enables verbose logging', false); program .command('search') .description('Search for MCP packages') .argument('[query]', 'Search query') .option('--json', 'Output in JSON format') .action(async (query, options) => { try { if (!query) { const response = await prompts({ type: 'text', name: 'query', message: 'Enter search query:', }); if (!response.query) { console.log('\nSearch cancelled'); return; } query = response.query; } const packages = await registrySrv.searchPackages(query); if (options.json) { console.log(JSON.stringify(packages, null, 2)); } else { if (packages.length === 0) { console.log('\nšŸ“¦ No packages found matching your query.'); return; } console.log(`\nšŸ“¦ Found ${packages.length} package${packages.length > 1 ? 's' : ''} matching "${query}"\n`); for (const pkg of packages) { console.log(`\x1b[1m${pkg.title}\x1b[0m (\x1b[36m${pkg.id}\x1b[0m)`); console.log(` ${pkg.description}`); if (pkg.tags?.length) { console.log(` \x1b[33m${pkg.tags.map(tag => `#${tag}`).join(' ')}\x1b[0m`); } console.log(''); // Add empty line between packages } console.log('šŸ’” Tip: Use \x1b[36mmcpm install <package-id>\x1b[0m to install a package\n'); } } catch (error) { console.error('Error:', error.message); process.exit(1); } }); program .command('install') .alias('i') .description('Install a MCP package from the registry') .argument('<name>', 'Package name to install') .option('-y, --yes', 'Skip confirmation') .option('-p, --param <param...>', 'Parameters in format KEY=VALUE') .action(async (name, options) => { try { // Get package info const packageInfo = await registrySrv.getPackageInfo(name); if (!options.yes) { console.log(`Package: ${packageInfo.title} (${name})`); console.log(`Description: ${packageInfo.description}`); const { confirmed } = await prompts({ type: 'confirm', name: 'confirmed', message: 'Do you want to install this package?', initial: true, }); if (!confirmed) { console.log('Installation cancelled'); return; } } // Parse command line parameters const paramValues = {}; if (options.param) { for (const param of options.param) { const [key, value] = param.split('='); if (!key || !value) { throw new Error(`Invalid parameter format: ${param}. Use KEY=VALUE format`); } paramValues[key] = value; } } // Get missing parameters interactively const missingParams = Object.entries(packageInfo.parameters).filter(([key, info]) => info.required && !paramValues[key]); if (missingParams.length > 0) { const paramQuestions = missingParams.map(([paramName, paramInfo]) => ({ type: 'text', name: paramName, message: `Please enter ${paramName} (${paramInfo.description}):`, validate: (value) => paramInfo.required && !value ? 'This field is required' : true, })); const paramAnswers = await prompts(paramQuestions); Object.assign(paramValues, paramAnswers); } // Install package await claudeSrv.installPackage(name, paramValues); console.log(`Package '${name}' installed successfully!`); } catch (error) { console.error('Error:', error.message); process.exit(1); } }); program .command('add') .description('Manually add a new MCP server to your Claude App') .argument('[name]', 'name of the MCP server') .option('-c, --command <command>', 'command to run the server') .option('-a, --args <args...>', 'arguments for the command') .option('--self', 'add the MCPM CLI as a MCP server') .action(async (name, options) => { if (options.self) { await claudeSrv.addMCPMSelfMCPServer(); console.log('MCPM CLI added successfully'); return; } if (!name || !options.command) { const questions = []; if (!name) { questions.push({ type: 'text', name: 'name', message: 'Enter a name for the MCP server:', validate: (value) => value.length > 0 ? true : 'Name cannot be empty', }); } if (!options.command) { questions.push({ type: 'text', name: 'command', message: 'Enter the command to run the server:', validate: (value) => value.length > 0 ? true : 'Command cannot be empty', }); } if (!options.args) { questions.push({ type: 'text', name: 'args', message: 'Enter command arguments (space separated):', initial: '', }); } const responses = await prompts(questions); name = name || responses.name; if (!name || (!responses.command && !options.command)) { console.log('Operation cancelled'); return; } options.command = options.command || responses.command; options.args = options.args || (responses.args ? responses.args .split(' ') .filter((arg) => arg.length > 0) : []); } if (!options.command) { console.log('Operation cancelled'); return; } await claudeSrv.addMCPServer(name, { command: options.command, args: options.args || [], }); console.log(`MCP server '${name}' added successfully`); }); program .command('remove') .alias('rm') .description('Remove a MCP server from your Claude App') .argument('[name]', 'name of the MCP server to remove') .action(async (name) => { if (!name) { const servers = await claudeSrv.getMCPServersInConfig(); const choices = Object.entries(servers).map(([name, server]) => ({ title: `${name} (${server.command} ${server.args?.join(' ') || ''})`, value: name, })); if (choices.length === 0) { console.log('No MCP servers found'); return; } const response = await prompts({ type: 'select', name: 'server', message: 'Select a server to remove:', choices, }); if (!response.server) { console.log('Operation cancelled'); return; } name = response.server; } if (!name) { console.log('Operation cancelled'); return; } try { await claudeSrv.removeMCPServer(name); console.log(`MCP server '${name}' removed successfully`); } catch (error) { console.error(`Failed to remove server '${name}':`, error); process.exit(1); } }); program .command('disable') .description('Disable an MCP server (moves it from Claude to storage)') .argument('[name]', 'name of the MCP server to disable') .action(async (name) => { if (!name) { const servers = await claudeSrv.getEnabledMCPServers(); const choices = Object.entries(servers).map(([name, info]) => ({ title: stringifyServerToTitle(info), value: name, })); if (choices.length === 0) { console.log('No MCP servers found'); return; } const response = await prompts({ type: 'select', name: 'server', message: 'Select a server to disable:', choices, }); if (!response.server) { console.log('Operation cancelled'); return; } name = response.server; } if (!name) { console.log('Operation cancelled'); return; } try { await claudeSrv.disableMCPServer(name); console.log(`MCP server '${name}' disabled successfully`); } catch (error) { console.error(`Failed to disable server '${name}':`, error); process.exit(1); } }); program .command('enable') .description('Enable a disabled MCP server (moves it from storage to Claude)') .argument('[name]', 'name of the MCP server to enable') .action(async (name) => { if (!name) { const servers = await claudeSrv.getDisabledMCPServers(); const choices = Object.entries(servers).map(([name, info]) => ({ title: stringifyServerToTitle(info), value: name, })); if (choices.length === 0) { console.log('No disabled MCP servers found'); return; } const response = await prompts({ type: 'select', name: 'server', message: 'Select a server to enable:', choices, }); if (!response.server) { console.log('Operation cancelled'); return; } name = response.server; } if (!name) { console.log('Operation cancelled'); return; } try { await claudeSrv.enableMCPServer(name); console.log(`MCP server '${name}' enabled successfully`); } catch (error) { console.error(`Failed to enable server '${name}':`, error); process.exit(1); } }); program .command('list') .description('List all your MCP servers') .option('--json', 'Output in JSON format') .action(async (options) => { const servers = await claudeSrv.getAllMCPServersWithStatus(); if (options.json) { console.log(JSON.stringify(servers, null, 2)); } else { console.log(formatMCPServers(servers)); } }); program .command('mcp') .description('Start the MCPM MCP server') .action(async () => { try { await startMCPServer(); } catch (error) { console.error('Error starting MCP server:', error.message); process.exit(1); } }); const debugCmd = program .command('debug', { hidden: true }) .description('Only for Debug'); debugCmd .command('clear') .description('Clear all data') .action(() => { console.log('Clearing all data'); claudeSrv.clearAllData(); }); program .command('update-params') .description('Update MCP server parameters') .argument('<name>', 'Server name') .action(async (name) => { try { // Get current server info const server = await claudeSrv.getMCPServerWithStatus(name); if (!server) { console.error('Server not found:', name); process.exit(1); } // Get parameter info from registry if it's a remote server let paramInfo = {}; if (server.info.from === MCPServerConfigSource.REMOTE) { try { const packageInfo = await registrySrv.getPackageInfo(server.info.registryId ?? name); paramInfo = packageInfo.parameters; } catch (error) { console.warn('Failed to fetch parameter info from registry, proceeding with basic parameter update'); } } // Get current parameter values const currentParams = server.info.arguments || {}; // Ask user which parameters to update const paramChoices = Object.entries(paramInfo).map(([key, info]) => ({ title: `${key}${info.required ? ' (required)' : ''}: ${info.description}`, value: key, description: `Current value: ${currentParams[key] || '(not set)'}`, })); if (paramChoices.length === 0) { // If no parameter info from registry, ask user to input parameter name const { paramKey } = await prompts({ type: 'text', name: 'paramKey', message: 'Enter parameter name to update:', }); if (!paramKey) { console.log('\nUpdate cancelled'); return; } const { paramValue } = await prompts({ type: 'text', name: 'paramValue', message: `Enter new value for ${paramKey}:`, initial: currentParams[paramKey], }); if (paramValue === undefined) { console.log('\nUpdate cancelled'); return; } await claudeSrv.updateMCPServerParams(name, { [paramKey]: paramValue }); } else { // If we have parameter info, let user choose which to update const { selectedParams } = await prompts({ type: 'multiselect', name: 'selectedParams', message: 'Select parameters to update:', choices: paramChoices, }); if (!selectedParams || selectedParams.length === 0) { console.log('\nUpdate cancelled'); return; } const newParams = {}; for (const param of selectedParams) { const { value } = await prompts({ type: 'text', name: 'value', message: `Enter new value for ${param}:`, initial: currentParams[param], }); if (value === undefined) { console.log('\nUpdate cancelled'); return; } newParams[param] = value; } await claudeSrv.updateMCPServerParams(name, newParams); } console.log('Server parameters updated successfully'); } catch (error) { console.error('Failed to update server parameters:', error); process.exit(1); } }); program .command('restart') .description('Restart Claude.app') .action(async () => { try { console.log('Restarting Claude.app...'); await claudeSrv.restartHostApp(); console.log('Claude.app has been restarted'); } catch (error) { console.error('Failed to restart Claude.app:', error.message); process.exit(1); } }); program.command('prepare').description('Prepare Claude.app for MCPM'); program .command('toolprompt') .description('Generate tool use prompt with all available MCP servers') .action(async () => { try { const servers = await claudeSrv.getAllMCPServersWithStatus(); const text = await generateToolPrompt(servers); console.clear(); console.log(text); } catch (error) { console.error('Error:', error.message); process.exit(1); } }); program .command('call') .description('Call an MCP server tool function') .argument('<tool>', 'MCP server tool identifier') .argument('<function>', 'Function name to call') .argument('<parameters>', 'Parameters in JSON format') .action(async (tool, functionName, parameters) => { try { const parsedParameters = JSON.parse(parameters); const { callToolFunction } = await import('./mcp-proxy/call.js'); await callToolFunction(tool, functionName, parsedParameters); } catch (error) { console.error('Error:', error instanceof Error ? error.message : error); process.exit(1); } }); program .command('start-bridge') .description('Start the MCP-Bridge Python service') .option('--server <url>', 'API server URL', 'https://api.anthropic.com/v1/') .action(async (options) => { try { await startBridge(options); } catch (error) { console.error('Error starting MCP-Bridge:', error.message); process.exit(1); } }); program.parse(process.argv);