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
text/typescript
/**
* 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);
}