mcp-manager.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. /**
  2. * GERENCIADOR DE MÚLTIPLOS SERVIDORES MCP
  3. *
  4. * Este módulo gerencia conexões com múltiplos servidores MCP
  5. *
  6. * Por que criar um gerenciador?
  7. * - Centraliza a lógica de conexão
  8. * - Facilita adicionar novos MCPs
  9. * - Mantém o código organizado (SOLID)
  10. * - Permite reutilizar em diferentes partes da API
  11. *
  12. * Como funciona?
  13. * 1. Registra configurações de cada MCP
  14. * 2. Inicializa conexões sob demanda
  15. * 3. Mantém pool de clientes conectados
  16. * 4. Fornece interface única para chamar ferramentas
  17. */
  18. import { Client } from "@modelcontextprotocol/sdk/client/index.js";
  19. import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
  20. import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
  21. /**
  22. * Interface para configuração de um servidor MCP
  23. *
  24. * Por que criar essa interface?
  25. * - Define claramente o que é necessário para cada MCP
  26. * - Facilita validação e documentação
  27. * - TypeScript garante que não esquecemos nenhum campo
  28. */
  29. interface ConfiguracaoMCP {
  30. id: string; // Identificador único (ex: 'academia-v1')
  31. nome: string; // Nome amigável (ex: 'Academia PT-BR')
  32. tipo: 'stdio' | 'sse'; // Tipo de transporte
  33. url?: string; // URL para conexão SSE (obrigatório se tipo='sse')
  34. versao: string; // Versão do servidor
  35. caminhoScript?: string; // Caminho para o script do servidor (obrigatório se tipo='stdio')
  36. idioma: 'pt-br' | 'en'; // Idioma do servidor
  37. dominios: string[]; // Domínios que o MCP conhece (ex: ['exercicios', 'treinos'])
  38. ferramentas?: string[]; // Lista de ferramentas disponíveis (opcional)
  39. }
  40. /**
  41. * Interface para um cliente MCP ativo
  42. */
  43. interface ClienteMCPAtivo {
  44. config: ConfiguracaoMCP;
  45. client: Client;
  46. transport: StdioClientTransport | StreamableHTTPClientTransport;
  47. conectadoEm: Date;
  48. ferramentasDisponiveis: any[]; // Armazena a definição completa das ferramentas
  49. }
  50. /**
  51. * Classe que gerencia múltiplos servidores MCP
  52. *
  53. * Por que usar uma classe?
  54. * - Encapsula estado (clientes conectados)
  55. * - Métodos organizados e reutilizáveis
  56. * - Facilita testes unitários
  57. * - Segue o princípio de Responsabilidade Única
  58. */
  59. class GerenciadorMCP {
  60. private configuracoes: Map<string, ConfiguracaoMCP> = new Map();
  61. private clientes: Map<string, ClienteMCPAtivo> = new Map();
  62. /**
  63. * Registra um novo servidor MCP
  64. *
  65. * Por que registrar antes de conectar?
  66. * - Permite inicialização lazy (só conecta quando necessário)
  67. * - Facilita descoberta (listar MCPs disponíveis)
  68. * - Validação prévia de configuração
  69. */
  70. registrarMCP(config: ConfiguracaoMCP): void {
  71. if (this.configuracoes.has(config.id)) {
  72. console.warn(`[MCP-MANAGER] MCP ${config.id} já está registrado, sobrescrevendo...`);
  73. }
  74. this.configuracoes.set(config.id, config);
  75. console.log(`[MCP-MANAGER] MCP registrado: ${config.nome} (${config.id})`);
  76. }
  77. /**
  78. * Conecta a um servidor MCP específico
  79. *
  80. * Por que conexão sob demanda?
  81. * - Economiza recursos (não conecta MCPs não usados)
  82. * - Mais rápido na inicialização
  83. * - Falha em um MCP não impede os outros
  84. */
  85. async conectarMCP(id: string): Promise<void> {
  86. // Se já está conectado, não faz nada
  87. if (this.clientes.has(id)) {
  88. console.log(`[MCP-MANAGER] MCP ${id} já está conectado`);
  89. return;
  90. }
  91. const config = this.configuracoes.get(id);
  92. if (!config) {
  93. throw new Error(`MCP ${id} não está registrado`);
  94. }
  95. try {
  96. console.log(`[MCP-MANAGER] Conectando ao MCP: ${config.nome}...`);
  97. let transport;
  98. if (config.tipo === 'sse') {
  99. if (!config.url) {
  100. throw new Error(`MCP ${id} configurado como SSE mas sem URL`);
  101. }
  102. console.log(`[MCP-MANAGER] Inicializando transporte SSE: ${config.url}`);
  103. transport = new StreamableHTTPClientTransport(new URL(config.url));
  104. } else {
  105. if (!config.caminhoScript) {
  106. throw new Error(`MCP ${id} configurado como STDIO mas sem caminhoScript`);
  107. }
  108. // Cria o transporte stdio
  109. transport = new StdioClientTransport({
  110. command: "bun",
  111. args: ["run", config.caminhoScript],
  112. });
  113. }
  114. // Cria o cliente MCP v2.0
  115. const client = new Client(
  116. { name: `api-client-${id}`, version: "1.0.0" },
  117. { capabilities: {} }
  118. );
  119. // Conecta o transporte
  120. await client.connect(transport);
  121. console.log(`[MCP-MANAGER] Transporte conectado para ${id} (Streamable HTTP v2024-11-05)`);
  122. // Lista ferramentas disponíveis
  123. let ferramentasDisponiveis: any[] = [];
  124. try {
  125. const result = await client.listTools();
  126. ferramentasDisponiveis = result.tools;
  127. console.log(`[MCP-MANAGER] Ferramentas carregadas de ${id}: ${ferramentasDisponiveis.length} ferramentas`);
  128. ferramentasDisponiveis.slice(0, 3).forEach((tool: any) => {
  129. console.log(`[MCP-MANAGER] - ${tool.name}`);
  130. });
  131. } catch (erro) {
  132. console.warn(`[MCP-MANAGER] Não foi possível listar ferramentas de ${id}`, erro);
  133. }
  134. // Salva o cliente ativo
  135. this.clientes.set(id, {
  136. config,
  137. client,
  138. transport,
  139. conectadoEm: new Date(),
  140. ferramentasDisponiveis
  141. });
  142. console.log(`[MCP-MANAGER] MCP ${config.nome} conectado com sucesso`);
  143. } catch (erro) {
  144. console.error(`[MCP-MANAGER] Erro ao conectar MCP ${id}:`, erro);
  145. throw erro;
  146. }
  147. }
  148. /**
  149. * Conecta a todos os MCPs registrados
  150. *
  151. * Útil para pré-conectar na inicialização da API
  152. * Agora com conformidade MCP v2.0 (protocolo 2024-11-05)
  153. */
  154. async conectarTodos(): Promise<void> {
  155. const ids = Array.from(this.configuracoes.keys());
  156. console.log(`[MCP-MANAGER] Conectando a ${ids.length} MCPs com protocolo v2024-11-05...`);
  157. // Conecta em paralelo para ser mais rápido
  158. // Por que Promise.allSettled? Para não interromper se um falhar
  159. const resultados = await Promise.allSettled(
  160. ids.map(id => this.conectarMCP(id))
  161. );
  162. const sucessos = resultados.filter(r => r.status === 'fulfilled').length;
  163. const falhas = resultados.filter(r => r.status === 'rejected').length;
  164. console.log(`[MCP-MANAGER] ✓ Conectados: ${sucessos} | ✗ Falhas: ${falhas}`);
  165. if (sucessos > 0) {
  166. console.log(`[MCP-MANAGER] MCPs ativos:`);
  167. this.listarMCPsConectados().forEach((config: ConfiguracaoMCP) => {
  168. console.log(`[MCP-MANAGER] - ${config.nome} (${config.tipo})`);
  169. });
  170. }
  171. }
  172. /**
  173. * Chama uma ferramenta em um MCP específico
  174. *
  175. * Por que especificar o MCP?
  176. * - Permite controlar qual servidor usar
  177. * - Evita ambiguidade se dois MCPs tiverem ferramentas com mesmo nome
  178. * - Mais explícito e fácil de debugar
  179. */
  180. async chamarFerramenta(
  181. mcpId: string,
  182. nomeFerramenta: string,
  183. argumentos: Record<string, any>
  184. ): Promise<string> {
  185. // Garante que está conectado
  186. if (!this.clientes.has(mcpId)) {
  187. await this.conectarMCP(mcpId);
  188. }
  189. const clienteAtivo = this.clientes.get(mcpId);
  190. if (!clienteAtivo) {
  191. throw new Error(`MCP ${mcpId} não está conectado`);
  192. }
  193. console.log(`[MCP-MANAGER] Chamando ${nomeFerramenta} em ${mcpId}`);
  194. console.log(`[MCP-MANAGER] Argumentos:`, JSON.stringify(argumentos).substring(0, 100));
  195. try {
  196. const resultado = await clienteAtivo.client.callTool({
  197. name: nomeFerramenta,
  198. arguments: argumentos,
  199. });
  200. // Valida resposta v2.0
  201. if (!resultado) {
  202. console.warn(`[MCP-MANAGER] Resultado vazio para ${nomeFerramenta}`);
  203. return '';
  204. }
  205. // Processa o resultado para retornar string (compatibilidade)
  206. if (!resultado.content || !Array.isArray(resultado.content) || resultado.content.length === 0) {
  207. console.warn(`[MCP-MANAGER] ${nomeFerramenta} retornou content vazio`);
  208. return '';
  209. }
  210. // Extrai texto do array de conteúdo (v2.0)
  211. const conteudo = (resultado.content as any[])
  212. .filter((item: any) => item && item.type === 'text')
  213. .map((item: any) => item.text || '')
  214. .join('\n');
  215. if (!conteudo) {
  216. console.warn(`[MCP-MANAGER] Nenhum conteúdo text extraído de ${nomeFerramenta}`);
  217. }
  218. console.log(`[MCP-MANAGER] ✓ Resposta recebida (${conteudo.length} caracteres)`);
  219. return conteudo;
  220. } catch (erro) {
  221. console.error(`[MCP-MANAGER] Erro ao chamar ${nomeFerramenta}:`, erro);
  222. throw erro;
  223. }
  224. }
  225. /**
  226. * Retorna todas as ferramentas de todos os MCPs conectados
  227. */
  228. obterTodasFerramentas(): any[] {
  229. const todas: any[] = [];
  230. for (const cliente of this.clientes.values()) {
  231. todas.push(...cliente.ferramentasDisponiveis);
  232. }
  233. return todas;
  234. }
  235. /**
  236. * Busca e chama uma ferramenta pelo nome em qualquer MCP conectado
  237. */
  238. async chamarFerramentaGlobal(nome: string, args: any): Promise<string> {
  239. // Encontra qual MCP tem essa ferramenta
  240. let mcpIdAlvo: string | null = null;
  241. for (const [id, cliente] of this.clientes.entries()) {
  242. const temFerramenta = cliente.ferramentasDisponiveis.some(t => t.name === nome);
  243. if (temFerramenta) {
  244. mcpIdAlvo = id;
  245. break;
  246. }
  247. }
  248. if (!mcpIdAlvo) {
  249. throw new Error(`Ferramenta '${nome}' não encontrada em nenhum MCP conectado.`);
  250. }
  251. return this.chamarFerramenta(mcpIdAlvo, nome, args);
  252. }
  253. /**
  254. * Busca automaticamente qual MCP usar baseado em critérios
  255. *
  256. * Esta é a mágica que escolhe o MCP certo automaticamente!
  257. *
  258. * Como funciona?
  259. * 1. Verifica idioma da mensagem
  260. * 2. Verifica domínios (exercícios, nutrição, etc)
  261. * 3. Retorna o MCP mais adequado
  262. */
  263. selecionarMCP(
  264. mensagem: string,
  265. criterios?: {
  266. idioma?: 'pt-br' | 'en';
  267. dominio?: string;
  268. }
  269. ): string | null {
  270. const mensagemLower = mensagem.toLowerCase();
  271. console.log(`[MCP-MANAGER] Selecionando MCP para mensagem: "${mensagemLower}"`);
  272. // Detecta idioma se não foi especificado
  273. const idiomaDetectado = criterios?.idioma || this.detectarIdioma(mensagem);
  274. console.log(`[MCP-MANAGER] Selecionando MCP para idioma: ${idiomaDetectado}`);
  275. // Filtra MCPs compatíveis
  276. const mcpsCompativeis = Array.from(this.configuracoes.values()).filter(config => {
  277. // Verifica idioma
  278. if (config.idioma !== idiomaDetectado) {
  279. return false;
  280. }
  281. // Verifica domínio se especificado
  282. if (criterios?.dominio) {
  283. return config.dominios.includes(criterios.dominio);
  284. }
  285. return true;
  286. });
  287. if (mcpsCompativeis.length === 0) {
  288. console.warn(`[MCP-MANAGER] Nenhum MCP compatível encontrado`);
  289. return null;
  290. }
  291. // Por enquanto, retorna o primeiro compatível
  292. // Pode evoluir para scoring baseado em relevância
  293. const selecionado = mcpsCompativeis[0];
  294. if (selecionado) {
  295. console.log(`[MCP-MANAGER] MCP selecionado: ${selecionado.nome}`);
  296. return selecionado.id;
  297. }
  298. return null;
  299. }
  300. /**
  301. * Detecta o idioma de uma mensagem
  302. *
  303. * Por que detectar?
  304. * - Permite escolher o MCP certo automaticamente
  305. * - Usuário não precisa especificar
  306. * - Melhora a experiência
  307. *
  308. * Como funciona?
  309. * - Procura por palavras comuns em cada idioma
  310. * - Simples mas eficaz para nosso caso
  311. * - Pode evoluir para usar biblioteca de detecção de idioma
  312. */
  313. private detectarIdioma(mensagem: string): 'pt-br' | 'en' {
  314. const mensagemLower = mensagem.toLowerCase();
  315. // Palavras comuns em português
  316. const palavrasPT = [
  317. 'o que', 'como', 'quais', 'qual', 'onde', 'quando',
  318. 'meu', 'meus', 'minha', 'minhas',
  319. 'exercício', 'exercícios', 'treino',
  320. 'lista', 'listar', 'mostre', 'mostrar'
  321. ];
  322. // Palavras comuns em inglês
  323. const palavrasEN = [
  324. 'what', 'how', 'which', 'where', 'when',
  325. 'my', 'mine',
  326. 'exercise', 'exercises', 'workout',
  327. 'list', 'show', 'display'
  328. ];
  329. const contagemPT = palavrasPT.filter(p => mensagemLower.includes(p)).length;
  330. const contagemEN = palavrasEN.filter(p => mensagemLower.includes(p)).length;
  331. // Padrão é português
  332. return contagemEN > contagemPT ? 'en' : 'pt-br';
  333. }
  334. /**
  335. * Lista todos os MCPs disponíveis
  336. */
  337. listarMCPs(): ConfiguracaoMCP[] {
  338. return Array.from(this.configuracoes.values());
  339. }
  340. /**
  341. * Lista apenas MCPs conectados
  342. */
  343. listarMCPsConectados(): ConfiguracaoMCP[] {
  344. return Array.from(this.clientes.values()).map(c => c.config);
  345. }
  346. /**
  347. * Verifica se um MCP específico está conectado
  348. */
  349. estaConectado(id: string): boolean {
  350. return this.clientes.has(id);
  351. }
  352. /**
  353. * Desconecta um MCP específico
  354. */
  355. async desconectarMCP(id: string): Promise<void> {
  356. const clienteAtivo = this.clientes.get(id);
  357. if (!clienteAtivo) {
  358. return;
  359. }
  360. try {
  361. await clienteAtivo.client.close();
  362. this.clientes.delete(id);
  363. console.log(`[MCP-MANAGER] MCP ${id} desconectado`);
  364. } catch (erro) {
  365. console.error(`[MCP-MANAGER] Erro ao desconectar ${id}:`, erro);
  366. }
  367. }
  368. /**
  369. * Desconecta todos os MCPs
  370. *
  371. * Deve ser chamado quando a aplicação for encerrar
  372. * Implementa graceful shutdown conforme MCP v2.0
  373. */
  374. async desconectarTodos(): Promise<void> {
  375. const ids = Array.from(this.clientes.keys());
  376. if (ids.length === 0) {
  377. console.log(`[MCP-MANAGER] Nenhum MCP conectado para desconectar`);
  378. return;
  379. }
  380. console.log(`[MCP-MANAGER] Desconectando ${ids.length} MCPs...`);
  381. await Promise.allSettled(
  382. ids.map(id => this.desconectarMCP(id))
  383. );
  384. console.log(`[MCP-MANAGER] ✓ Todos os MCPs desconectados com sucesso`);
  385. }
  386. }
  387. // Singleton do gerenciador
  388. // Por que singleton? Para ter uma única instância compartilhada em toda a aplicação
  389. const gerenciadorMCP = new GerenciadorMCP();
  390. /**
  391. * Função de inicialização que registra todos os MCPs disponíveis
  392. *
  393. * Esta função deve ser chamada na inicialização da API
  394. *
  395. * Conformidade: MCP v2.0 (protocolo 2024-11-05 com Streamable HTTP)
  396. */
  397. async function inicializarMCPs(): Promise<void> {
  398. console.log('[MCP-MANAGER] ════════════════════════════════════════');
  399. console.log('[MCP-MANAGER] Inicializando MCPs - Protocolo v2024-11-05');
  400. console.log('[MCP-MANAGER] ════════════════════════════════════════');
  401. // Registra MCP v1 (Português) via Stdio Local
  402. // gerenciadorMCP.registrarMCP({
  403. // id: 'academia-v1',
  404. // nome: 'Academia MCP v1 (PT-BR) (STDIO)',
  405. // tipo: 'stdio',
  406. // versao: '1.0.0',
  407. // // Caminho relativo ao api-chatbot-academia
  408. // caminhoScript: join(__dirname, '../../servidores-mcp/base-de-dados-academia/via-stdio.ts'),
  409. // idioma: 'pt-br',
  410. // dominios: ['exercicios', 'treinos', 'musculacao'],
  411. // ferramentas: [
  412. // 'buscar_exercicios_por_grupo',
  413. // 'listar_grupos_musculares',
  414. // 'buscar_exercicio_por_nome',
  415. // 'listar_todos_exercicios',
  416. // 'obter_detalhes_exercicio'
  417. // ]
  418. // });
  419. // Exemplo de como registrar um MCP via SSE (para futura integração remota)
  420. gerenciadorMCP.registrarMCP({
  421. id: 'academia-remota-sse',
  422. nome: 'Academia MCP Remoto (SSE)',
  423. tipo: 'sse',
  424. url: process.env.MCP_URL_ACADEMIA || 'http://localhost:3401/mcp', // URL do endpoint SSE (DEVE incluir /mcp)
  425. versao: '1.0.0',
  426. idioma: 'pt-br',
  427. dominios: ['exercicios', 'treinos', 'musculacao'],
  428. ferramentas: [
  429. 'buscar_exercicios_por_grupo',
  430. 'listar_grupos_musculares',
  431. 'buscar_exercicio_por_nome',
  432. 'listar_todos_exercicios',
  433. 'obter_detalhes_exercicio'
  434. ]
  435. });
  436. // Conecta a todos
  437. try {
  438. await gerenciadorMCP.conectarTodos();
  439. console.log('[MCP-MANAGER] ✓ Inicialização concluída com sucesso');
  440. console.log('[MCP-MANAGER] Protocolo: v2024-11-05 (Streamable HTTP)');
  441. } catch (erro) {
  442. console.error('[MCP-MANAGER] ✗ Erro na inicialização:', erro);
  443. console.log('[MCP-MANAGER] API continuará funcionando sem MCPs');
  444. // Não lança erro para não impedir a API de iniciar
  445. }
  446. }
  447. export {
  448. gerenciadorMCP,
  449. inicializarMCPs,
  450. type ConfiguracaoMCP
  451. };