Skip to main content
Glama
mcp-client.ts7.09 kB
/** * Story 2: Pre-WebSocket Command Execution - MCPClient Implementation * * MCPClient provides communication with MCP server via stdin/stdout using MCP protocol. * This is the core functionality for executing commands before WebSocket connection. * * CRITICAL: No mocks in production code. This communicates with real MCP server. */ import { ChildProcess } from 'child_process'; import * as readline from 'readline'; export interface MCPClientConfig { timeout?: number; } interface MCPRequest { jsonrpc: string; method: string; params: { name: string; arguments: Record<string, unknown>; }; id: string; } interface MCPResponse { jsonrpc: string; id: string; result?: unknown; error?: { code: number; message: string; data?: unknown; }; } export interface MCPToolResponse { success: boolean; result?: unknown; error?: string; } export class MCPClient { private process: ChildProcess; private config: Required<MCPClientConfig>; private requestId: number = 0; private pendingRequests: Map<string, { resolve: (value: MCPToolResponse) => void; reject: (error: Error) => void; timeout: NodeJS.Timeout; }> = new Map(); private readline: readline.Interface | null = null; constructor(process: ChildProcess, config: MCPClientConfig = {}) { this.process = process; this.config = { timeout: config.timeout ?? 30000 }; this.setupMessageHandling(); } /** * Setup message handling for MCP protocol communication */ private setupMessageHandling(): void { if (!this.process || !this.process.stdout || !this.process.stdin) { throw new Error('Process must have stdin and stdout streams'); } // Setup readline to parse line-delimited JSON responses this.readline = readline.createInterface({ input: this.process.stdout, crlfDelay: Infinity }); this.readline.on('line', (line: string) => { // Filter out non-JSON lines (debug output, logs, etc.) const trimmedLine = line.trim(); // Skip empty lines and lines that don't look like JSON if (!trimmedLine || !trimmedLine.startsWith('{')) { return; } try { const response = JSON.parse(trimmedLine) as MCPResponse; this.handleResponse(response); } catch (error) { // Handle malformed JSON responses - but don't reject all if it's just debug output console.warn(`Ignoring non-JSON output from MCP server: ${trimmedLine}`); } }); this.readline.on('error', (error: Error) => { this.rejectAllPending(new Error(`MCP communication error: ${error.message}`)); }); } /** * Handle MCP protocol response from server */ private handleResponse(response: MCPResponse): void { const request = this.pendingRequests.get(response.id); if (!request) { // Ignore responses for unknown requests return; } // Clear timeout and remove from pending clearTimeout(request.timeout); this.pendingRequests.delete(response.id); if (response.error) { request.reject(new Error(`MCP Error: ${response.error.message}`)); } else { // Parse the result as MCP tool response const result = this.parseToolResponse(response.result); request.resolve(result); } } /** * Parse MCP tool response from raw result */ private parseToolResponse(result: unknown): MCPToolResponse { if (typeof result === 'object' && result !== null) { // Handle MCP tool response format: {content: [{text: "JSON_STRING", type: "text"}]} const resultObj = result as any; if (resultObj.content && Array.isArray(resultObj.content) && resultObj.content.length > 0) { const contentItem = resultObj.content[0]; if (contentItem.text && typeof contentItem.text === 'string') { try { // Parse the nested JSON from text field const parsedText = JSON.parse(contentItem.text); return parsedText as MCPToolResponse; } catch (error) { // If parsing fails, return error return { success: false, error: `Failed to parse tool response: ${error instanceof Error ? error.message : String(error)}` }; } } } // Direct result format return result as MCPToolResponse; } else { // Wrap primitive results return { success: true, result } as MCPToolResponse; } } /** * Reject all pending requests with the given error */ private rejectAllPending(error: Error): void { for (const request of this.pendingRequests.values()) { clearTimeout(request.timeout); request.reject(error); } this.pendingRequests.clear(); } /** * Send MCP message to server */ private sendMessage(message: MCPRequest): void { if (!this.process.stdin || this.process.stdin.destroyed) { throw new Error('Cannot send message: stdin stream is not available'); } const messageJson = JSON.stringify(message) + '\n'; this.process.stdin.write(messageJson); } /** * Generate unique request ID */ private generateRequestId(): string { return `req_${++this.requestId}_${Date.now()}`; } /** * Call MCP tool with given name and arguments * This is the core method for executing commands via MCP protocol */ public async callTool(name: string, args: Record<string, unknown>): Promise<MCPToolResponse> { const requestId = this.generateRequestId(); const request: MCPRequest = { jsonrpc: '2.0', method: 'tools/call', params: { name, arguments: args }, id: requestId }; return new Promise<MCPToolResponse>((resolve, reject) => { // Setup timeout const timeout = setTimeout(() => { this.pendingRequests.delete(requestId); reject(new Error(`MCP request timeout for tool '${name}'`)); }, this.config.timeout); // Store pending request this.pendingRequests.set(requestId, { resolve, reject, timeout }); try { // Send the request this.sendMessage(request); } catch (error) { // Clean up on send failure clearTimeout(timeout); this.pendingRequests.delete(requestId); reject(error); } }); } /** * Disconnect from MCP server and cleanup resources */ public async disconnect(): Promise<void> { // Reject all pending requests this.rejectAllPending(new Error('MCP client disconnecting')); // Close readline interface if (this.readline) { this.readline.close(); this.readline = null; } // Don't close process streams - they're managed by MCPServerManager } /** * Check if client is connected and ready for communication */ public isConnected(): boolean { return this.process.stdin !== null && !this.process.stdin.destroyed && this.process.stdout !== null && !this.process.stdout.destroyed; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LightspeedDMS/ssh-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server