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
JavaScript
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();