Skip to main content
Glama
index.ts8.7 kB
#!/usr/bin/env node /** * MCP Bridge para HandsAI * Conecta MCP clients con HandsAI Spring Boot server via HTTP */ import * as readline from 'readline'; interface McpRequest { jsonrpc: string; id?: number | string; method: string; params?: any; } interface McpResponse { jsonrpc: string; id?: number | string; result?: any; error?: { code: number; message: string; data?: any; }; } class HandsAIMcpBridge { private readonly handsaiBaseUrl: string; private readonly rl: readline.Interface; constructor(handsaiUrl = 'http://localhost:8080') { this.handsaiBaseUrl = handsaiUrl; this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); } async start(): Promise<void> { this.rl.on('line', async (line) => { try { const request: McpRequest = JSON.parse(line.trim()); const response = await this.handleMcpRequest(request); if (response) { console.log(JSON.stringify(response)); } } catch (error) { // SIEMPRE incluir el ID de la request original, incluso en errores de parse const errorResponse: McpResponse = { jsonrpc: '2.0', id: null, // Para errores de parse, se usa null según JSON-RPC 2.0 error: { code: -32700, message: 'Parse error', data: error instanceof Error ? error.message : String(error) } }; console.log(JSON.stringify(errorResponse)); } }); this.rl.on('close', () => { process.exit(0); }); } private async handleMcpRequest(request: McpRequest): Promise<McpResponse | null> { // CRÍTICO: Validar que el request tenga la estructura mínima if (!request.jsonrpc || request.jsonrpc !== '2.0') { return { jsonrpc: '2.0', id: request.id || null, error: { code: -32600, message: 'Invalid Request: jsonrpc must be "2.0"' } }; } if (!request.method) { return { jsonrpc: '2.0', id: request.id || null, error: { code: -32600, message: 'Invalid Request: method is required' } }; } switch (request.method) { case 'initialize': return this.handleInitialize(request); case 'tools/list': return await this.handleListTools(request); case 'tools/call': return await this.handleCallTool(request); case 'notifications/initialized': // Este es un notification, no requiere respuesta return null; default: return { jsonrpc: '2.0', id: request.id || null, error: { code: -32601, message: `Method not found: ${request.method}` } }; } } private handleInitialize(request: McpRequest): McpResponse { return { jsonrpc: '2.0', id: request.id, // Usar el ID original de la request result: { protocolVersion: '2024-11-05', capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'HandsAI MCP Bridge', version: '1.0.0' } } }; } private async handleListTools(request: McpRequest): Promise<McpResponse> { try { const response = await fetch(`${this.handsaiBaseUrl}/mcp/tools/list`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); // Validar que la respuesta tenga la estructura esperada if (!data || typeof data !== 'object') { throw new Error('Invalid response format from HandsAI server'); } // Manejar diferentes formatos de respuesta del servidor HandsAI let tools = []; if (data.result && Array.isArray(data.result.tools)) { tools = data.result.tools; } else if (Array.isArray(data.tools)) { tools = data.tools; } else if (Array.isArray(data)) { tools = data; } // Convertir a formato MCP estándar const mcpTools = tools.map((tool: any) => ({ name: tool.name || 'unknown', description: tool.description || 'No description available', inputSchema: tool.inputSchema || { type: 'object', properties: {}, required: [] } })); return { jsonrpc: '2.0', id: request.id, // Usar el ID original result: { tools: mcpTools } }; } catch (error) { return { jsonrpc: '2.0', id: request.id, // Usar el ID original error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : String(error) } }; } } private async handleCallTool(request: McpRequest): Promise<McpResponse> { // Validar parámetros requeridos if (!request.params || !request.params.name) { return { jsonrpc: '2.0', id: request.id, // Usar el ID original error: { code: -32602, message: 'Invalid params: tool name is required' } }; } const toolName = request.params.name; const arguments_ = request.params.arguments || {}; try { const mcpCallRequest = { jsonrpc: '2.0', id: `internal-${Date.now()}`, // ID único para evitar conflictos method: 'tools/call', params: { name: toolName, arguments: arguments_ } }; const response = await fetch(`${this.handsaiBaseUrl}/mcp/tools/call`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(mcpCallRequest) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); // Validar respuesta del servidor if (!data || typeof data !== 'object') { throw new Error('Invalid response format from HandsAI server'); } // Manejar diferentes formatos de respuesta let result; if (data.result) { result = data.result; } else if (data.content) { result = { content: data.content }; } else { result = { content: [{ type: 'text', text: JSON.stringify(data) }] }; } return { jsonrpc: '2.0', id: request.id, // CRÍTICO: Usar el ID original de la request result: result }; } catch (error) { return { jsonrpc: '2.0', id: request.id, // Usar el ID original error: { code: -32603, message: 'Execution error', data: error instanceof Error ? error.message : String(error) } }; } } } // Inicializar bridge const bridge = new HandsAIMcpBridge(); bridge.start().catch((error) => { console.error('Bridge startup error:', error); process.exit(1); });

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/Vrivaans/handsai-bridge'

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