699 lines
25 KiB
JavaScript
699 lines
25 KiB
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 { execFileSync } from 'child_process';
|
|
import { request as httpRequestFn } from 'http';
|
|
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';
|
|
import { trackRequest } from './mcp-tools/request-tracker.js';
|
|
// 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 (skip if status reports our own PID —
|
|
// getStatus() returns running=true for the current process in stdio mode
|
|
// even before the server is actually started)
|
|
const status = await this.getStatus();
|
|
if (status.running && status.pid !== process.pid) {
|
|
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) {
|
|
// No PID file found. Detect if we are running in stdio mode
|
|
// (e.g., launched by Claude Code via `claude mcp add`).
|
|
const isStdio = !process.stdin.isTTY;
|
|
const envTransport = process.env.CLAUDE_FLOW_MCP_TRANSPORT;
|
|
if (isStdio || envTransport === 'stdio' || this.options.transport === 'stdio') {
|
|
return {
|
|
running: true,
|
|
pid: process.pid,
|
|
transport: 'stdio',
|
|
startedAt: this.startTime?.toISOString(),
|
|
uptime: this.startTime
|
|
? Math.floor((Date.now() - this.startTime.getTime()) / 1000)
|
|
: undefined,
|
|
};
|
|
}
|
|
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`);
|
|
// Auto-initialize memory database before tools are registered (#1524)
|
|
// This ensures memory_store and other memory tools work immediately
|
|
// without waiting for the first tool call to trigger lazy init.
|
|
try {
|
|
const { initializeMemoryDatabase, checkMemoryInitialization } = await import('./memory/memory-initializer.js');
|
|
const status = await checkMemoryInitialization();
|
|
if (!status.initialized) {
|
|
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Auto-initializing memory database...`);
|
|
const result = await initializeMemoryDatabase({ force: false, verbose: false });
|
|
if (result.success) {
|
|
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Memory database initialized at ${result.dbPath}`);
|
|
}
|
|
else if (result.error && !result.error.includes('already exists')) {
|
|
console.error(`[${new Date().toISOString()}] WARN [claude-flow-mcp] (${sessionId}) Memory database init returned: ${result.error}`);
|
|
}
|
|
}
|
|
else {
|
|
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Memory database already initialized (v${status.version || 'unknown'})`);
|
|
}
|
|
}
|
|
catch (memInitError) {
|
|
// Graceful degradation: server continues even if memory init fails.
|
|
// Memory tools will attempt lazy init on first call via ensureInitialized().
|
|
console.error(`[${new Date().toISOString()}] WARN [claude-flow-mcp] (${sessionId}) Memory auto-init failed (tools will retry on first call): ${memInitError instanceof Error ? memInitError.message : String(memInitError)}`);
|
|
}
|
|
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: 'ruflo',
|
|
version: VERSION,
|
|
capabilities: {
|
|
tools: { listChanged: true },
|
|
resources: { subscribe: true, listChanged: true },
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
// Handle stdin messages (S-5: bounded buffer to prevent OOM)
|
|
const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
|
|
let buffer = '';
|
|
process.stdin.on('data', async (chunk) => {
|
|
buffer += chunk.toString();
|
|
if (buffer.length > MAX_BUFFER_SIZE) {
|
|
console.error(`[${new Date().toISOString()}] ERROR [claude-flow-mcp] Buffer exceeded ${MAX_BUFFER_SIZE} bytes, rejecting`);
|
|
buffer = '';
|
|
console.log(JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
error: { code: -32600, message: 'Request too large' },
|
|
}));
|
|
return;
|
|
}
|
|
// 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: 'ruflo', 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 });
|
|
trackRequest(toolName, true);
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id: message.id,
|
|
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] },
|
|
};
|
|
}
|
|
catch (error) {
|
|
trackRequest(toolName, false);
|
|
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);
|
|
this.healthCheckInterval.unref();
|
|
}
|
|
/**
|
|
* 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
|
|
}
|
|
// Also clean up legacy PID file location from older versions
|
|
try {
|
|
const legacyPath = path.join(process.env.CLAUDE_FLOW_CWD || process.cwd(), '.claude-flow', 'mcp-server.pid');
|
|
if (legacyPath !== this.options.pidFile) {
|
|
await fs.promises.unlink(legacyPath);
|
|
}
|
|
}
|
|
catch {
|
|
// Ignore — file may not exist
|
|
}
|
|
}
|
|
/**
|
|
* Check if process is running AND is a node/claude-flow process.
|
|
* Plain `kill -0` returns true for any process with the same owner,
|
|
* which causes false positives when the OS recycles the PID.
|
|
*/
|
|
isProcessRunning(pid) {
|
|
try {
|
|
process.kill(pid, 0);
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
// Verify it's actually a node process (guards against PID reuse)
|
|
// DA-CRIT-3: Use execFileSync to prevent command injection via PID values
|
|
try {
|
|
const safePid = String(Math.floor(Math.abs(pid)));
|
|
let cmdline = '';
|
|
try {
|
|
// Try /proc on Linux
|
|
cmdline = fs.readFileSync(`/proc/${safePid}/cmdline`, 'utf8');
|
|
}
|
|
catch {
|
|
// Fall back to ps on macOS/other
|
|
try {
|
|
cmdline = execFileSync('ps', ['-p', safePid, '-o', 'comm='], {
|
|
encoding: 'utf8',
|
|
timeout: 1000,
|
|
}).trim();
|
|
}
|
|
catch {
|
|
// ps failed — fall through
|
|
}
|
|
}
|
|
if (!cmdline)
|
|
return true; // Can't inspect, fall back to kill check
|
|
// Must be a node process to be our MCP server
|
|
return cmdline.includes('node') || cmdline.includes('claude-flow') || cmdline.includes('npx');
|
|
}
|
|
catch {
|
|
// If we can't inspect the process (macOS, Windows, permissions), fall back to kill check
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* Make HTTP request
|
|
*/
|
|
async httpRequest(url, method, timeout) {
|
|
return new Promise((resolve, reject) => {
|
|
const urlObj = new URL(url);
|
|
const req = httpRequestFn({
|
|
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
|