/** * GERENCIADOR DE MÚLTIPLOS SERVIDORES MCP * * Este módulo gerencia conexões com múltiplos servidores MCP * * Por que criar um gerenciador? * - Centraliza a lógica de conexão * - Facilita adicionar novos MCPs * - Mantém o código organizado (SOLID) * - Permite reutilizar em diferentes partes da API * * Como funciona? * 1. Registra configurações de cada MCP * 2. Inicializa conexões sob demanda * 3. Mantém pool de clientes conectados * 4. Fornece interface única para chamar ferramentas */ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; /** * Interface para configuração de um servidor MCP * * Por que criar essa interface? * - Define claramente o que é necessário para cada MCP * - Facilita validação e documentação * - TypeScript garante que não esquecemos nenhum campo */ interface ConfiguracaoMCP { id: string; // Identificador único (ex: 'academia-v1') nome: string; // Nome amigável (ex: 'Academia PT-BR') tipo: 'stdio' | 'sse'; // Tipo de transporte url?: string; // URL para conexão SSE (obrigatório se tipo='sse') versao: string; // Versão do servidor caminhoScript?: string; // Caminho para o script do servidor (obrigatório se tipo='stdio') idioma: 'pt-br' | 'en'; // Idioma do servidor dominios: string[]; // Domínios que o MCP conhece (ex: ['exercicios', 'treinos']) ferramentas?: string[]; // Lista de ferramentas disponíveis (opcional) } /** * Interface para um cliente MCP ativo */ interface ClienteMCPAtivo { config: ConfiguracaoMCP; client: Client; transport: StdioClientTransport | StreamableHTTPClientTransport; conectadoEm: Date; ferramentasDisponiveis: any[]; // Armazena a definição completa das ferramentas } /** * Classe que gerencia múltiplos servidores MCP * * Por que usar uma classe? * - Encapsula estado (clientes conectados) * - Métodos organizados e reutilizáveis * - Facilita testes unitários * - Segue o princípio de Responsabilidade Única */ class GerenciadorMCP { private configuracoes: Map = new Map(); private clientes: Map = new Map(); /** * Registra um novo servidor MCP * * Por que registrar antes de conectar? * - Permite inicialização lazy (só conecta quando necessário) * - Facilita descoberta (listar MCPs disponíveis) * - Validação prévia de configuração */ registrarMCP(config: ConfiguracaoMCP): void { if (this.configuracoes.has(config.id)) { console.warn(`[MCP-MANAGER] MCP ${config.id} já está registrado, sobrescrevendo...`); } this.configuracoes.set(config.id, config); console.log(`[MCP-MANAGER] MCP registrado: ${config.nome} (${config.id})`); } /** * Conecta a um servidor MCP específico * * Por que conexão sob demanda? * - Economiza recursos (não conecta MCPs não usados) * - Mais rápido na inicialização * - Falha em um MCP não impede os outros */ async conectarMCP(id: string): Promise { // Se já está conectado, não faz nada if (this.clientes.has(id)) { console.log(`[MCP-MANAGER] MCP ${id} já está conectado`); return; } const config = this.configuracoes.get(id); if (!config) { throw new Error(`MCP ${id} não está registrado`); } try { console.log(`[MCP-MANAGER] Conectando ao MCP: ${config.nome}...`); let transport; if (config.tipo === 'sse') { if (!config.url) { throw new Error(`MCP ${id} configurado como SSE mas sem URL`); } console.log(`[MCP-MANAGER] Inicializando transporte SSE: ${config.url}`); transport = new StreamableHTTPClientTransport(new URL(config.url)); } else { if (!config.caminhoScript) { throw new Error(`MCP ${id} configurado como STDIO mas sem caminhoScript`); } // Cria o transporte stdio transport = new StdioClientTransport({ command: "bun", args: ["run", config.caminhoScript], }); } // Cria o cliente MCP v2.0 const client = new Client( { name: `api-client-${id}`, version: "1.0.0" }, { capabilities: {} } ); // Conecta o transporte await client.connect(transport); console.log(`[MCP-MANAGER] Transporte conectado para ${id} (Streamable HTTP v2024-11-05)`); // Lista ferramentas disponíveis let ferramentasDisponiveis: any[] = []; try { const result = await client.listTools(); ferramentasDisponiveis = result.tools; console.log(`[MCP-MANAGER] Ferramentas carregadas de ${id}: ${ferramentasDisponiveis.length} ferramentas`); ferramentasDisponiveis.slice(0, 3).forEach((tool: any) => { console.log(`[MCP-MANAGER] - ${tool.name}`); }); } catch (erro) { console.warn(`[MCP-MANAGER] Não foi possível listar ferramentas de ${id}`, erro); } // Salva o cliente ativo this.clientes.set(id, { config, client, transport, conectadoEm: new Date(), ferramentasDisponiveis }); console.log(`[MCP-MANAGER] MCP ${config.nome} conectado com sucesso`); } catch (erro) { console.error(`[MCP-MANAGER] Erro ao conectar MCP ${id}:`, erro); throw erro; } } /** * Conecta a todos os MCPs registrados * * Útil para pré-conectar na inicialização da API * Agora com conformidade MCP v2.0 (protocolo 2024-11-05) */ async conectarTodos(): Promise { const ids = Array.from(this.configuracoes.keys()); console.log(`[MCP-MANAGER] Conectando a ${ids.length} MCPs com protocolo v2024-11-05...`); // Conecta em paralelo para ser mais rápido // Por que Promise.allSettled? Para não interromper se um falhar const resultados = await Promise.allSettled( ids.map(id => this.conectarMCP(id)) ); const sucessos = resultados.filter(r => r.status === 'fulfilled').length; const falhas = resultados.filter(r => r.status === 'rejected').length; console.log(`[MCP-MANAGER] ✓ Conectados: ${sucessos} | ✗ Falhas: ${falhas}`); if (sucessos > 0) { console.log(`[MCP-MANAGER] MCPs ativos:`); this.listarMCPsConectados().forEach((config: ConfiguracaoMCP) => { console.log(`[MCP-MANAGER] - ${config.nome} (${config.tipo})`); }); } } /** * Chama uma ferramenta em um MCP específico * * Por que especificar o MCP? * - Permite controlar qual servidor usar * - Evita ambiguidade se dois MCPs tiverem ferramentas com mesmo nome * - Mais explícito e fácil de debugar */ async chamarFerramenta( mcpId: string, nomeFerramenta: string, argumentos: Record ): Promise { // Garante que está conectado if (!this.clientes.has(mcpId)) { await this.conectarMCP(mcpId); } const clienteAtivo = this.clientes.get(mcpId); if (!clienteAtivo) { throw new Error(`MCP ${mcpId} não está conectado`); } console.log(`[MCP-MANAGER] Chamando ${nomeFerramenta} em ${mcpId}`); console.log(`[MCP-MANAGER] Argumentos:`, JSON.stringify(argumentos).substring(0, 100)); try { const resultado = await clienteAtivo.client.callTool({ name: nomeFerramenta, arguments: argumentos, }); // Valida resposta v2.0 if (!resultado) { console.warn(`[MCP-MANAGER] Resultado vazio para ${nomeFerramenta}`); return ''; } // Processa o resultado para retornar string (compatibilidade) if (!resultado.content || !Array.isArray(resultado.content) || resultado.content.length === 0) { console.warn(`[MCP-MANAGER] ${nomeFerramenta} retornou content vazio`); return ''; } // Extrai texto do array de conteúdo (v2.0) const conteudo = (resultado.content as any[]) .filter((item: any) => item && item.type === 'text') .map((item: any) => item.text || '') .join('\n'); if (!conteudo) { console.warn(`[MCP-MANAGER] Nenhum conteúdo text extraído de ${nomeFerramenta}`); } console.log(`[MCP-MANAGER] ✓ Resposta recebida (${conteudo.length} caracteres)`); return conteudo; } catch (erro) { console.error(`[MCP-MANAGER] Erro ao chamar ${nomeFerramenta}:`, erro); throw erro; } } /** * Retorna todas as ferramentas de todos os MCPs conectados */ obterTodasFerramentas(): any[] { const todas: any[] = []; for (const cliente of this.clientes.values()) { todas.push(...cliente.ferramentasDisponiveis); } return todas; } /** * Busca e chama uma ferramenta pelo nome em qualquer MCP conectado */ async chamarFerramentaGlobal(nome: string, args: any): Promise { // Encontra qual MCP tem essa ferramenta let mcpIdAlvo: string | null = null; for (const [id, cliente] of this.clientes.entries()) { const temFerramenta = cliente.ferramentasDisponiveis.some(t => t.name === nome); if (temFerramenta) { mcpIdAlvo = id; break; } } if (!mcpIdAlvo) { throw new Error(`Ferramenta '${nome}' não encontrada em nenhum MCP conectado.`); } return this.chamarFerramenta(mcpIdAlvo, nome, args); } /** * Busca automaticamente qual MCP usar baseado em critérios * * Esta é a mágica que escolhe o MCP certo automaticamente! * * Como funciona? * 1. Verifica idioma da mensagem * 2. Verifica domínios (exercícios, nutrição, etc) * 3. Retorna o MCP mais adequado */ selecionarMCP( mensagem: string, criterios?: { idioma?: 'pt-br' | 'en'; dominio?: string; } ): string | null { const mensagemLower = mensagem.toLowerCase(); console.log(`[MCP-MANAGER] Selecionando MCP para mensagem: "${mensagemLower}"`); // Detecta idioma se não foi especificado const idiomaDetectado = criterios?.idioma || this.detectarIdioma(mensagem); console.log(`[MCP-MANAGER] Selecionando MCP para idioma: ${idiomaDetectado}`); // Filtra MCPs compatíveis const mcpsCompativeis = Array.from(this.configuracoes.values()).filter(config => { // Verifica idioma if (config.idioma !== idiomaDetectado) { return false; } // Verifica domínio se especificado if (criterios?.dominio) { return config.dominios.includes(criterios.dominio); } return true; }); if (mcpsCompativeis.length === 0) { console.warn(`[MCP-MANAGER] Nenhum MCP compatível encontrado`); return null; } // Por enquanto, retorna o primeiro compatível // Pode evoluir para scoring baseado em relevância const selecionado = mcpsCompativeis[0]; if (selecionado) { console.log(`[MCP-MANAGER] MCP selecionado: ${selecionado.nome}`); return selecionado.id; } return null; } /** * Detecta o idioma de uma mensagem * * Por que detectar? * - Permite escolher o MCP certo automaticamente * - Usuário não precisa especificar * - Melhora a experiência * * Como funciona? * - Procura por palavras comuns em cada idioma * - Simples mas eficaz para nosso caso * - Pode evoluir para usar biblioteca de detecção de idioma */ private detectarIdioma(mensagem: string): 'pt-br' | 'en' { const mensagemLower = mensagem.toLowerCase(); // Palavras comuns em português const palavrasPT = [ 'o que', 'como', 'quais', 'qual', 'onde', 'quando', 'meu', 'meus', 'minha', 'minhas', 'exercício', 'exercícios', 'treino', 'lista', 'listar', 'mostre', 'mostrar' ]; // Palavras comuns em inglês const palavrasEN = [ 'what', 'how', 'which', 'where', 'when', 'my', 'mine', 'exercise', 'exercises', 'workout', 'list', 'show', 'display' ]; const contagemPT = palavrasPT.filter(p => mensagemLower.includes(p)).length; const contagemEN = palavrasEN.filter(p => mensagemLower.includes(p)).length; // Padrão é português return contagemEN > contagemPT ? 'en' : 'pt-br'; } /** * Lista todos os MCPs disponíveis */ listarMCPs(): ConfiguracaoMCP[] { return Array.from(this.configuracoes.values()); } /** * Lista apenas MCPs conectados */ listarMCPsConectados(): ConfiguracaoMCP[] { return Array.from(this.clientes.values()).map(c => c.config); } /** * Verifica se um MCP específico está conectado */ estaConectado(id: string): boolean { return this.clientes.has(id); } /** * Desconecta um MCP específico */ async desconectarMCP(id: string): Promise { const clienteAtivo = this.clientes.get(id); if (!clienteAtivo) { return; } try { await clienteAtivo.client.close(); this.clientes.delete(id); console.log(`[MCP-MANAGER] MCP ${id} desconectado`); } catch (erro) { console.error(`[MCP-MANAGER] Erro ao desconectar ${id}:`, erro); } } /** * Desconecta todos os MCPs * * Deve ser chamado quando a aplicação for encerrar * Implementa graceful shutdown conforme MCP v2.0 */ async desconectarTodos(): Promise { const ids = Array.from(this.clientes.keys()); if (ids.length === 0) { console.log(`[MCP-MANAGER] Nenhum MCP conectado para desconectar`); return; } console.log(`[MCP-MANAGER] Desconectando ${ids.length} MCPs...`); await Promise.allSettled( ids.map(id => this.desconectarMCP(id)) ); console.log(`[MCP-MANAGER] ✓ Todos os MCPs desconectados com sucesso`); } } // Singleton do gerenciador // Por que singleton? Para ter uma única instância compartilhada em toda a aplicação const gerenciadorMCP = new GerenciadorMCP(); /** * Função de inicialização que registra todos os MCPs disponíveis * * Esta função deve ser chamada na inicialização da API * * Conformidade: MCP v2.0 (protocolo 2024-11-05 com Streamable HTTP) */ async function inicializarMCPs(): Promise { console.log('[MCP-MANAGER] ════════════════════════════════════════'); console.log('[MCP-MANAGER] Inicializando MCPs - Protocolo v2024-11-05'); console.log('[MCP-MANAGER] ════════════════════════════════════════'); // Registra MCP v1 (Português) via Stdio Local // gerenciadorMCP.registrarMCP({ // id: 'academia-v1', // nome: 'Academia MCP v1 (PT-BR) (STDIO)', // tipo: 'stdio', // versao: '1.0.0', // // Caminho relativo ao api-chatbot-academia // caminhoScript: join(__dirname, '../../servidores-mcp/base-de-dados-academia/via-stdio.ts'), // idioma: 'pt-br', // dominios: ['exercicios', 'treinos', 'musculacao'], // ferramentas: [ // 'buscar_exercicios_por_grupo', // 'listar_grupos_musculares', // 'buscar_exercicio_por_nome', // 'listar_todos_exercicios', // 'obter_detalhes_exercicio' // ] // }); // Exemplo de como registrar um MCP via SSE (para futura integração remota) gerenciadorMCP.registrarMCP({ id: 'academia-remota-sse', nome: 'Academia MCP Remoto (SSE)', tipo: 'sse', url: process.env.MCP_URL_ACADEMIA || 'http://localhost:3401/mcp', // URL do endpoint SSE (DEVE incluir /mcp) versao: '1.0.0', idioma: 'pt-br', dominios: ['exercicios', 'treinos', 'musculacao'], ferramentas: [ 'buscar_exercicios_por_grupo', 'listar_grupos_musculares', 'buscar_exercicio_por_nome', 'listar_todos_exercicios', 'obter_detalhes_exercicio' ] }); // Conecta a todos try { await gerenciadorMCP.conectarTodos(); console.log('[MCP-MANAGER] ✓ Inicialização concluída com sucesso'); console.log('[MCP-MANAGER] Protocolo: v2024-11-05 (Streamable HTTP)'); } catch (erro) { console.error('[MCP-MANAGER] ✗ Erro na inicialização:', erro); console.log('[MCP-MANAGER] API continuará funcionando sem MCPs'); // Não lança erro para não impedir a API de iniciar } } export { gerenciadorMCP, inicializarMCPs, type ConfiguracaoMCP };