UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

333 lines (332 loc) 11 kB
import { log } from "../util/logging.js"; /** * Model Context Protocol client implementation * Enables TermCoder to integrate with external data sources * following Claude Code's MCP pattern */ export class MCPClient { servers = new Map(); resources = new Map(); tools = new Map(); prompts = new Map(); constructor() { } /** * Connect to an MCP server */ async connect(serverConfig) { try { const connection = new MCPServerConnection(serverConfig); await connection.initialize(); this.servers.set(serverConfig.name, connection); // Fetch available resources, tools, and prompts await this.refreshCapabilities(serverConfig.name); log.success(`Connected to MCP server: ${serverConfig.name}`); return true; } catch (error) { log.error(`Failed to connect to MCP server ${serverConfig.name}:`, error); return false; } } /** * Disconnect from an MCP server */ async disconnect(serverName) { const connection = this.servers.get(serverName); if (connection) { await connection.close(); this.servers.delete(serverName); // Remove associated resources/tools/prompts this.cleanupServerCapabilities(serverName); log.info(`Disconnected from MCP server: ${serverName}`); } } /** * List all available resources across servers */ async listResources() { return Array.from(this.resources.values()); } /** * Read resource content */ async readResource(uri) { for (const [serverName, connection] of this.servers) { try { const content = await connection.readResource(uri); if (content) return content; } catch (error) { log.warn(`Failed to read resource ${uri} from ${serverName}:`, error); } } return null; } /** * List available tools */ async listTools() { return Array.from(this.tools.values()); } /** * Call a tool */ async callTool(name, arguments_) { for (const [serverName, connection] of this.servers) { if (await connection.hasTool(name)) { try { return await connection.callTool(name, arguments_); } catch (error) { log.warn(`Tool call failed on ${serverName}:`, error); } } } throw new Error(`Tool ${name} not found or failed on all servers`); } /** * Get completion context from all connected servers with @-mention support */ async getCompletionContext(query) { const contexts = []; // Check for @-mentions in query const mentions = this.extractMentions(query); if (mentions.length > 0) { // Handle @-mentions specifically for (const mention of mentions) { const resource = Array.from(this.resources.values()).find(r => r.name.toLowerCase() === mention.toLowerCase() || r.uri.toLowerCase().includes(mention.toLowerCase())); if (resource) { const content = await this.readResource(resource.uri); if (content) { contexts.push(`[@${mention}]\n${content}`); } } } } else { // Get relevant resources by semantic matching const resources = await this.listResources(); for (const resource of resources) { if (this.isRelevantResource(resource, query)) { const content = await this.readResource(resource.uri); if (content) { contexts.push(`[${resource.name}]\n${content}`); } } } } return contexts; } /** * Refresh capabilities from a specific server */ async refreshCapabilities(serverName) { const connection = this.servers.get(serverName); if (!connection) return; try { // Fetch resources const resources = await connection.listResources(); for (const resource of resources) { this.resources.set(`${serverName}:${resource.uri}`, resource); } // Fetch tools const tools = await connection.listTools(); for (const tool of tools) { this.tools.set(`${serverName}:${tool.name}`, tool); } // Fetch prompts const prompts = await connection.listPrompts(); for (const prompt of prompts) { this.prompts.set(`${serverName}:${prompt.name}`, prompt); } } catch (error) { log.warn(`Failed to refresh capabilities for ${serverName}:`, error); } } /** * Clean up capabilities when disconnecting from server */ cleanupServerCapabilities(serverName) { // Remove resources for (const key of this.resources.keys()) { if (key.startsWith(`${serverName}:`)) { this.resources.delete(key); } } // Remove tools for (const key of this.tools.keys()) { if (key.startsWith(`${serverName}:`)) { this.tools.delete(key); } } // Remove prompts for (const key of this.prompts.keys()) { if (key.startsWith(`${serverName}:`)) { this.prompts.delete(key); } } } /** * Extract @-mentions from query */ extractMentions(query) { const mentionRegex = /@([\w\-\.]+)/g; const mentions = []; let match; while ((match = mentionRegex.exec(query)) !== null) { mentions.push(match[1]); } return mentions; } /** * Check if a resource is relevant to the query */ isRelevantResource(resource, query) { const queryLower = query.toLowerCase(); return (resource.name.toLowerCase().includes(queryLower) || (resource.description && resource.description.toLowerCase().includes(queryLower))); } /** * Get server health status */ async getServerHealth() { const health = []; for (const [name, connection] of this.servers) { try { await connection.ping(); health.push({ name, status: 'healthy' }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; health.push({ name, status: 'unhealthy', error: errorMessage }); } } return health; } } /** * Connection to a single MCP server */ class MCPServerConnection { process; serverConfig; constructor(config) { this.serverConfig = config; } async initialize() { const { spawn } = await import("node:child_process"); this.process = spawn(this.serverConfig.command, this.serverConfig.args, { env: { ...process.env, ...this.serverConfig.env }, stdio: ["pipe", "pipe", "pipe"] }); // Wait for initialization return new Promise((resolve, reject) => { let initData = ""; const timeout = setTimeout(() => { reject(new Error("MCP server initialization timeout")); }, 10000); this.process.stdout.on("data", (data) => { initData += data.toString(); if (initData.includes("initialized")) { clearTimeout(timeout); resolve(); } }); this.process.stderr.on("data", (data) => { log.warn(`MCP server stderr:`, data.toString()); }); this.process.on("error", (error) => { clearTimeout(timeout); reject(error); }); }); } async close() { if (this.process) { this.process.kill(); this.process = undefined; } } async reconnect() { await this.close(); await this.initialize(); } async ping() { try { await this.sendRequest("ping"); return true; } catch (error) { return false; } } async listResources() { return await this.sendRequest("resources/list"); } async readResource(uri) { const response = await this.sendRequest("resources/read", { uri }); return response?.contents?.[0]?.text || null; } async listTools() { return await this.sendRequest("tools/list"); } async callTool(name, arguments_) { return await this.sendRequest("tools/call", { name, arguments: arguments_ }); } async listPrompts() { return await this.sendRequest("prompts/list"); } async hasTool(name) { try { const tools = await this.listTools(); return tools.some(tool => tool.name === name); } catch (error) { return false; } } async sendRequest(method, params) { if (!this.process) { throw new Error("MCP server not connected"); } const request = { jsonrpc: "2.0", id: Math.random().toString(36), method, params: params || {} }; return new Promise((resolve, reject) => { let responseData = ""; const timeout = setTimeout(() => { reject(new Error(`MCP request timeout: ${method}`)); }, 30000); const dataHandler = (data) => { responseData += data.toString(); try { const response = JSON.parse(responseData); if (response.id === request.id) { clearTimeout(timeout); this.process.stdout.removeListener("data", dataHandler); if (response.error) { reject(new Error(response.error.message)); } else { resolve(response.result); } } } catch (e) { // Incomplete JSON, continue waiting } }; this.process.stdout.on("data", dataHandler); this.process.stdin.write(JSON.stringify(request) + "\n"); }); } } // Export singleton instance export const mcpClient = new MCPClient();