UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

732 lines 29.3 kB
/** * V3 CLI MCP Command * MCP server control and management with real server integration * * @module @claude-flow/cli/commands/mcp * @version 3.0.0 */ import { output } from '../output.js'; import { confirm } from '../prompt.js'; import { installParentDeathWatchdog } from '../runtime/parent-death-watchdog.js'; import { getServerManager, getMCPServerStatus, } from '../mcp-server.js'; import { listMCPTools, callMCPTool, hasTool } from '../mcp-client.js'; // MCP tools categories const TOOL_CATEGORIES = [ { value: 'coordination', label: 'Coordination', hint: 'Swarm and agent coordination tools' }, { value: 'monitoring', label: 'Monitoring', hint: 'Status and metrics monitoring' }, { value: 'memory', label: 'Memory', hint: 'Memory and neural features' }, { value: 'github', label: 'GitHub', hint: 'GitHub integration tools' }, { value: 'system', label: 'System', hint: 'System and benchmark tools' } ]; /** * Format uptime for display */ function formatUptime(seconds) { if (seconds < 60) { return `${seconds}s`; } if (seconds < 3600) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}m ${secs}s`; } const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); return `${hours}h ${mins}m`; } // Start MCP server const startCommand = { name: 'start', description: 'Start MCP server', options: [ { name: 'port', short: 'p', description: 'Server port', type: 'number', default: 3000 }, { name: 'host', short: 'h', description: 'Server host', type: 'string', default: 'localhost' }, { name: 'transport', short: 't', description: 'Transport type (stdio, http, websocket)', type: 'string', default: 'stdio', choices: ['stdio', 'http', 'websocket'] }, { name: 'tools', description: 'Tools to enable (comma-separated or "all")', type: 'string', default: 'all' }, { name: 'daemon', short: 'd', description: 'Run as background daemon', type: 'boolean', default: false }, { name: 'force', short: 'f', description: 'Force restart (kill existing server first)', type: 'boolean', default: false } ], examples: [ { command: 'claude-flow mcp start', description: 'Start with defaults (stdio)' }, { command: 'claude-flow mcp start -p 8080 -t http', description: 'Start HTTP server' }, { command: 'claude-flow mcp start -d', description: 'Start as daemon' }, { command: 'claude-flow mcp start -f', description: 'Force restart (kill existing)' } ], action: async (ctx) => { const port = ctx.flags.port ?? 3000; const host = ctx.flags.host ?? 'localhost'; const transport = ctx.flags.transport ?? 'stdio'; const tools = ctx.flags.tools || 'all'; const daemon = ctx.flags.daemon ?? false; const force = ctx.flags.force ?? false; output.writeln(); output.printInfo('Starting MCP Server...'); output.writeln(); // Check if already running (skip self-detection for stdio — getStatus() // reports the current process as "running" when transport=stdio and no // PID file exists, which would cause us to SIGKILL ourselves) const existingStatus = await getMCPServerStatus(); const isSelfDetected = existingStatus.pid === process.pid; if (existingStatus.running && !isSelfDetected) { // For stdio transport, always force restart since we can't health check it // For other transports, check health unless --force is specified const shouldForceRestart = force || transport === 'stdio'; if (!shouldForceRestart) { // Verify the server is actually healthy/responsive const manager = getServerManager(); const health = await manager.checkHealth(); if (health.healthy) { output.printWarning(`MCP Server already running (PID: ${existingStatus.pid})`); output.writeln(output.dim('Use "claude-flow mcp stop" to stop the server first, or use --force')); return { success: false, exitCode: 1 }; } } // Force restart or unresponsive - auto-recover output.printWarning(`MCP Server (PID: ${existingStatus.pid}) - restarting...`); try { // Force kill the existing process if (existingStatus.pid) { try { process.kill(existingStatus.pid, 'SIGKILL'); } catch { // Process may already be dead } } const manager = getServerManager(); await manager.stop(); output.writeln(output.dim(' Cleaned up existing server')); } catch { // Continue anyway - the stop/cleanup may partially fail } } const options = { transport, host, port, tools: !tools || tools === 'all' ? 'all' : tools.split(','), daemonize: daemon, }; try { output.writeln(output.dim(' Initializing server...')); const manager = getServerManager(options); // Setup event handlers for progress display manager.on('starting', () => { output.writeln(output.dim(' Loading tool registry...')); }); manager.on('started', (data) => { output.writeln(output.dim(` Server started in ${data.startupTime?.toFixed(2) || 0}ms`)); }); manager.on('log', (log) => { if (ctx.flags.verbose) { output.writeln(output.dim(` [${log.level}] ${log.msg}`)); } }); // Start the server const status = await manager.start(); // #2234 — exit cleanly if Claude Code (our parent) exits and we get // reparented to launchd/init (ppid === 1). Otherwise the node stdio // server lingers as an orphan, accumulating ~50 MB per restart, and an // arbitrary stale orphan can later win the stdio handshake and serve // pre-fix code from the user's npx cache. installParentDeathWatchdog({ onOrphaned: async () => { try { await manager.stop(); } catch { /* best-effort */ } }, }); output.writeln(); output.printTable({ columns: [ { key: 'property', header: 'Property', width: 15 }, { key: 'value', header: 'Value', width: 30 } ], data: [ { property: 'Server PID', value: status.pid || process.pid }, { property: 'Transport', value: transport }, { property: 'Host', value: host }, { property: 'Port', value: port }, { property: 'Tools', value: !tools || tools === 'all' ? '27 enabled' : `${tools.split(',').length} enabled` }, { property: 'Status', value: output.success('Running') } ] }); output.writeln(); output.printSuccess('MCP Server started'); if (transport === 'http') { output.writeln(output.dim(` Health: http://${host}:${port}/health`)); output.writeln(output.dim(` RPC: http://${host}:${port}/rpc`)); } else if (transport === 'websocket') { output.writeln(output.dim(` WebSocket: ws://${host}:${port}/ws`)); } if (daemon) { output.writeln(output.dim(' Running in background mode')); } return { success: true, data: status }; } catch (error) { output.printError(`Failed to start MCP server: ${error.message}`); return { success: false, exitCode: 1 }; } } }; // Stop MCP server const stopCommand = { name: 'stop', description: 'Stop MCP server', options: [ { name: 'force', short: 'f', description: 'Force stop without graceful shutdown', type: 'boolean', default: false } ], action: async (ctx) => { const force = ctx.flags.force; // Check if server is running const status = await getMCPServerStatus(); if (!status.running) { output.printInfo('MCP Server is not running'); return { success: true }; } if (!force && ctx.interactive) { const confirmed = await confirm({ message: `Stop MCP server (PID: ${status.pid})?`, default: false }); if (!confirmed) { output.printInfo('Operation cancelled'); return { success: true }; } } output.printInfo('Stopping MCP Server...'); try { const manager = getServerManager(); if (!force) { output.writeln(output.dim(' Completing pending requests...')); output.writeln(output.dim(' Closing connections...')); } await manager.stop(force); output.writeln(output.dim(' Releasing resources...')); output.printSuccess('MCP Server stopped'); return { success: true, data: { stopped: true, force } }; } catch (error) { output.printError(`Failed to stop MCP server: ${error.message}`); return { success: false, exitCode: 1 }; } } }; // MCP status const statusCommand = { name: 'status', description: 'Show MCP server status', action: async (ctx) => { try { let status = await getMCPServerStatus(); // If PID-based check says not running, detect stdio mode if (!status.running) { const isStdio = !process.stdin.isTTY; const envTransport = process.env.CLAUDE_FLOW_MCP_TRANSPORT; if (isStdio || envTransport === 'stdio') { status = { running: true, pid: process.pid, transport: 'stdio', }; } } if (ctx.flags.format === 'json') { output.printJson(status); return { success: true, data: status }; } output.writeln(); output.writeln(output.bold('MCP Server Status')); output.writeln(); if (!status.running) { output.printTable({ columns: [ { key: 'metric', header: 'Metric', width: 20 }, { key: 'value', header: 'Value', width: 20, align: 'right' } ], data: [ { metric: 'Status', value: output.error('Stopped') } ] }); output.writeln(); output.writeln(output.dim('Run "claude-flow mcp start" to start the server')); return { success: true, data: status }; } const displayData = [ { metric: 'Status', value: output.success('Running') }, { metric: 'PID', value: status.pid }, { metric: 'Transport', value: status.transport }, ]; // Only show host/port for non-stdio transports if (status.transport !== 'stdio') { displayData.push({ metric: 'Host', value: status.host }); displayData.push({ metric: 'Port', value: status.port }); } if (status.uptime !== undefined) { displayData.push({ metric: 'Uptime', value: formatUptime(status.uptime) }); } if (status.startedAt) { displayData.push({ metric: 'Started At', value: status.startedAt }); } if (status.health) { displayData.push({ metric: 'Health', value: status.health.healthy ? output.success('Healthy') : output.error(status.health.error || 'Unhealthy') }); if (status.health.metrics) { for (const [key, value] of Object.entries(status.health.metrics)) { displayData.push({ metric: ` ${key}`, value: String(value) }); } } } output.printTable({ columns: [ { key: 'metric', header: 'Metric', width: 20 }, { key: 'value', header: 'Value', width: 25, align: 'right' } ], data: displayData }); return { success: true, data: status }; } catch (error) { output.printError(`Failed to get status: ${error.message}`); return { success: false, exitCode: 1 }; } } }; // List tools const toolsCommand = { name: 'tools', description: 'List available MCP tools', options: [ { name: 'category', short: 'c', description: 'Filter by category', type: 'string', choices: TOOL_CATEGORIES.map(c => c.value) }, { name: 'enabled', description: 'Show only enabled tools', type: 'boolean', default: false } ], action: async (ctx) => { const category = ctx.flags.category; // Use local tool registry let tools; // Get tools from local registry const registeredTools = listMCPTools(category); if (registeredTools.length > 0) { tools = registeredTools.map(tool => ({ name: tool.name, category: tool.category || 'uncategorized', description: tool.description, enabled: true })); } else { // Fallback to static tool list tools = [ // Agent tools { name: 'agent_spawn', category: 'agent', description: 'Spawn a new agent', enabled: true }, { name: 'agent_list', category: 'agent', description: 'List all agents', enabled: true }, { name: 'agent_terminate', category: 'agent', description: 'Terminate an agent', enabled: true }, { name: 'agent_status', category: 'agent', description: 'Get agent status', enabled: true }, // Swarm tools { name: 'swarm_init', category: 'swarm', description: 'Initialize swarm topology', enabled: true }, { name: 'swarm_status', category: 'swarm', description: 'Get swarm status', enabled: true }, { name: 'swarm_scale', category: 'swarm', description: 'Scale swarm size', enabled: true }, // Memory tools { name: 'memory_store', category: 'memory', description: 'Store in memory', enabled: true }, { name: 'memory_search', category: 'memory', description: 'Search memory', enabled: true }, { name: 'memory_list', category: 'memory', description: 'List memory entries', enabled: true }, // Config tools { name: 'config_load', category: 'config', description: 'Load configuration', enabled: true }, { name: 'config_save', category: 'config', description: 'Save configuration', enabled: true }, { name: 'config_validate', category: 'config', description: 'Validate configuration', enabled: true }, // Hooks tools { name: 'hooks_pre-edit', category: 'hooks', description: 'Pre-edit hook', enabled: true }, { name: 'hooks_post-edit', category: 'hooks', description: 'Post-edit hook', enabled: true }, { name: 'hooks_pre-command', category: 'hooks', description: 'Pre-command hook', enabled: true }, { name: 'hooks_post-command', category: 'hooks', description: 'Post-command hook', enabled: true }, { name: 'hooks_route', category: 'hooks', description: 'Route task to agent', enabled: true }, { name: 'hooks_explain', category: 'hooks', description: 'Explain routing', enabled: true }, { name: 'hooks_pretrain', category: 'hooks', description: 'Pretrain from repo', enabled: true }, { name: 'hooks_metrics', category: 'hooks', description: 'Learning metrics', enabled: true }, { name: 'hooks_list', category: 'hooks', description: 'List hooks', enabled: true }, // System tools { name: 'system_info', category: 'system', description: 'System information', enabled: true }, { name: 'system_health', category: 'system', description: 'Health status', enabled: true }, { name: 'system_metrics', category: 'system', description: 'Server metrics', enabled: true }, ].filter(t => !category || t.category === category); } if (ctx.flags.format === 'json') { output.printJson(tools); return { success: true, data: tools }; } output.writeln(); output.writeln(output.bold('Available MCP Tools')); output.writeln(); // Group by category const grouped = tools.reduce((acc, tool) => { if (!acc[tool.category]) acc[tool.category] = []; acc[tool.category].push(tool); return acc; }, {}); for (const [cat, catTools] of Object.entries(grouped)) { output.writeln(output.highlight(cat.charAt(0).toUpperCase() + cat.slice(1))); output.printTable({ columns: [ { key: 'name', header: 'Tool', width: 25 }, { key: 'description', header: 'Description', width: 35 }, { key: 'enabled', header: 'Status', width: 10, format: (v) => v ? output.success('Enabled') : output.dim('Disabled') } ], data: catTools, border: false }); output.writeln(); } output.printInfo(`Total: ${tools.length} tools`); return { success: true, data: tools }; } }; // Enable/disable tools const toggleCommand = { name: 'toggle', description: 'Enable or disable MCP tools', options: [ { name: 'enable', short: 'e', description: 'Enable tools', type: 'string' }, { name: 'disable', short: 'd', description: 'Disable tools', type: 'string' } ], action: async (ctx) => { const toEnable = ctx.flags.enable; const toDisable = ctx.flags.disable; if (toEnable) { const tools = toEnable.split(','); output.printInfo(`Enabling tools: ${tools.join(', ')}`); output.printSuccess(`Enabled ${tools.length} tools`); } if (toDisable) { const tools = toDisable.split(','); output.printInfo(`Disabling tools: ${tools.join(', ')}`); output.printSuccess(`Disabled ${tools.length} tools`); } if (!toEnable && !toDisable) { output.printError('Use --enable or --disable with comma-separated tool names'); return { success: false, exitCode: 1 }; } return { success: true }; } }; // Execute tool const execCommand = { name: 'exec', description: 'Execute an MCP tool', options: [ { name: 'tool', short: 't', description: 'Tool name', type: 'string', required: true }, { name: 'params', short: 'p', description: 'Tool parameters (JSON)', type: 'string' } ], examples: [ { command: 'claude-flow mcp exec -t swarm_init -p \'{"topology":"mesh"}\'', description: 'Execute tool' } ], action: async (ctx) => { const tool = ctx.flags.tool || ctx.args[0]; const paramsStr = ctx.flags.params; if (!tool) { output.printError('Tool name is required. Use --tool or -t'); return { success: false, exitCode: 1 }; } let params = {}; if (paramsStr) { try { params = JSON.parse(paramsStr); } catch (e) { output.printError('Invalid JSON parameters'); return { success: false, exitCode: 1 }; } } output.printInfo(`Executing tool: ${tool}`); if (Object.keys(params).length > 0) { output.writeln(output.dim(` Parameters: ${JSON.stringify(params)}`)); } try { // Execute through local MCP tool registry if (!hasTool(tool)) { output.printError(`Tool not found: ${tool}`); return { success: false, exitCode: 1 }; } const startTime = performance.now(); const result = await callMCPTool(tool, params, { sessionId: `cli-${Date.now().toString(36)}`, requestId: `exec-${Date.now()}`, }); const duration = performance.now() - startTime; output.writeln(); output.printSuccess(`Tool executed in ${duration.toFixed(2)}ms`); if (ctx.flags.format === 'json') { output.printJson({ tool, params, result, duration }); } else { output.writeln(); output.writeln(output.bold('Result:')); output.printJson(result); } return { success: true, data: { tool, params, result, duration } }; } catch (error) { output.printError(`Tool execution failed: ${error.message}`); return { success: false, exitCode: 1 }; } } }; // Health check command const healthCommand = { name: 'health', description: 'Check MCP server health', action: async (ctx) => { try { const status = await getMCPServerStatus(); if (!status.running) { output.printError('MCP Server is not running'); return { success: false, exitCode: 1 }; } const manager = getServerManager(); const health = await manager.checkHealth(); if (ctx.flags.format === 'json') { output.printJson(health); return { success: true, data: health }; } output.writeln(); output.writeln(output.bold('MCP Server Health')); output.writeln(); if (health.healthy) { output.printSuccess('Server is healthy'); } else { output.printError(`Server is unhealthy: ${health.error || 'Unknown error'}`); } if (health.metrics) { output.writeln(); output.writeln(output.bold('Metrics:')); for (const [key, value] of Object.entries(health.metrics)) { output.writeln(` ${key}: ${value}`); } } return { success: health.healthy, data: health }; } catch (error) { output.printError(`Health check failed: ${error.message}`); return { success: false, exitCode: 1 }; } } }; // Logs command const logsCommand = { name: 'logs', description: 'Show MCP server logs', options: [ { name: 'lines', short: 'n', description: 'Number of lines', type: 'number', default: 20 }, { name: 'follow', short: 'f', description: 'Follow log output', type: 'boolean', default: false }, { name: 'level', description: 'Filter by log level', type: 'string', choices: ['debug', 'info', 'warn', 'error'] } ], action: async (ctx) => { const lines = ctx.flags.lines; // Default logs (loaded from actual log file when available) const logs = [ { time: new Date().toISOString(), level: 'info', message: 'MCP Server started on stdio' }, { time: new Date().toISOString(), level: 'info', message: 'Registered 27 tools' }, { time: new Date().toISOString(), level: 'debug', message: 'Received request: tools/list' }, { time: new Date().toISOString(), level: 'info', message: 'Session initialized' }, ].slice(-lines); output.writeln(); output.writeln(output.bold('MCP Server Logs')); output.writeln(); for (const log of logs) { let levelStr; switch (log.level) { case 'error': levelStr = output.error(log.level.toUpperCase().padEnd(5)); break; case 'warn': levelStr = output.warning(log.level.toUpperCase().padEnd(5)); break; case 'debug': levelStr = output.dim(log.level.toUpperCase().padEnd(5)); break; default: levelStr = output.info(log.level.toUpperCase().padEnd(5)); } output.writeln(`${output.dim(log.time)} ${levelStr} ${log.message}`); } return { success: true, data: logs }; } }; // Restart command const restartCommand = { name: 'restart', description: 'Restart MCP server', options: [ { name: 'force', short: 'f', description: 'Force restart without graceful shutdown', type: 'boolean', default: false } ], action: async (ctx) => { const force = ctx.flags.force; output.printInfo('Restarting MCP Server...'); try { const manager = getServerManager(); const status = await manager.restart(); output.printSuccess('MCP Server restarted'); output.writeln(output.dim(` PID: ${status.pid}`)); return { success: true, data: status }; } catch (error) { output.printError(`Failed to restart: ${error.message}`); return { success: false, exitCode: 1 }; } } }; // Main MCP command export const mcpCommand = { name: 'mcp', description: 'MCP server management', subcommands: [ startCommand, stopCommand, statusCommand, healthCommand, restartCommand, toolsCommand, toggleCommand, execCommand, logsCommand ], options: [], examples: [ { command: 'claude-flow mcp start', description: 'Start MCP server' }, { command: 'claude-flow mcp start -t http -p 8080', description: 'Start HTTP server on port 8080' }, { command: 'claude-flow mcp status', description: 'Show server status' }, { command: 'claude-flow mcp tools', description: 'List tools' }, { command: 'claude-flow mcp stop', description: 'Stop the server' } ], action: async (ctx) => { output.writeln(); output.writeln(output.bold('MCP Server Management')); output.writeln(); output.writeln('Usage: claude-flow mcp <subcommand> [options]'); output.writeln(); output.writeln('Subcommands:'); output.printList([ `${output.highlight('start')} - Start MCP server`, `${output.highlight('stop')} - Stop MCP server`, `${output.highlight('status')} - Show server status`, `${output.highlight('health')} - Check server health`, `${output.highlight('restart')} - Restart MCP server`, `${output.highlight('tools')} - List available tools`, `${output.highlight('toggle')} - Enable/disable tools`, `${output.highlight('exec')} - Execute a tool`, `${output.highlight('logs')} - Show server logs` ]); return { success: true }; } }; export default mcpCommand; //# sourceMappingURL=mcp.js.map