Gemini MCP Server

  • src
import { spawn, ChildProcess } from 'child_process'; import WebSocket from 'ws'; export interface MCPServerParameters { command: string; args: string[]; env?: NodeJS.ProcessEnv | null; } export interface MCPClient { connect(): Promise<void>; disconnect(): Promise<void>; call_tool(toolName: string): (args: any) => Promise<any>; list_tools(): Promise<any[]>; } export class MCPClientImpl implements MCPClient { private process: ChildProcess | null = null; private socket: WebSocket | null = null; private messageQueue: Buffer[] = []; private currentResolver: ((value: Buffer) => void) | null = null; private rpcInterface: { read: () => Promise<Buffer>; write: (data: Buffer) => Promise<void>; } | null = null; constructor(private serverParams: MCPServerParameters) { console.log('MCPClientImpl initialized with params:', { command: serverParams.command, args: serverParams.args, env: serverParams.env ? Object.keys(serverParams.env) : null }); } async connect(): Promise<void> { console.log('Attempting to connect to MCP server...'); return new Promise((resolve, reject) => { console.log('Spawning process:', this.serverParams.command, this.serverParams.args); this.process = spawn(this.serverParams.command, this.serverParams.args, { env: { ...process.env, ...this.serverParams.env } }); if (!this.process) { const error = new Error('Failed to start MCP server process'); console.error('Spawn failed:', error); reject(error); return; } console.log('Process spawned with PID:', this.process.pid); this.process.on('error', (err: Error) => { console.error('MCP server process error:', err); console.error('Error details:', { message: err.message, name: err.name, stack: err.stack }); reject(new Error(`Failed to execute MCP server: ${err.message}`)); this.process = null; }); this.process.on('exit', (code: number, signal: string) => { console.warn(`MCP server process exited with code ${code} and signal ${signal}`); this.process = null; }); let wsUrl = ''; this.process.stdout?.on('data', (data: Buffer) => { const msg = data.toString('utf-8'); console.log('MCP server stdout:', msg); const match = msg.match(/ws:\/\/localhost:\d+/); if (match) { wsUrl = match[0]; console.log('WebSocket URL found:', wsUrl); this.createWebSocket(wsUrl).then(resolve).catch(reject); } }); this.process.stderr?.on('data', (data: Buffer) => { console.error(`MCP server stderr: ${data.toString('utf-8')}`); }); }); } private async createWebSocket(wsUrl: string): Promise<void> { console.log('Creating WebSocket connection to:', wsUrl); return new Promise((resolve, reject) => { this.socket = new WebSocket(wsUrl); this.socket.on('open', () => { console.log('WebSocket connection established'); this.rpcInterface = { read: async () => { console.log('RPC read called'); return new Promise<Buffer>((resolveRead) => { if (this.messageQueue.length > 0) { const message = Buffer.concat(this.messageQueue); this.messageQueue = []; console.log('Reading from message queue:', message.toString()); resolveRead(message); } else { console.log('Waiting for message...'); this.currentResolver = resolveRead; } }); }, write: async (data: Buffer) => { console.log('RPC write called with data:', data.toString()); if (!this.socket?.readyState) { const error = new Error('WebSocket not connected'); console.error('Write failed:', error); throw error; } this.socket.send(data); console.log('Data sent successfully'); }, }; resolve(); }); this.socket.on('message', (data: WebSocket.Data) => { console.log('WebSocket message received:', data.toString()); const buffer = Buffer.from(data as Buffer); if (this.currentResolver) { console.log('Resolving pending read'); this.currentResolver(buffer); this.currentResolver = null; } else { console.log('Queueing message'); this.messageQueue.push(buffer); } }); this.socket.on('error', (err: Error) => { console.error('WebSocket error:', { message: err.message, name: err.name, stack: err.stack }); reject(new Error(`WebSocket connection failed: ${err.message}`)); }); this.socket.on('close', (code: number, reason: Buffer) => { console.log(`WebSocket connection closed with code ${code}`, { reason: reason.toString(), wasClean: code === 1000 }); }); // Add connection timeout setTimeout(() => { if (this.socket?.readyState !== WebSocket.OPEN) { const error = new Error('WebSocket connection timeout'); console.error('Connection timeout:', error); reject(error); } }, 10000); // 10 second timeout }); } async list_tools(): Promise<any[]> { if (!this.rpcInterface) { throw new Error('Not connected to MCP server'); } const request = { jsonrpc: '2.0', method: 'list_tools', id: Math.floor(Math.random() * 1000000), }; await this.rpcInterface.write(Buffer.from(JSON.stringify(request))); const response = await this.rpcInterface.read(); const result = JSON.parse(response.toString()); if (result.error) { throw new Error(result.error.message); } return result.result; } call_tool(toolName: string): (args: any) => Promise<any> { const rpcInterface = this.rpcInterface; if (!rpcInterface) { throw new Error('Not connected to MCP server'); } return async (args: any) => { const request = { jsonrpc: '2.0', method: toolName, params: args, id: Math.floor(Math.random() * 1000000), }; await rpcInterface.write(Buffer.from(JSON.stringify(request))); const response = await rpcInterface.read(); const result = JSON.parse(response.toString()); if (result.error) { throw new Error(result.error.message); } return result.result; }; } async disconnect(): Promise<void> { if (this.socket?.readyState === WebSocket.OPEN) { this.socket.close(); this.socket = null; } if (this.process) { this.process.kill(); this.process = null; } } }