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
601 lines • 20.1 kB
JavaScript
/**
* V3 CLI MCP Server Management
*
* Provides server lifecycle management for MCP integration:
* - Start/stop/status methods with process management
* - Health check endpoint integration
* - Graceful shutdown handling
* - PID file management for daemon detection
* - Event-based status monitoring
*
* Performance Targets:
* - Server startup: <400ms
* - Health check: <10ms
* - Graceful shutdown: <5s
*
* @module @claude-flow/cli/mcp-server
* @version 3.0.0
*/
import { EventEmitter } from 'events';
import { randomUUID } from 'crypto';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
// ESM-compatible __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Default configuration
*/
const DEFAULT_OPTIONS = {
transport: 'stdio',
host: 'localhost',
port: 3000,
pidFile: path.join(os.tmpdir(), 'claude-flow-mcp.pid'),
logFile: path.join(os.tmpdir(), 'claude-flow-mcp.log'),
tools: 'all',
daemonize: false,
timeout: 30000,
};
/**
* MCP Server Manager
*
* Manages the lifecycle of the MCP server process
*/
export class MCPServerManager extends EventEmitter {
options;
process;
server;
startTime;
healthCheckInterval;
constructor(options = {}) {
super();
this.options = { ...DEFAULT_OPTIONS, ...options };
}
/**
* Start the MCP server
*/
async start() {
// Check if already running
const status = await this.getStatus();
if (status.running) {
throw new Error(`MCP Server already running (PID: ${status.pid})`);
}
const startTime = performance.now();
this.startTime = new Date();
this.emit('starting', { options: this.options });
try {
if (this.options.transport === 'stdio') {
// For stdio transport, spawn the server process
await this.startStdioServer();
}
else {
// For HTTP/WebSocket, start in-process server
await this.startHttpServer();
}
const duration = performance.now() - startTime;
// Write PID file
await this.writePidFile();
// Start health check monitoring
this.startHealthMonitoring();
const finalStatus = await this.getStatus();
this.emit('started', {
...finalStatus,
startupTime: duration,
});
return finalStatus;
}
catch (error) {
this.emit('error', error);
throw error;
}
}
/**
* Stop the MCP server
*/
async stop(force = false) {
const status = await this.getStatus();
if (!status.running) {
return;
}
this.emit('stopping', { force });
try {
// Stop health monitoring
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = undefined;
}
if (this.process) {
// Graceful shutdown
if (!force) {
this.process.kill('SIGTERM');
await this.waitForExit(5000);
}
// Force kill if still running
if (this.process && !this.process.killed) {
this.process.kill('SIGKILL');
}
this.process = undefined;
}
if (this.server) {
await new Promise((resolve) => {
this.server.close(() => resolve());
});
this.server = undefined;
}
// Remove PID file
await this.removePidFile();
this.startTime = undefined;
this.emit('stopped');
}
catch (error) {
this.emit('error', error);
throw error;
}
}
/**
* Get server status
*/
async getStatus() {
// Check PID file
const pid = await this.readPidFile();
if (!pid) {
return { running: false };
}
// Check if process is running
const isRunning = this.isProcessRunning(pid);
if (!isRunning) {
// Clean up stale PID file
await this.removePidFile();
return { running: false };
}
// Build status
const status = {
running: true,
pid,
transport: this.options.transport,
host: this.options.host,
port: this.options.port,
startedAt: this.startTime?.toISOString(),
uptime: this.startTime
? Math.floor((Date.now() - this.startTime.getTime()) / 1000)
: undefined,
};
// Get health status for HTTP transport
if (this.options.transport !== 'stdio') {
status.health = await this.checkHealth();
}
return status;
}
/**
* Check server health
*/
async checkHealth() {
if (this.options.transport === 'stdio') {
// For stdio, check if process is running
const pid = await this.readPidFile();
if (pid === null) {
return { healthy: false, error: 'No PID file found' };
}
if (!this.isProcessRunning(pid)) {
// Clean up stale PID file
await this.removePidFile();
return { healthy: false, error: 'Process not running (cleaned up stale PID)' };
}
return { healthy: true };
}
// For HTTP/WebSocket, make health check request
try {
const response = await this.httpRequest(`http://${this.options.host}:${this.options.port}/health`, 'GET', this.options.timeout);
return {
healthy: response.status === 'ok',
metrics: {
connections: response.connections || 0,
},
};
}
catch (error) {
return {
healthy: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Restart the server
*/
async restart() {
await this.stop();
return await this.start();
}
/**
* Start stdio server in-process
* Handles stdin/stdout directly like V2 implementation
*/
async startStdioServer() {
// Import the tool registry
const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
const VERSION = '3.0.0';
const sessionId = `mcp-${Date.now()}-${randomUUID().slice(0, 8)}`;
// Log to stderr to not corrupt stdout
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Starting in stdio mode`);
console.error(JSON.stringify({
arch: process.arch,
mode: 'mcp-stdio',
nodeVersion: process.version,
pid: process.pid,
platform: process.platform,
protocol: 'stdio',
sessionId,
version: VERSION,
}));
// Send server initialization notification
console.log(JSON.stringify({
jsonrpc: '2.0',
method: 'server.initialized',
params: {
serverInfo: {
name: 'claude-flow',
version: VERSION,
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
},
},
},
}));
// Handle stdin messages
let buffer = '';
process.stdin.on('data', async (chunk) => {
buffer += chunk.toString();
// Process complete JSON messages
let lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
for (const line of lines) {
if (line.trim()) {
try {
const message = JSON.parse(line);
const response = await this.handleMCPMessage(message, sessionId);
if (response) {
console.log(JSON.stringify(response));
}
}
catch (error) {
console.error(`[${new Date().toISOString()}] ERROR [claude-flow-mcp] Failed to parse message:`, error instanceof Error ? error.message : String(error));
}
}
}
});
process.stdin.on('end', () => {
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) stdin closed, shutting down...`);
process.exit(0);
});
// Handle process termination
process.on('SIGINT', () => {
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Received SIGINT, shutting down...`);
process.exit(0);
});
process.on('SIGTERM', () => {
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Received SIGTERM, shutting down...`);
process.exit(0);
});
// Mark as ready immediately for stdio
this.emit('ready');
}
/**
* Handle incoming MCP message
*/
async handleMCPMessage(message, sessionId) {
const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
if (!message.method) {
return {
jsonrpc: '2.0',
id: message.id,
error: { code: -32600, message: 'Invalid Request: missing method' },
};
}
const params = (message.params || {});
try {
switch (message.method) {
case 'initialize':
return {
jsonrpc: '2.0',
id: message.id,
result: {
protocolVersion: '2024-11-05',
serverInfo: { name: 'claude-flow', version: '3.0.0' },
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
},
},
};
case 'tools/list':
const tools = listMCPTools();
return {
jsonrpc: '2.0',
id: message.id,
result: {
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
},
};
case 'tools/call':
const toolName = params.name;
const toolParams = (params.arguments || {});
if (!hasTool(toolName)) {
return {
jsonrpc: '2.0',
id: message.id,
error: { code: -32601, message: `Tool not found: ${toolName}` },
};
}
try {
const result = await callMCPTool(toolName, toolParams, { sessionId });
return {
jsonrpc: '2.0',
id: message.id,
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] },
};
}
catch (error) {
return {
jsonrpc: '2.0',
id: message.id,
error: {
code: -32603,
message: error instanceof Error ? error.message : 'Tool execution failed',
},
};
}
case 'notifications/initialized':
// Client notification - no response needed
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Client initialized`);
return null;
case 'ping':
return {
jsonrpc: '2.0',
id: message.id,
result: {},
};
default:
return {
jsonrpc: '2.0',
id: message.id,
error: { code: -32601, message: `Method not found: ${message.method}` },
};
}
}
catch (error) {
console.error(`[${new Date().toISOString()}] ERROR [claude-flow-mcp] Error handling ${message.method}:`, error);
return {
jsonrpc: '2.0',
id: message.id,
error: {
code: -32603,
message: error instanceof Error ? error.message : 'Internal error',
},
};
}
}
/**
* Start HTTP server in-process
*/
async startHttpServer() {
// Dynamically import the MCP server package
// FIX for issue #942: Use proper package import instead of broken relative path
const { createMCPServer } = await import('@claude-flow/mcp');
const logger = {
debug: (msg, data) => this.emit('log', { level: 'debug', msg, data }),
info: (msg, data) => this.emit('log', { level: 'info', msg, data }),
warn: (msg, data) => this.emit('log', { level: 'warn', msg, data }),
error: (msg, data) => this.emit('log', { level: 'error', msg, data }),
};
const mcpServer = createMCPServer({
name: 'Claude-Flow MCP Server V3',
version: '3.0.0',
transport: this.options.transport,
host: this.options.host,
port: this.options.port,
enableMetrics: true,
enableCaching: true,
}, logger);
await mcpServer.start();
// Store reference for stopping
this._mcpServer = mcpServer;
}
/**
* Wait for server to be ready
*/
async waitForReady(timeout = 10000) {
// For stdio transport, we're ready immediately (in-process)
if (this.options.transport === 'stdio') {
return;
}
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const health = await this.checkHealth();
if (health.healthy) {
return;
}
await this.sleep(100);
}
throw new Error('Server failed to start within timeout');
}
/**
* Wait for process to exit
*/
async waitForExit(timeout) {
if (!this.process)
return;
return new Promise((resolve) => {
const timer = setTimeout(() => {
resolve();
}, timeout);
this.process.once('exit', () => {
clearTimeout(timer);
resolve();
});
});
}
/**
* Start health monitoring
*/
startHealthMonitoring() {
this.healthCheckInterval = setInterval(async () => {
try {
const health = await this.checkHealth();
this.emit('health', health);
if (!health.healthy) {
this.emit('unhealthy', health);
}
}
catch (error) {
this.emit('health-error', error);
}
}, 30000);
}
/**
* Write PID file
*/
async writePidFile() {
const pid = this.process?.pid || process.pid;
await fs.promises.writeFile(this.options.pidFile, String(pid), 'utf8');
}
/**
* Read PID file
*/
async readPidFile() {
try {
const content = await fs.promises.readFile(this.options.pidFile, 'utf8');
const pid = parseInt(content.trim(), 10);
return isNaN(pid) ? null : pid;
}
catch {
return null;
}
}
/**
* Remove PID file
*/
async removePidFile() {
try {
await fs.promises.unlink(this.options.pidFile);
}
catch {
// Ignore errors
}
}
/**
* Check if process is running
*/
isProcessRunning(pid) {
try {
process.kill(pid, 0);
return true;
}
catch {
return false;
}
}
/**
* Make HTTP request
*/
async httpRequest(url, method, timeout) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const http = require('http');
const req = http.request({
hostname: urlObj.hostname,
port: urlObj.port,
path: urlObj.pathname,
method,
timeout,
}, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
resolve(JSON.parse(data));
}
catch {
resolve({ status: res.statusCode === 200 ? 'ok' : 'error' });
}
});
});
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}
/**
* Sleep utility
*/
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
/**
* Create MCP server manager
*/
export function createMCPServerManager(options) {
return new MCPServerManager(options);
}
/**
* Singleton server manager instance
*/
let serverManager = null;
let currentTransport = undefined;
/**
* Get or create server manager singleton
*
* FIX for issue #942: Recreate singleton if transport type changes
* Previously, once created with stdio (default), HTTP options were ignored
*/
export function getServerManager(options) {
const requestedTransport = options?.transport;
// Recreate if transport type changes (fixes HTTP transport not working)
if (serverManager && requestedTransport && requestedTransport !== currentTransport) {
serverManager = new MCPServerManager(options);
currentTransport = requestedTransport;
}
if (!serverManager) {
serverManager = new MCPServerManager(options);
currentTransport = options?.transport;
}
return serverManager;
}
/**
* Quick start MCP server
*/
export async function startMCPServer(options) {
const manager = getServerManager(options);
return await manager.start();
}
/**
* Quick stop MCP server
*/
export async function stopMCPServer(force = false) {
if (serverManager) {
await serverManager.stop(force);
}
}
/**
* Get MCP server status
*/
export async function getMCPServerStatus() {
const manager = getServerManager();
return await manager.getStatus();
}
export default MCPServerManager;
//# sourceMappingURL=mcp-server.js.map