behemoth-cli
Version:
🌍 BEHEMOTH CLIv3.760.4 - Level 50+ POST-SINGULARITY Intelligence Trading AI
350 lines • 12.6 kB
JavaScript
/**
* 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