Skip to main content
Glama
mcp-client.ts5.99 kB
import { spawn, ChildProcess } from 'child_process'; import { EventEmitter } from 'events'; export interface MCPRequest { jsonrpc: '2.0'; id: string | number; method: string; params?: Record<string, unknown> | undefined; } export interface MCPResponse { jsonrpc: '2.0'; id: string | number; result?: unknown; error?: { code: number; message: string; data?: unknown; }; } export class MCPClient extends EventEmitter { private serverProcess: ChildProcess | null = null; private requestIdCounter = 0; private pendingRequests = new Map< string | number, { resolve: (value: unknown) => void; reject: (reason: unknown) => void; timeout: NodeJS.Timeout; } >(); constructor( private serverPath: string, private env: Record<string, string> = {}, ) { super(); } async start(): Promise<void> { return new Promise((resolve, reject) => { this.serverProcess = spawn('node', [this.serverPath], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, ...this.env }, }); if ( !this.serverProcess.stdout || !this.serverProcess.stdin || !this.serverProcess.stderr ) { reject(new Error('Failed to create server process streams')); return; } let buffer = ''; let serverReady = false; this.serverProcess.stdout.on('data', (chunk) => { buffer += chunk.toString(); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.trim()) { try { const response: MCPResponse = JSON.parse(line); this.handleResponse(response); // If we get any valid JSON-RPC response, the server is ready if (!serverReady) { serverReady = true; resolve(); } } catch (error) { console.error('Failed to parse response:', line, error); } } } }); this.serverProcess.stderr.on('data', (chunk) => { const output = chunk.toString(); console.error('Server stderr:', output); }); this.serverProcess.on('error', (error) => { reject(error); }); this.serverProcess.on('exit', (code) => { if (code !== 0 && !serverReady) { reject(new Error(`Server process exited with code ${code}`)); } }); // Send an initialization request to check if server is ready setTimeout(() => { if (!serverReady) { try { // Try to send an initialization request to test server readiness const initRequest = { jsonrpc: '2.0' as const, id: 'startup-test', method: 'initialize', }; const requestLine = JSON.stringify(initRequest) + '\n'; this.serverProcess!.stdin!.write(requestLine); } catch { // Ignore errors during initialization test } } }, 1000); setTimeout(() => { if (!serverReady) { reject(new Error('Server startup timeout')); } }, 10000); }); } private handleResponse(response: MCPResponse): void { const pending = this.pendingRequests.get(response.id); if (pending) { clearTimeout(pending.timeout); this.pendingRequests.delete(response.id); if (response.error) { pending.reject( new Error(`${response.error.code}: ${response.error.message}`), ); } else { pending.resolve(response.result); } } } async sendRequest( method: string, params?: Record<string, unknown> | undefined, ): Promise<unknown> { if (!this.serverProcess || !this.serverProcess.stdin) { throw new Error('Server not started'); } const id = ++this.requestIdCounter; const request: MCPRequest = { jsonrpc: '2.0', id, method, params, }; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(id); reject(new Error(`Request timeout for method: ${method}`)); }, 10000); this.pendingRequests.set(id, { resolve, reject, timeout }); const requestLine = JSON.stringify(request) + '\n'; this.serverProcess!.stdin!.write(requestLine); }); } async listTools(): Promise<{ tools: Array<{ name: string; description: string; inputSchema: Record<string, unknown>; }>; }> { return this.sendRequest('tools/list') as Promise<{ tools: Array<{ name: string; description: string; inputSchema: Record<string, unknown>; }>; }>; } async callTool( name: string, arguments_: Record<string, unknown>, ): Promise<{ content: Array<{ type: string; text: string }> }> { const response = (await this.sendRequest('tools/call', { name, arguments: arguments_, })) as { content: Array<{ type: string; text: string }>; isError?: boolean; }; if (response.isError) { throw new Error(response.content[0]?.text || 'Tool call failed'); } return response; } async getServerInfo(): Promise<{ protocolVersion: string; capabilities: Record<string, unknown>; serverInfo: Record<string, unknown>; }> { return this.sendRequest('initialize', { protocolVersion: '2024-11-05', capabilities: { tools: {}, }, clientInfo: { name: 'playwright-test-client', version: '1.0.0', }, }) as Promise<{ protocolVersion: string; capabilities: Record<string, unknown>; serverInfo: Record<string, unknown>; }>; } async stop(): Promise<void> { if (this.serverProcess) { this.serverProcess.kill(); this.serverProcess = null; } this.pendingRequests.clear(); } }

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/withinfocus/tba'

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