Skip to main content
Glama
rawMcpClient.ts6.36 kB
/** * Raw MCP Client for Protocol Testing * * This client provides low-level access to the MCP protocol without * any abstraction or interference, allowing us to test protocol compliance * directly. */ import { spawn, ChildProcessWithoutNullStreams } from 'child_process'; import { join } from 'path'; import { setTimeout as sleep } from 'timers/promises'; import { v4 as uuidv4 } from 'uuid'; interface JsonRpcRequest { jsonrpc: '2.0'; id: string; method: string; params?: any; } interface JsonRpcResponse { jsonrpc: '2.0'; id: string; result?: any; error?: { code: number; message: string; data?: any; }; } export class RawMcpClient { private serverProcess: ChildProcessWithoutNullStreams | null = null; private responseBuffer: string = ''; private pendingRequests: Map<string, { resolve: (value: JsonRpcResponse) => void; reject: (reason: any) => void; timeout: NodeJS.Timeout; }> = new Map(); private serverLogs: string[] = []; private isReady = false; /** * Start the MCP server and establish raw connection */ async connect(): Promise<void> { // Start the server process this.serverProcess = spawn('node', [join(process.cwd(), 'build', 'index.js')], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, NODE_ENV: 'test' } }); // Capture stderr for debugging this.serverProcess.stderr.on('data', (data) => { const log = data.toString(); this.serverLogs.push(`[stderr] ${log}`); }); // Handle stdout for JSON-RPC responses this.serverProcess.stdout.on('data', (data) => { const dataStr = data.toString(); this.responseBuffer += dataStr; // Check for server ready messages in stdout if (dataStr.includes('tmux server started') || dataStr.includes('Terminally-mcp server running on stdio') || dataStr.includes('Initializing MCP server')) { this.isReady = true; } this.processResponseBuffer(); }); // Handle process exit this.serverProcess.on('exit', (code) => { this.isReady = false; // Reject all pending requests for (const [id, pending] of this.pendingRequests) { clearTimeout(pending.timeout); pending.reject(new Error(`Server exited with code ${code}`)); } this.pendingRequests.clear(); }); // Wait for server to be ready let retries = 0; while (!this.isReady && retries < 30) { await sleep(200); retries++; } if (!this.isReady) { throw new Error('Server failed to start within timeout'); } // Give it a bit more time to fully initialize await sleep(500); } /** * Process the response buffer for complete JSON-RPC messages */ private processResponseBuffer(): void { const lines = this.responseBuffer.split('\n'); this.responseBuffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (!line.trim()) continue; try { const response = JSON.parse(line) as JsonRpcResponse; // Check if this is a valid JSON-RPC response if (response.jsonrpc === '2.0' && response.id) { const pending = this.pendingRequests.get(response.id); if (pending) { clearTimeout(pending.timeout); this.pendingRequests.delete(response.id); pending.resolve(response); } } } catch (error) { // Not valid JSON, might be debug output this.serverLogs.push(`[stdout-non-json] ${line}`); } } } /** * Send a raw JSON-RPC request and get the raw response */ async sendRawRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> { if (!this.serverProcess || !this.isReady) { throw new Error('Server is not connected'); } return new Promise((resolve, reject) => { // Set up timeout const timeout = setTimeout(() => { this.pendingRequests.delete(request.id); reject(new Error(`Request timeout for ${request.method}`)); }, 10000); // Store pending request this.pendingRequests.set(request.id, { resolve, reject, timeout }); // Send request const requestStr = JSON.stringify(request) + '\n'; this.serverProcess!.stdin.write(requestStr); }); } /** * Send a request with auto-generated ID */ async sendRequest(method: string, params?: any): Promise<JsonRpcResponse> { const request: JsonRpcRequest = { jsonrpc: '2.0', id: uuidv4(), method, params }; return this.sendRawRequest(request); } /** * Send a malformed request (for error testing) */ async sendMalformedRequest(data: string): Promise<JsonRpcResponse | null> { if (!this.serverProcess || !this.isReady) { throw new Error('Server is not connected'); } // Try to extract an ID if possible for tracking let id: string | null = null; try { const parsed = JSON.parse(data); id = parsed.id; } catch { // Malformed JSON, can't extract ID } if (id) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(id!); resolve(null); // Timeout means no response }, 2000); this.pendingRequests.set(id, { resolve, reject, timeout }); this.serverProcess!.stdin.write(data + '\n'); }); } else { // No ID, just send and don't expect a response this.serverProcess!.stdin.write(data + '\n'); await sleep(500); // Give it time to process return null; } } /** * Disconnect from the server */ async disconnect(): Promise<void> { if (this.serverProcess) { this.serverProcess.kill(); this.serverProcess = null; this.isReady = false; // Clear pending requests for (const [id, pending] of this.pendingRequests) { clearTimeout(pending.timeout); pending.reject(new Error('Client disconnected')); } this.pendingRequests.clear(); } } /** * Get server logs for debugging */ getServerLogs(): string[] { return [...this.serverLogs]; } /** * Clear server logs */ clearServerLogs(): void { this.serverLogs = []; } }

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/NightTrek/Terminally-mcp'

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