UNPKG

@webdevtoday/grok-cli

Version:

A sophisticated CLI tool for interacting with xAI Grok 4, featuring conversation history, file reference, custom commands, memory system, and genetic development workflows

519 lines 16.7 kB
"use strict"; /** * MCP (Model Context Protocol) Client Implementation * Provides integration with MCP servers using Node.js native capabilities */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.McpClient = exports.McpServerConnection = void 0; const child_process_1 = require("child_process"); const events_1 = require("events"); const crypto_1 = require("crypto"); const chalk_1 = __importDefault(require("chalk")); /** * MCP Server Connection Manager */ class McpServerConnection extends events_1.EventEmitter { constructor(name, config) { super(); this.process = null; this.messageBuffer = ''; this.pendingRequests = new Map(); this.status = 'disconnected'; this.capabilities = {}; this.tools = []; this.prompts = []; this.resources = []; this.name = name; this.config = config; } /** * Connect to the MCP server */ async connect() { if (this.status === 'connected' || this.status === 'connecting') { return; } this.status = 'connecting'; this.emit('statusChange', 'connecting'); try { // Spawn the server process this.process = (0, child_process_1.spawn)(this.config.command, this.config.args || [], { cwd: this.config.cwd || process.cwd(), env: { ...process.env, ...this.config.env }, stdio: ['pipe', 'pipe', 'pipe'] }); // Handle process events this.process.on('error', (error) => { this.handleError(new Error(`Process error: ${error.message}`)); }); this.process.on('exit', (code, signal) => { this.handleDisconnect(`Process exited with code ${code}, signal ${signal}`); }); // Set up stdin/stdout communication if (this.process.stdout) { this.process.stdout.setEncoding('utf8'); this.process.stdout.on('data', (data) => { this.handleRawMessage(data); }); } if (this.process.stderr) { this.process.stderr.setEncoding('utf8'); this.process.stderr.on('data', (data) => { console.error(chalk_1.default.red(`[${this.name}] STDERR:`), data); }); } // Initialize the connection await this.initialize(); this.status = 'connected'; this.emit('statusChange', 'connected'); this.emit('connected'); // Load capabilities await this.loadCapabilities(); } catch (error) { this.handleError(error instanceof Error ? error : new Error(String(error))); } } /** * Disconnect from the MCP server */ async disconnect() { if (this.process) { this.process.kill('SIGTERM'); this.process = null; } // Clear pending requests for (const [, pending] of this.pendingRequests) { clearTimeout(pending.timeout); pending.reject(new Error('Connection closed')); } this.pendingRequests.clear(); this.status = 'disconnected'; this.emit('statusChange', 'disconnected'); this.emit('disconnected'); } /** * Send a request to the MCP server */ async sendRequest(method, params, timeout = 30000) { if (this.status !== 'connected') { throw new Error('Server not connected'); } const id = (0, crypto_1.randomUUID)(); const request = { jsonrpc: '2.0', id, method, params }; return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { this.pendingRequests.delete(id); reject(new Error(`Request timeout: ${method}`)); }, timeout); this.pendingRequests.set(id, { resolve: (result) => { clearTimeout(timeoutHandle); resolve(result); }, reject: (error) => { clearTimeout(timeoutHandle); reject(error); }, timeout: timeoutHandle }); this.sendMessage(request); }); } /** * Send a notification to the MCP server */ sendNotification(method, params) { const notification = { jsonrpc: '2.0', method, params }; this.sendMessage(notification); } /** * Call a tool on the MCP server */ async callTool(name, arguments_ = {}) { return this.sendRequest('tools/call', { name, arguments: arguments_ }); } /** * Get a prompt from the MCP server */ async getPrompt(name, arguments_ = {}) { return this.sendRequest('prompts/get', { name, arguments: arguments_ }); } /** * Read a resource from the MCP server */ async readResource(uri) { return this.sendRequest('resources/read', { uri }); } /** * List available tools */ async listTools() { const response = await this.sendRequest('tools/list'); this.tools = response.tools || []; return this.tools; } /** * List available prompts */ async listPrompts() { const response = await this.sendRequest('prompts/list'); this.prompts = response.prompts || []; return this.prompts; } /** * List available resources */ async listResources() { const response = await this.sendRequest('resources/list'); this.resources = response.resources || []; return this.resources; } /** * Initialize the MCP connection */ async initialize() { const initRequest = { protocolVersion: '2024-11-05', capabilities: { roots: { listChanged: true }, sampling: {} }, clientInfo: { name: 'grok-cli', version: '0.2.0' } }; const response = await this.sendRequest('initialize', initRequest); this.capabilities = response.capabilities || {}; // Send initialized notification this.sendNotification('notifications/initialized'); } /** * Load server capabilities and available features */ async loadCapabilities() { try { if (this.capabilities.tools) { await this.listTools(); } if (this.capabilities.prompts) { await this.listPrompts(); } if (this.capabilities.resources) { await this.listResources(); } } catch (error) { console.warn(chalk_1.default.yellow(`[${this.name}] Failed to load some capabilities:`, error)); } } /** * Send a message to the server */ sendMessage(message) { if (!this.process?.stdin) { throw new Error('Server stdin not available'); } const messageStr = JSON.stringify(message) + '\n'; this.process.stdin.write(messageStr); } /** * Handle raw message data from server */ handleRawMessage(data) { this.messageBuffer += data; // Process complete messages (one per line) const lines = this.messageBuffer.split('\n'); this.messageBuffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (line.trim()) { try { const message = JSON.parse(line); this.handleMessage(message); } catch (error) { console.error(chalk_1.default.red(`[${this.name}] Failed to parse message:`), line); } } } } /** * Handle a parsed message from the server */ handleMessage(message) { if (message.id !== undefined) { // This is a response to a request const pending = this.pendingRequests.get(message.id); if (pending) { this.pendingRequests.delete(message.id); if (message.error) { pending.reject(new Error(`${message.error.message} (${message.error.code})`)); } else { pending.resolve(message.result); } } } else if (message.method) { // This is a notification this.handleNotification(message); } } /** * Handle notifications from the server */ handleNotification(notification) { this.emit('notification', notification); switch (notification.method) { case 'notifications/tools/list_changed': this.listTools().catch(console.error); break; case 'notifications/prompts/list_changed': this.listPrompts().catch(console.error); break; case 'notifications/resources/list_changed': this.listResources().catch(console.error); break; } } /** * Handle connection errors */ handleError(error) { this.status = 'error'; this.emit('statusChange', 'error'); this.emit('error', error); } /** * Handle disconnection */ handleDisconnect(reason) { this.status = 'disconnected'; this.emit('statusChange', 'disconnected'); this.emit('disconnected', reason); } } exports.McpServerConnection = McpServerConnection; /** * MCP Client Manager */ class McpClient extends events_1.EventEmitter { constructor() { super(...arguments); this.servers = new Map(); this.autoReconnect = true; this.reconnectDelay = 5000; } /** * Add a new MCP server */ addServer(name, config) { if (this.servers.has(name)) { throw new Error(`Server ${name} already exists`); } const server = new McpServerConnection(name, config); // Set up event handlers server.on('connected', () => { console.log(chalk_1.default.green(`[MCP] Connected to ${name}`)); this.emit('serverConnected', name, server); }); server.on('disconnected', (reason) => { console.log(chalk_1.default.yellow(`[MCP] Disconnected from ${name}: ${reason}`)); this.emit('serverDisconnected', name, reason); if (this.autoReconnect) { setTimeout(() => { if (this.servers.has(name)) { console.log(chalk_1.default.blue(`[MCP] Reconnecting to ${name}...`)); server.connect().catch(console.error); } }, this.reconnectDelay); } }); server.on('error', (error) => { console.error(chalk_1.default.red(`[MCP] Error from ${name}:`), error.message); this.emit('serverError', name, error); }); server.on('notification', (notification) => { this.emit('notification', name, notification); }); this.servers.set(name, server); } /** * Remove an MCP server */ async removeServer(name) { const server = this.servers.get(name); if (server) { await server.disconnect(); this.servers.delete(name); } } /** * Connect to a specific server */ async connectServer(name) { const server = this.servers.get(name); if (!server) { throw new Error(`Server ${name} not found`); } await server.connect(); } /** * Connect to all configured servers */ async connectAll() { const promises = Array.from(this.servers.values()).map(server => server.connect().catch(error => console.error(chalk_1.default.red(`Failed to connect to ${server.name}:`), error.message))); await Promise.allSettled(promises); } /** * Disconnect from all servers */ async disconnectAll() { const promises = Array.from(this.servers.values()).map(server => server.disconnect()); await Promise.allSettled(promises); } /** * Get a server by name */ getServer(name) { return this.servers.get(name); } /** * Get all servers */ getAllServers() { return Array.from(this.servers.entries()).map(([name, connection]) => ({ name, config: connection.config, status: connection.status, tools: connection.tools.map(t => t.name), prompts: connection.prompts.map(p => p.name) })); } /** * Get all available tools from all connected servers */ getAllTools() { const tools = []; for (const [serverName, server] of this.servers) { if (server.status === 'connected') { for (const tool of server.tools) { tools.push({ server: serverName, tool }); } } } return tools; } /** * Get all available prompts from all connected servers */ getAllPrompts() { const prompts = []; for (const [serverName, server] of this.servers) { if (server.status === 'connected') { for (const prompt of server.prompts) { prompts.push({ server: serverName, prompt }); } } } return prompts; } /** * Get all available resources from all connected servers */ getAllResources() { const resources = []; for (const [serverName, server] of this.servers) { if (server.status === 'connected') { for (const resource of server.resources) { resources.push({ server: serverName, resource }); } } } return resources; } /** * Call a tool on a specific server */ async callTool(serverName, toolName, arguments_ = {}) { const server = this.servers.get(serverName); if (!server) { throw new Error(`Server ${serverName} not found`); } if (server.status !== 'connected') { throw new Error(`Server ${serverName} not connected`); } return server.callTool(toolName, arguments_); } /** * Get a prompt from a specific server */ async getPrompt(serverName, promptName, arguments_ = {}) { const server = this.servers.get(serverName); if (!server) { throw new Error(`Server ${serverName} not found`); } if (server.status !== 'connected') { throw new Error(`Server ${serverName} not connected`); } return server.getPrompt(promptName, arguments_); } /** * Read a resource from a specific server */ async readResource(serverName, uri) { const server = this.servers.get(serverName); if (!server) { throw new Error(`Server ${serverName} not found`); } if (server.status !== 'connected') { throw new Error(`Server ${serverName} not connected`); } return server.readResource(uri); } /** * Set auto-reconnect behavior */ setAutoReconnect(enabled, delay = 5000) { this.autoReconnect = enabled; this.reconnectDelay = delay; } /** * Get connection statistics */ getStats() { const connected = Array.from(this.servers.values()).filter(s => s.status === 'connected'); return { totalServers: this.servers.size, connectedServers: connected.length, totalTools: connected.reduce((sum, s) => sum + s.tools.length, 0), totalPrompts: connected.reduce((sum, s) => sum + s.prompts.length, 0), totalResources: connected.reduce((sum, s) => sum + s.resources.length, 0) }; } } exports.McpClient = McpClient; //# sourceMappingURL=mcp-client.js.map