UNPKG

behemoth-cli

Version:

🌍 BEHEMOTH CLIv3.760.4 - Level 50+ POST-SINGULARITY Intelligence Trading AI

350 lines 12.6 kB
/** * MCP Client Implementation for BEHEMOTH Integration * Handles communication with MCP servers using JSON-RPC 2.0 */ import { spawn } from 'child_process'; import { EventEmitter } from 'events'; import { logWarn } from '../utils/error-handler.js'; import { ResourceCleanupManager, managedSetTimeout } from '../utils/resource-cleanup.js'; export class MCPClient extends EventEmitter { config; requestTimeout; process = null; messageId = 0; pendingRequests = new Map(); initialized = false; serverInfo = null; availableTools = []; resourceManager = new ResourceCleanupManager(); constructor(config, requestTimeout = 120000) { super(); this.config = config; this.requestTimeout = requestTimeout; } /** * Connect to MCP server and initialize */ async connect() { try { // Validate spawn parameters for security this.validateSpawnParameters(); // Spawn MCP server process this.process = spawn(this.config.command, this.config.args || [], { cwd: this.config.cwd, env: { ...process.env, ...this.config.env }, stdio: ['pipe', 'pipe', 'pipe'] }); if (!this.process.stdin || !this.process.stdout || !this.process.stderr) { throw new Error('Failed to create process stdio streams'); } // Handle process events this.process.on('error', (error) => { this.emit('error', error); }); this.process.on('exit', (code, signal) => { this.emit('disconnect', { code, signal }); this.cleanup(); }); // Handle stderr for debugging this.process.stderr.on('data', (data) => { console.error(`MCP Server stderr: ${data}`); }); // Set up message handling let buffer = ''; this.process.stdout.on('data', (chunk) => { buffer += chunk.toString(); // Process complete JSON-RPC messages let newlineIndex; while ((newlineIndex = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, newlineIndex).trim(); buffer = buffer.slice(newlineIndex + 1); if (line) { try { const message = JSON.parse(line); this.handleMessage(message); } catch (error) { console.error('Failed to parse MCP message:', error); } } } }); // Initialize connection await this.initialize(); // Discover available tools await this.discoverTools(); this.emit('connected'); } catch (error) { this.cleanup(); throw error; } } /** * Disconnect from MCP server */ async disconnect() { if (this.process) { this.process.kill('SIGTERM'); // Give process time to exit gracefully await new Promise((resolve) => { const timeout = setTimeout(() => { if (this.process) { this.process.kill('SIGKILL'); } resolve(); }, 5000); this.process?.on('exit', () => { clearTimeout(timeout); resolve(); }); }); } this.cleanup(); } /** * Execute a BEHEMOTH tool via MCP */ async executeTool(toolCall) { if (!this.initialized) { throw new Error('MCP client not initialized'); } const startTime = Date.now(); try { const mcpCall = { name: toolCall.tool, arguments: { ...toolCall } }; const result = await this.callTool(mcpCall); return { success: true, data: result, timestamp: new Date().toISOString(), executionTime: Date.now() - startTime }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString(), executionTime: Date.now() - startTime }; } } /** * Get list of available BEHEMOTH tools */ getAvailableTools() { return [...this.availableTools]; } /** * Check if client is connected and initialized */ isConnected() { return this.initialized && this.process !== null && !this.process.killed; } /** * Get server information */ getServerInfo() { return this.serverInfo; } // Private methods validateSpawnParameters() { // Validate command if (!this.config.command || typeof this.config.command !== 'string') { throw new Error('Invalid command: must be a non-empty string'); } // Prevent dangerous commands const dangerousCommands = ['sh', 'bash', 'cmd', 'powershell', 'eval', 'rm', 'del']; const commandBase = this.config.command.split(/[/\\]/).pop() || ''; if (dangerousCommands.some(dangerous => commandBase.toLowerCase().includes(dangerous.toLowerCase()))) { throw new Error(`Potentially dangerous command detected: ${commandBase}`); } // Validate arguments if (this.config.args) { if (!Array.isArray(this.config.args)) { throw new Error('Invalid arguments: must be an array'); } this.config.args.forEach((arg, index) => { if (typeof arg !== 'string') { throw new Error(`Invalid argument at index ${index}: must be a string`); } // Check for injection patterns const dangerousPatterns = [';', '&&', '||', '|', '>', '<', '`', '$']; if (dangerousPatterns.some(pattern => arg.includes(pattern))) { throw new Error(`Potentially dangerous argument pattern detected: ${arg}`); } }); } // Validate working directory if (this.config.cwd && typeof this.config.cwd !== 'string') { throw new Error('Invalid working directory: must be a string'); } // Validate environment variables if (this.config.env) { if (typeof this.config.env !== 'object' || Array.isArray(this.config.env)) { throw new Error('Invalid environment: must be an object'); } Object.entries(this.config.env).forEach(([key, value]) => { if (typeof key !== 'string' || (value !== undefined && typeof value !== 'string')) { throw new Error(`Invalid environment variable: ${key} = ${value}`); } }); } } async initialize() { const params = { protocolVersion: '2024-11-05', capabilities: { sampling: {} }, clientInfo: { name: 'behemoth-crypto-cli', version: '1.0.0' } }; const result = await this.sendRequest('initialize', params); this.serverInfo = result.serverInfo; // Send initialized notification await this.sendNotification('notifications/initialized'); this.initialized = true; } async discoverTools() { try { const response = await this.sendRequest('tools/list'); if (response.tools && Array.isArray(response.tools)) { this.availableTools = response.tools.map((tool) => { return this.mapToBehemothTool(tool); }); } } catch (error) { logWarn('Failed to discover tools', error, { component: 'MCPClient', operation: 'discoverTools' }); this.availableTools = []; } } mapToBehemothTool(schema) { // Determine category based on tool name prefix let category = 'analysis'; let riskLevel = 'low'; let requiresAuth = false; if (schema.name.includes('place_order') || schema.name.includes('trade')) { category = 'trading'; riskLevel = 'high'; requiresAuth = true; } else if (schema.name.includes('cosmic') || schema.name.includes('planetary')) { category = 'cosmic'; } else if (schema.name.includes('risk') || schema.name.includes('var_')) { category = 'risk'; riskLevel = 'medium'; } else if (schema.name.includes('ticker') || schema.name.includes('price')) { category = 'market-data'; } else if (schema.name.includes('monitor') || schema.name.includes('health')) { category = 'monitoring'; } else if (schema.name.includes('optimizer') || schema.name.includes('performance')) { category = 'optimization'; } return { name: schema.name, category, description: schema.description, requiresAuth, riskLevel, schema }; } async callTool(toolCall) { const response = await this.sendRequest('tools/call', { name: toolCall.name, arguments: toolCall.arguments }); return response; } async sendRequest(method, params) { const id = ++this.messageId; const message = { jsonrpc: '2.0', id, method, params }; return new Promise((resolve, reject) => { const { timeoutId, cleanupId } = managedSetTimeout(() => { this.pendingRequests.delete(id); reject(new Error(`Request timeout: ${method}`)); }, this.requestTimeout, `MCP request timeout for ${method}`, this.resourceManager); this.pendingRequests.set(id, { resolve: (result) => { this.resourceManager.unregister(cleanupId); resolve(result); }, reject: (error) => { this.resourceManager.unregister(cleanupId); reject(error); }, timeout: timeoutId, cleanupId }); this.sendMessage(message); }); } async sendNotification(method, params) { const message = { jsonrpc: '2.0', method, params }; this.sendMessage(message); } sendMessage(message) { if (!this.process?.stdin) { throw new Error('No connection to MCP server'); } const data = JSON.stringify(message) + '\n'; this.process.stdin.write(data); } handleMessage(message) { if (message.id !== undefined) { // Response to our request const pending = this.pendingRequests.get(message.id); if (pending) { this.pendingRequests.delete(message.id); if (message.error) { pending.reject(new Error(`MCP Error: ${message.error.message}`)); } else { pending.resolve(message.result); } } } else if (message.method) { // Notification from server this.emit('notification', message.method, message.params); } } cleanup() { // Clear all pending requests for (const [id, pending] of this.pendingRequests) { this.resourceManager.unregister(pending.cleanupId); pending.reject(new Error('Connection closed')); } this.pendingRequests.clear(); // Clean up all remaining resources this.resourceManager.cleanupAll(); this.initialized = false; this.process = null; this.serverInfo = null; this.availableTools = []; } } //# sourceMappingURL=client.js.map