UNPKG

@tiflux/mcp

Version:

TiFlux MCP Server - Model Context Protocol integration for Claude Code and other AI clients

303 lines (255 loc) 8.68 kB
/** * Lambda MCP Handler - Logica principal HTTP para MCP sobre Lambda * * Este handler processa requisicoes HTTP do Lambda Function URL e * as traduz para o protocolo MCP usando StreamableHTTPServerTransport. * * Fluxo: * 1. Recebe evento HTTP Lambda * 2. Parse do evento (extrai API key, body, headers) * 3. Valida API key (401 se ausente) * 4. Cria servidor MCP isolado com API key do cliente * 5. Processa requisicao MCP * 6. Retorna resposta HTTP */ const EventParser = require('./EventParser'); const ResponseBuilder = require('./ResponseBuilder'); const ServerFactory = require('./ServerFactory'); class MCPHandler { /** * Processa requisicao HTTP do Lambda Function URL * @param {Object} event - Evento Lambda Function URL * @returns {Object} - Resposta HTTP Lambda */ static async handle(event) { let parsedEvent = null; let sessionId = null; try { // 1. Extrair path primeiro (antes de validar API key) const path = event.requestContext?.http?.path || event.path || event.rawPath || '/'; const method = event.requestContext?.http?.method || event.httpMethod || 'GET'; // 2. Roteamento por path (health e 404 nao precisam de API key) if (path === '/health' || path === '/health/') { return this.handleHealth(); } // Verificar se path é válido antes de exigir API key if (path !== '/mcp' && path !== '/mcp/' && path !== '/') { // Path desconhecido - retornar 404 sem exigir API key return ResponseBuilder.notFound(`Path nao encontrado: ${path}`, null); } // 3. Processar endpoint /mcp // 3.1. Tratar preflight CORS (sem API key) if (method === 'OPTIONS') { return ResponseBuilder.corsPreFlight(); } // 3.2. GET retorna info do servidor (sem API key - para MCP client discovery) if (method === 'GET') { console.log('[MCPHandler] GET /mcp - Server info request', { method, path, timestamp: new Date().toISOString() }); return ResponseBuilder.serverInfo(); } // 4. Parse completo do evento (valida API key) - apenas para POST parsedEvent = EventParser.parse(event); sessionId = parsedEvent.sessionId; const { apiKey, body } = parsedEvent; console.log('[MCPHandler] Requisicao recebida', { sessionId, method, path, apiKeyHash: EventParser.getApiKeyHash(apiKey), timestamp: new Date().toISOString() }); // 5. Processar operacoes MCP // 4.3. Apenas POST e permitido para operacoes MCP if (method !== 'POST') { return ResponseBuilder.badRequest( `Metodo ${method} nao permitido. Use POST para /mcp ou GET para info`, sessionId ); } // 4.3. Validar que body e uma requisicao MCP valida if (!EventParser.isValidMCPRequest(parsedEvent)) { return ResponseBuilder.badRequest( 'Body deve ser uma requisicao JSON-RPC 2.0 valida', sessionId ); } // 5. Processar requisicao MCP return await this.handleMCPRequest(apiKey, body, sessionId); } catch (error) { // Erro de parse ou validacao if (error.message.includes('x-tiflux-api-key')) { return ResponseBuilder.unauthorized(error.message, sessionId); } console.error('[MCPHandler] Erro ao processar requisicao', { sessionId, error: error.message, stack: error.stack }); return ResponseBuilder.internalError( 'Erro ao processar requisicao', error, sessionId ); } } /** * Processa requisicao MCP (tools/list ou tools/call) * @param {string} apiKey - API key do cliente * @param {Object} body - Body JSON-RPC * @param {string} sessionId - Session ID * @returns {Object} - Resposta HTTP Lambda */ static async handleMCPRequest(apiKey, body, sessionId) { try { const { method, id } = body; const isNotification = id === undefined; console.log('[MCPHandler] Processando requisicao MCP', { sessionId, mcpMethod: method, mcpId: id, isNotification, timestamp: new Date().toISOString() }); // 1. Criar servidor MCP isolado para este request (stateless) const { server } = ServerFactory.createServer(apiKey, sessionId); // 2. Processar requisicao MCP let result; switch (method) { case 'tools/list': result = await this.handleToolsList(server); break; case 'tools/call': result = await this.handleToolsCall(server, body); break; case 'initialize': result = await this.handleInitialize(server); break; case 'notifications/initialized': // Notificacao do cliente informando que inicializacao completa // Nao precisa retornar nada (notificacoes nao tem resposta) console.log('[MCPHandler] Cliente inicializado', { sessionId }); return ResponseBuilder.noContent(sessionId); default: // Se for notificacao desconhecida, ignorar (sem resposta) if (isNotification) { console.log('[MCPHandler] Notificacao desconhecida ignorada', { sessionId, method }); return ResponseBuilder.noContent(sessionId); } // Se for request desconhecido, retornar erro return ResponseBuilder.mcpError( `Metodo MCP desconhecido: ${method}`, -32601, // Method not found id, sessionId ); } console.log('[MCPHandler] Requisicao MCP processada com sucesso', { sessionId, mcpMethod: method, mcpId: id, timestamp: new Date().toISOString() }); // 3. Retornar resposta MCP (apenas para requests, nao para notificacoes) if (isNotification) { return ResponseBuilder.noContent(sessionId); } return ResponseBuilder.mcpResponse(result, id, sessionId); } catch (error) { console.error('[MCPHandler] Erro ao processar requisicao MCP', { sessionId, error: error.message, stack: error.stack }); // Notificacoes nao retornam erro if (body.id === undefined) { return ResponseBuilder.noContent(sessionId); } return ResponseBuilder.mcpError( error.message, -32603, // Internal error body.id, sessionId ); } } /** * Handler para tools/list * @param {Object} server - Instancia do servidor MCP * @returns {Object} - Lista de tools */ static async handleToolsList(server) { // Disparar request tools/list no servidor MCP const request = { method: 'tools/list', params: {} }; // Invocar handler do servidor const handlers = server._requestHandlers; const listHandler = handlers.get('tools/list'); if (!listHandler) { throw new Error('Handler tools/list nao encontrado no servidor'); } const result = await listHandler(request); return result; } /** * Handler para tools/call * @param {Object} server - Instancia do servidor MCP * @param {Object} body - Body JSON-RPC completo * @returns {Object} - Resultado da tool */ static async handleToolsCall(server, body) { const { params } = body; if (!params || !params.name) { throw new Error('Parametro "name" obrigatorio para tools/call'); } // Disparar request tools/call no servidor MCP const request = { method: 'tools/call', params: { name: params.name, arguments: params.arguments || {} } }; // Invocar handler do servidor const handlers = server._requestHandlers; const callHandler = handlers.get('tools/call'); if (!callHandler) { throw new Error('Handler tools/call nao encontrado no servidor'); } const result = await callHandler(request); return result; } /** * Handler para initialize (handshake MCP) * @param {Object} server - Instancia do servidor MCP * @returns {Object} - Info de inicializacao */ static async handleInitialize(server) { return { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'tiflux-mcp-lambda', version: '2.0.0' } }; } /** * Handler para /health * @returns {Object} - Resposta HTTP de health check */ static handleHealth() { return ResponseBuilder.health(); } } module.exports = MCPHandler;