@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
JavaScript
"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