UNPKG

claude-flow

Version:

Enterprise-grade AI agent orchestration with WASM-powered ReasoningBank memory and AgentDB vector database (always uses latest agentic-flow)

416 lines (367 loc) 10.3 kB
/** * In-Process MCP Server Implementation * * Provides 10-100x performance improvement by eliminating IPC overhead. * Uses Claude Code SDK's in-process server API for direct tool registration. */ import { EventEmitter } from 'node:events'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { logger } from '../core/logger.js'; import type { MCPTool, MCPContext } from '../utils/types.js'; export interface InProcessServerConfig { name: string; version?: string; tools?: MCPTool[]; enableMetrics?: boolean; enableCaching?: boolean; } export interface ToolCallMetrics { toolName: string; duration: number; success: boolean; timestamp: number; transport: 'in-process' | 'stdio' | 'sse'; } /** * In-Process MCP Server * Registers Claude-Flow tools directly in the SDK process to eliminate IPC overhead */ export class InProcessMCPServer extends EventEmitter { private name: string; private version: string; private tools: Map<string, MCPTool>; private metrics: ToolCallMetrics[]; private context?: MCPContext; private enableMetrics: boolean; private enableCaching: boolean; private cache: Map<string, { result: any; timestamp: number; ttl: number }>; constructor(config: InProcessServerConfig) { super(); this.name = config.name; this.version = config.version || '1.0.0'; this.tools = new Map(); this.metrics = []; this.enableMetrics = config.enableMetrics !== false; this.enableCaching = config.enableCaching !== false; this.cache = new Map(); if (config.tools) { config.tools.forEach(tool => this.registerTool(tool)); } logger.info('InProcessMCPServer initialized', { name: this.name, version: this.version, toolCount: this.tools.size, }); } /** * Register a tool for in-process execution */ registerTool(tool: MCPTool): void { if (this.tools.has(tool.name)) { logger.warn('Tool already registered, overwriting', { name: tool.name }); } this.tools.set(tool.name, tool); logger.debug('Tool registered', { name: tool.name }); this.emit('toolRegistered', tool.name); } /** * Unregister a tool */ unregisterTool(name: string): boolean { const deleted = this.tools.delete(name); if (deleted) { logger.debug('Tool unregistered', { name }); this.emit('toolUnregistered', name); } return deleted; } /** * Get all registered tool names */ getToolNames(): string[] { return Array.from(this.tools.keys()); } /** * Get tool metadata */ getTool(name: string): MCPTool | undefined { return this.tools.get(name); } /** * Execute a tool call in-process */ async callTool( name: string, args: Record<string, unknown>, context?: MCPContext ): Promise<CallToolResult> { const startTime = performance.now(); let success = false; try { // Check cache first if (this.enableCaching) { const cached = this.checkCache(name, args); if (cached) { logger.debug('Cache hit for tool', { name }); success = true; return cached; } } const tool = this.tools.get(name); if (!tool) { throw new Error(`Tool not found: ${name}`); } logger.debug('Executing tool in-process', { name, args }); // Merge context const execContext: MCPContext = { ...this.context, ...context, }; // Execute tool handler directly (in-process, no IPC!) const result = await tool.handler(args, execContext); // Cache result if applicable if (this.enableCaching && this.isCacheable(name)) { this.cacheResult(name, args, result); } success = true; // Return in MCP CallToolResult format return { content: [ { type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2), }, ], isError: false, }; } catch (error) { logger.error('Tool execution failed', { name, error }); success = false; return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } finally { // Record metrics if (this.enableMetrics) { const duration = performance.now() - startTime; this.recordMetric({ toolName: name, duration, success, timestamp: Date.now(), transport: 'in-process', }); } } } /** * Set execution context (orchestrator, session, etc.) */ setContext(context: MCPContext): void { this.context = context; logger.debug('Context updated', { sessionId: context.sessionId }); } /** * Get performance metrics */ getMetrics(): ToolCallMetrics[] { return [...this.metrics]; } /** * Get performance statistics */ getStats() { const stats: Record< string, { totalCalls: number; successRate: number; avgDuration: number; minDuration: number; maxDuration: number; } > = {}; // Group by tool name const grouped = new Map<string, ToolCallMetrics[]>(); for (const metric of this.metrics) { if (!grouped.has(metric.toolName)) { grouped.set(metric.toolName, []); } grouped.get(metric.toolName)!.push(metric); } // Calculate statistics for (const [toolName, metrics] of grouped) { const totalCalls = metrics.length; const successfulCalls = metrics.filter(m => m.success).length; const durations = metrics.map(m => m.duration); const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length; const minDuration = Math.min(...durations); const maxDuration = Math.max(...durations); stats[toolName] = { totalCalls, successRate: successfulCalls / totalCalls, avgDuration, minDuration, maxDuration, }; } return { toolStats: stats, totalCalls: this.metrics.length, averageDuration: this.metrics.reduce((sum, m) => sum + m.duration, 0) / this.metrics.length || 0, cacheHitRate: this.getCacheHitRate(), }; } /** * Clear metrics */ clearMetrics(): void { this.metrics = []; logger.debug('Metrics cleared'); } /** * Clear cache */ clearCache(): void { this.cache.clear(); logger.debug('Cache cleared'); } /** * Get server info */ getInfo() { return { name: this.name, version: this.version, toolCount: this.tools.size, tools: this.getToolNames(), metrics: { totalCalls: this.metrics.length, cacheSize: this.cache.size, }, }; } /** * Record a metric */ private recordMetric(metric: ToolCallMetrics): void { this.metrics.push(metric); this.emit('metricRecorded', metric); // Keep only last 1000 metrics if (this.metrics.length > 1000) { this.metrics = this.metrics.slice(-1000); } } /** * Check cache for a tool call result */ private checkCache( name: string, args: Record<string, unknown> ): CallToolResult | undefined { const cacheKey = this.getCacheKey(name, args); const cached = this.cache.get(cacheKey); if (cached) { const now = Date.now(); if (now - cached.timestamp < cached.ttl) { return { content: [ { type: 'text', text: typeof cached.result === 'string' ? cached.result : JSON.stringify(cached.result, null, 2), }, ], isError: false, }; } else { // Expired, remove from cache this.cache.delete(cacheKey); } } return undefined; } /** * Cache a tool result */ private cacheResult(name: string, args: Record<string, unknown>, result: any): void { const cacheKey = this.getCacheKey(name, args); const ttl = this.getCacheTTL(name); this.cache.set(cacheKey, { result, timestamp: Date.now(), ttl, }); // Limit cache size if (this.cache.size > 100) { const firstKey = this.cache.keys().next().value; if (firstKey) { this.cache.delete(firstKey); } } } /** * Generate cache key */ private getCacheKey(name: string, args: Record<string, unknown>): string { return `${name}:${JSON.stringify(args)}`; } /** * Determine if a tool is cacheable */ private isCacheable(name: string): boolean { // Only cache read-only operations const cacheableTools = [ 'agents/list', 'agents/info', 'tasks/list', 'tasks/status', 'system/status', 'system/metrics', 'config/get', 'workflow/list', 'terminal/list', ]; return cacheableTools.includes(name); } /** * Get cache TTL for a tool */ private getCacheTTL(name: string): number { // Different TTLs for different tools const ttls: Record<string, number> = { 'agents/list': 5000, // 5 seconds 'agents/info': 10000, // 10 seconds 'system/status': 2000, // 2 seconds 'config/get': 30000, // 30 seconds }; return ttls[name] || 10000; // Default 10 seconds } /** * Calculate cache hit rate */ private getCacheHitRate(): number { // Simple estimation based on cache size vs total calls if (this.metrics.length === 0) return 0; const cacheableMetrics = this.metrics.filter(m => this.isCacheable(m.toolName)); if (cacheableMetrics.length === 0) return 0; // Estimate: assume cache hits are very fast (< 1ms) const likelyCacheHits = cacheableMetrics.filter(m => m.duration < 1).length; return likelyCacheHits / cacheableMetrics.length; } } /** * Factory function to create an in-process MCP server */ export function createInProcessServer(config: InProcessServerConfig): InProcessMCPServer { return new InProcessMCPServer(config); }