| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- /**
- * 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<string, ConfiguracaoMCP> = new Map();
- private clientes: Map<string, ClienteMCPAtivo> = 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<void> {
- // 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<void> {
- 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<string, any>
- ): Promise<string> {
- // 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<string> {
- // 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<void> {
- 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<void> {
- 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<void> {
- 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
- };
|