From dc2f3fe93ff579ad0dcc1bf7d192d6ad131c4108 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 01:39:58 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20adiciona=20MCP=20Server=20para=20an?= =?UTF-8?q?=C3=A1lise=20de=20mensagens=20do=20PostgreSQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementa um servidor MCP (Model Context Protocol) completo que permite ao Claude analisar mensagens e dados do Evolution API diretamente do PostgreSQL. Funcionalidades incluídas: - list_instances: Lista instâncias WhatsApp - get_messages: Busca mensagens com filtros avançados - search_messages: Busca por conteúdo de texto - get_conversation: Obtém conversas completas - get_message_stats: Estatísticas detalhadas - get_contacts: Lista contatos - get_chats: Lista chats ativos - get_instance_details: Detalhes de instâncias - execute_query: Queries SQL personalizadas (apenas SELECT) Inclui documentação completa, guia rápido e exemplos de configuração para Claude Desktop. --- mcp-server/.env.example | 6 + mcp-server/.gitignore | 28 + mcp-server/QUICKSTART.md | 153 +++ mcp-server/README.md | 279 +++++ mcp-server/claude_desktop_config.example.json | 13 + mcp-server/package.json | 37 + mcp-server/src/index.ts | 1108 +++++++++++++++++ mcp-server/tsconfig.json | 21 + 8 files changed, 1645 insertions(+) create mode 100644 mcp-server/.env.example create mode 100644 mcp-server/.gitignore create mode 100644 mcp-server/QUICKSTART.md create mode 100644 mcp-server/README.md create mode 100644 mcp-server/claude_desktop_config.example.json create mode 100644 mcp-server/package.json create mode 100644 mcp-server/src/index.ts create mode 100644 mcp-server/tsconfig.json diff --git a/mcp-server/.env.example b/mcp-server/.env.example new file mode 100644 index 00000000..3a9c5ce7 --- /dev/null +++ b/mcp-server/.env.example @@ -0,0 +1,6 @@ +# Evolution API MCP Server - Configuração de Ambiente + +# URL de conexão com o PostgreSQL +# Formato: postgresql://usuario:senha@host:porta/database +# Exemplo: postgresql://postgres:postgres@localhost:5432/evolution +DATABASE_CONNECTION_URI=postgresql://usuario:senha@localhost:5432/evolution diff --git a/mcp-server/.gitignore b/mcp-server/.gitignore new file mode 100644 index 00000000..66501703 --- /dev/null +++ b/mcp-server/.gitignore @@ -0,0 +1,28 @@ +# Dependências +node_modules/ +package-lock.json + +# Build +dist/ +*.tsbuildinfo + +# Ambiente +.env +.env.local + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/mcp-server/QUICKSTART.md b/mcp-server/QUICKSTART.md new file mode 100644 index 00000000..7b0964a7 --- /dev/null +++ b/mcp-server/QUICKSTART.md @@ -0,0 +1,153 @@ +# 🚀 Guia Rápido - Evolution API MCP Server + +Este é um guia rápido para configurar o MCP Server em **5 minutos**. + +## Passo 1: Instalar Dependências + +```bash +cd mcp-server +npm install +``` + +## Passo 2: Configurar Banco de Dados + +Crie o arquivo `.env`: + +```bash +cp .env.example .env +``` + +Edite `.env` e configure sua string de conexão PostgreSQL: + +```env +DATABASE_CONNECTION_URI=postgresql://postgres:senha@localhost:5432/evolution +``` + +## Passo 3: Compilar + +```bash +npm run build +``` + +## Passo 4: Configurar Claude Desktop + +### macOS/Linux + +1. Abra o arquivo de configuração: + ```bash + nano ~/Library/Application\ Support/Claude/claude_desktop_config.json + ``` + +2. Cole esta configuração (ajuste o caminho): + ```json + { + "mcpServers": { + "evolution-api": { + "command": "node", + "args": [ + "/home/user/evolution-api/mcp-server/dist/index.js" + ], + "env": { + "DATABASE_CONNECTION_URI": "postgresql://postgres:senha@localhost:5432/evolution" + } + } + } + } + ``` + +### Windows + +1. Abra o arquivo de configuração: + ``` + notepad %APPDATA%\Claude\claude_desktop_config.json + ``` + +2. Cole esta configuração (ajuste o caminho): + ```json + { + "mcpServers": { + "evolution-api": { + "command": "node", + "args": [ + "C:\\Users\\SeuUsuario\\evolution-api\\mcp-server\\dist\\index.js" + ], + "env": { + "DATABASE_CONNECTION_URI": "postgresql://postgres:senha@localhost:5432/evolution" + } + } + } + } + ``` + +## Passo 5: Reiniciar Claude Desktop + +Feche completamente o Claude Desktop e abra novamente. + +## ✅ Testar + +Abra o Claude Desktop e digite: + +``` +Liste todas as instâncias do Evolution API +``` + +Se funcionar, você verá a lista de instâncias! 🎉 + +## 🔍 Exemplos de Comandos + +Experimente estes comandos no Claude Desktop: + +``` +Mostre as últimas 10 mensagens da instância "minha-instancia" +``` + +``` +Busque mensagens contendo "pedido" nas últimas 24 horas +``` + +``` +Mostre estatísticas de mensagens da instância "vendas" agrupadas por dia +``` + +``` +Liste os contatos da instância "suporte" +``` + +``` +Mostre a conversa completa com o número 5511999999999@s.whatsapp.net +``` + +## 🐛 Problemas? + +### Claude Desktop não mostra o MCP + +1. Verifique se o caminho no `claude_desktop_config.json` está correto (use caminho ABSOLUTO) +2. Verifique se o arquivo foi compilado: `ls mcp-server/dist/index.js` +3. Reinicie o Claude Desktop completamente +4. Veja os logs: Menu > Help > Show Logs + +### Erro de conexão com banco + +1. Teste a conexão manualmente: + ```bash + psql "postgresql://postgres:senha@localhost:5432/evolution" + ``` +2. Verifique se o PostgreSQL está rodando +3. Verifique usuário, senha e nome do banco + +### Para testar localmente (sem Claude Desktop) + +```bash +cd mcp-server +npm run dev +``` + +Isso iniciará o servidor MCP em modo stdio (você verá uma mensagem no console). + +## 📚 Documentação Completa + +Para mais detalhes, consulte o [README.md](README.md) completo. + +--- + +**Pronto!** Agora você pode analisar suas mensagens do WhatsApp com o poder do Claude! 🚀 diff --git a/mcp-server/README.md b/mcp-server/README.md new file mode 100644 index 00000000..39e7dedb --- /dev/null +++ b/mcp-server/README.md @@ -0,0 +1,279 @@ +# Evolution API MCP Server + +**MCP Server** (Model Context Protocol) para análise de mensagens e dados do Evolution API através do PostgreSQL. + +Este servidor permite que o Claude (e outros clientes MCP) consultem e analisem mensagens do WhatsApp armazenadas no banco de dados do Evolution API de forma segura e eficiente. + +## 🚀 Funcionalidades + +### Ferramentas Disponíveis + +1. **`list_instances`** - Lista todas as instâncias WhatsApp cadastradas + - Filtra por status de conexão (open, close, connecting) + - Retorna informações básicas de cada instância + +2. **`get_messages`** - Busca mensagens com filtros avançados + - Filtra por instância, período, contato, tipo de mensagem + - Suporta paginação e ordenação + - Inclui informações de mídia associada + +3. **`search_messages`** - Busca mensagens por conteúdo de texto + - Pesquisa em mensagens de texto, legendas de mídia + - Suporta busca case-sensitive ou case-insensitive + - Filtra por instância e contato + +4. **`get_conversation`** - Obtém conversa completa entre contatos + - Retorna histórico de mensagens ordenado cronologicamente + - Suporta paginação para conversas longas + - Inclui contexto completo da conversa + +5. **`get_message_stats`** - Estatísticas detalhadas de mensagens + - Total de mensagens, enviadas vs recebidas + - Distribuição por tipo de mensagem + - Agrupamento por dia, hora ou tipo + +6. **`get_contacts`** - Lista contatos de uma instância + - Busca por nome ou número + - Inclui foto de perfil e última atualização + +7. **`get_chats`** - Lista chats ativos + - Filtra chats com mensagens não lidas + - Informações de última atividade + +8. **`get_instance_details`** - Detalhes completos de uma instância + - Configurações, webhooks, integrações + - Status de chatbots (Typebot, OpenAI, etc.) + +9. **`execute_query`** - Executa query SQL personalizada (avançado) + - Apenas queries SELECT (segurança) + - Para análises complexas e customizadas + +## 📦 Instalação + +### Pré-requisitos + +- Node.js 18+ ou 20+ +- PostgreSQL com Evolution API rodando +- Acesso à string de conexão do banco de dados + +### Passo 1: Instalar dependências + +```bash +cd mcp-server +npm install +``` + +### Passo 2: Configurar variáveis de ambiente + +Copie o arquivo de exemplo e configure: + +```bash +cp .env.example .env +``` + +Edite o arquivo `.env` e configure a string de conexão: + +```env +DATABASE_CONNECTION_URI=postgresql://usuario:senha@localhost:5432/evolution +``` + +### Passo 3: Compilar o projeto + +```bash +npm run build +``` + +### Passo 4: Testar localmente + +```bash +npm run dev +``` + +## 🔧 Configuração no Claude Desktop + +Para usar este MCP server com o Claude Desktop, adicione a configuração no arquivo de configuração do Claude: + +### No macOS/Linux + +Edite o arquivo: `~/Library/Application Support/Claude/claude_desktop_config.json` + +```json +{ + "mcpServers": { + "evolution-api": { + "command": "node", + "args": [ + "/caminho/completo/para/evolution-api/mcp-server/dist/index.js" + ], + "env": { + "DATABASE_CONNECTION_URI": "postgresql://usuario:senha@localhost:5432/evolution" + } + } + } +} +``` + +### No Windows + +Edite o arquivo: `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "evolution-api": { + "command": "node", + "args": [ + "C:\\caminho\\completo\\para\\evolution-api\\mcp-server\\dist\\index.js" + ], + "env": { + "DATABASE_CONNECTION_URI": "postgresql://usuario:senha@localhost:5432/evolution" + } + } + } +} +``` + +**Importante:** Substitua `/caminho/completo/para` pelo caminho real onde o projeto está instalado. + +## 📖 Exemplos de Uso + +Depois de configurado, você pode interagir com o Claude Desktop usando comandos naturais: + +### Listar instâncias + +``` +Mostre todas as instâncias WhatsApp ativas +``` + +### Buscar mensagens + +``` +Busque as últimas 50 mensagens da instância "minha-instancia" recebidas hoje +``` + +### Pesquisar por conteúdo + +``` +Procure mensagens que contenham "orçamento" na instância "vendas" +``` + +### Obter conversa + +``` +Mostre a conversa completa com o contato 5511999999999@s.whatsapp.net +``` + +### Estatísticas + +``` +Mostre estatísticas de mensagens da instância "suporte" agrupadas por hora +``` + +### Análise avançada + +``` +Execute uma query para contar mensagens por tipo de mídia na última semana +``` + +## 🛠️ Desenvolvimento + +### Estrutura do projeto + +``` +mcp-server/ +├── src/ +│ └── index.ts # Código principal do servidor MCP +├── dist/ # Código compilado (gerado pelo build) +├── package.json # Dependências e scripts +├── tsconfig.json # Configuração TypeScript +├── .env # Variáveis de ambiente (não commitar!) +├── .env.example # Exemplo de configuração +└── README.md # Esta documentação +``` + +### Scripts disponíveis + +- `npm run build` - Compila o TypeScript para JavaScript +- `npm run dev` - Roda em modo desenvolvimento com hot reload +- `npm start` - Executa o servidor compilado +- `npm run watch` - Modo watch para desenvolvimento + +### Adicionando novas ferramentas + +1. Adicione a definição da ferramenta no array `TOOLS` +2. Implemente o método correspondente na classe `EvolutionMCPServer` +3. Adicione o case no switch do `CallToolRequestSchema` handler +4. Compile e teste + +## 🔒 Segurança + +- **Queries SQL**: Apenas queries SELECT são permitidas na ferramenta `execute_query` +- **Validação**: Palavras-chave perigosas (INSERT, UPDATE, DELETE, etc.) são bloqueadas +- **Conexão**: Use sempre variáveis de ambiente para credenciais +- **Pool de conexões**: Limite de 10 conexões simultâneas ao PostgreSQL + +## 📊 Schema do Banco de Dados + +O servidor trabalha com as seguintes tabelas principais: + +- `Instance` - Instâncias WhatsApp +- `Message` - Mensagens enviadas e recebidas +- `Contact` - Contatos sincronizados +- `Chat` - Conversas ativas +- `Media` - Arquivos de mídia +- `Webhook` - Configurações de webhook +- `Setting` - Configurações de instância +- E outras tabelas de integrações (Chatwoot, Typebot, OpenAI, etc.) + +Para mais detalhes, consulte o schema Prisma em `prisma/postgresql-schema.prisma`. + +## 🐛 Troubleshooting + +### Erro de conexão com o banco + +``` +Erro: password authentication failed for user "postgres" +``` + +**Solução**: Verifique se a string de conexão está correta no arquivo `.env` ou na configuração do Claude Desktop. + +### Servidor não aparece no Claude Desktop + +1. Verifique se o arquivo de configuração está no local correto +2. Certifique-se de que o caminho para o `index.js` está correto (absoluto) +3. Reinicie o Claude Desktop completamente +4. Verifique os logs do Claude Desktop (Menu > Help > Show Logs) + +### Timeout nas queries + +Se as queries estão demorando muito: + +1. Crie índices no banco de dados para campos frequentemente consultados +2. Reduza o `limit` das queries +3. Use filtros mais específicos (datas, instâncias) + +## 📝 Licença + +Apache-2.0 - Mesma licença do Evolution API + +## 🤝 Contribuindo + +Contribuições são bem-vindas! Por favor: + +1. Faça um fork do projeto +2. Crie uma branch para sua feature (`git checkout -b feature/MinhaFeature`) +3. Commit suas mudanças (`git commit -m 'feat: Adiciona nova feature'`) +4. Push para a branch (`git push origin feature/MinhaFeature`) +5. Abra um Pull Request + +## 📞 Suporte + +Para problemas e dúvidas: + +- Abra uma issue no repositório do Evolution API +- Consulte a documentação do MCP: https://modelcontextprotocol.io/ +- Comunidade Evolution API: https://evolution-api.com/ + +--- + +**Desenvolvido para o Evolution API** - A melhor API REST para WhatsApp diff --git a/mcp-server/claude_desktop_config.example.json b/mcp-server/claude_desktop_config.example.json new file mode 100644 index 00000000..ac4bcac6 --- /dev/null +++ b/mcp-server/claude_desktop_config.example.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "evolution-api": { + "command": "node", + "args": [ + "/CAMINHO/COMPLETO/PARA/evolution-api/mcp-server/dist/index.js" + ], + "env": { + "DATABASE_CONNECTION_URI": "postgresql://usuario:senha@localhost:5432/evolution" + } + } + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 00000000..15016825 --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,37 @@ +{ + "name": "evolution-api-mcp-server", + "version": "1.0.0", + "description": "MCP Server para análise de mensagens do Evolution API via PostgreSQL", + "type": "module", + "main": "dist/index.js", + "bin": { + "evolution-mcp": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "start": "node dist/index.js", + "watch": "tsx watch src/index.ts" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "evolution-api", + "whatsapp", + "database", + "postgresql" + ], + "author": "Evolution API", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4", + "pg": "^8.13.1", + "dotenv": "^16.4.7" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "@types/pg": "^8.11.10", + "tsx": "^4.20.5", + "typescript": "^5.7.2" + } +} diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts new file mode 100644 index 00000000..eff6baf6 --- /dev/null +++ b/mcp-server/src/index.ts @@ -0,0 +1,1108 @@ +#!/usr/bin/env node + +/** + * Evolution API MCP Server + * + * Servidor MCP (Model Context Protocol) para análise de mensagens e dados + * do Evolution API através do PostgreSQL. + * + * Ferramentas disponíveis: + * - list_instances: Lista todas as instâncias WhatsApp + * - get_messages: Busca mensagens com filtros avançados + * - search_messages: Busca mensagens por texto + * - get_conversation: Obtém conversa completa entre contatos + * - get_message_stats: Estatísticas de mensagens por instância + * - get_contacts: Lista contatos de uma instância + * - get_chats: Lista chats ativos + * - get_instance_details: Detalhes completos de uma instância + * - execute_query: Executa query SQL personalizada (uso avançado) + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + Tool, +} from "@modelcontextprotocol/sdk/types.js"; +import { Pool } from "pg"; +import * as dotenv from "dotenv"; + +// Carregar variáveis de ambiente +dotenv.config(); + +// Configuração do PostgreSQL +const pool = new Pool({ + connectionString: process.env.DATABASE_CONNECTION_URI, + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 10000, +}); + +// Validar conexão ao iniciar +pool.on('error', (err) => { + console.error('Erro inesperado no pool do PostgreSQL:', err); + process.exit(-1); +}); + +// Definição das ferramentas disponíveis +const TOOLS: Tool[] = [ + { + name: "list_instances", + description: "Lista todas as instâncias WhatsApp cadastradas no Evolution API com status de conexão e informações básicas", + inputSchema: { + type: "object", + properties: { + status: { + type: "string", + enum: ["open", "close", "connecting"], + description: "Filtrar por status de conexão (opcional)", + }, + limit: { + type: "number", + description: "Número máximo de resultados (padrão: 50)", + }, + }, + }, + }, + { + name: "get_messages", + description: "Busca mensagens com filtros avançados (instância, período, contato, tipo de mensagem, etc.)", + inputSchema: { + type: "object", + properties: { + instanceName: { + type: "string", + description: "Nome da instância (obrigatório se não especificar instanceId)", + }, + instanceId: { + type: "string", + description: "ID da instância (obrigatório se não especificar instanceName)", + }, + remoteJid: { + type: "string", + description: "JID do contato/grupo (ex: 5511999999999@s.whatsapp.net)", + }, + messageType: { + type: "string", + description: "Tipo de mensagem (conversation, imageMessage, videoMessage, audioMessage, documentMessage, etc.)", + }, + startDate: { + type: "string", + description: "Data inicial no formato ISO 8601 (ex: 2024-01-01T00:00:00Z)", + }, + endDate: { + type: "string", + description: "Data final no formato ISO 8601", + }, + limit: { + type: "number", + description: "Número máximo de mensagens (padrão: 100, máximo: 1000)", + }, + offset: { + type: "number", + description: "Deslocamento para paginação (padrão: 0)", + }, + orderBy: { + type: "string", + enum: ["asc", "desc"], + description: "Ordenação por timestamp (padrão: desc - mais recentes primeiro)", + }, + }, + }, + }, + { + name: "search_messages", + description: "Busca mensagens pelo conteúdo de texto. Suporta busca em mensagens de texto, legendas de mídia e mensagens extendidas", + inputSchema: { + type: "object", + properties: { + searchText: { + type: "string", + description: "Texto a ser buscado nas mensagens (obrigatório)", + }, + instanceName: { + type: "string", + description: "Nome da instância para filtrar (opcional)", + }, + remoteJid: { + type: "string", + description: "JID do contato/grupo para filtrar (opcional)", + }, + caseSensitive: { + type: "boolean", + description: "Busca case-sensitive (padrão: false)", + }, + limit: { + type: "number", + description: "Número máximo de resultados (padrão: 50, máximo: 500)", + }, + }, + required: ["searchText"], + }, + }, + { + name: "get_conversation", + description: "Obtém uma conversa completa entre a instância e um contato/grupo, ordenada cronologicamente", + inputSchema: { + type: "object", + properties: { + instanceName: { + type: "string", + description: "Nome da instância (obrigatório se não especificar instanceId)", + }, + instanceId: { + type: "string", + description: "ID da instância (obrigatório se não especificar instanceName)", + }, + remoteJid: { + type: "string", + description: "JID do contato/grupo (obrigatório)", + }, + limit: { + type: "number", + description: "Número de mensagens recentes (padrão: 50, máximo: 500)", + }, + beforeTimestamp: { + type: "number", + description: "Carregar mensagens anteriores a este timestamp (para paginação)", + }, + }, + required: ["remoteJid"], + }, + }, + { + name: "get_message_stats", + description: "Obtém estatísticas detalhadas de mensagens: total, por tipo, por período, mensagens enviadas/recebidas", + inputSchema: { + type: "object", + properties: { + instanceName: { + type: "string", + description: "Nome da instância (obrigatório se não especificar instanceId)", + }, + instanceId: { + type: "string", + description: "ID da instância (obrigatório se não especificar instanceName)", + }, + startDate: { + type: "string", + description: "Data inicial para análise (formato ISO 8601)", + }, + endDate: { + type: "string", + description: "Data final para análise (formato ISO 8601)", + }, + groupBy: { + type: "string", + enum: ["day", "hour", "type"], + description: "Agrupar estatísticas por dia, hora ou tipo de mensagem", + }, + }, + }, + }, + { + name: "get_contacts", + description: "Lista os contatos de uma instância com informações de nome e foto de perfil", + inputSchema: { + type: "object", + properties: { + instanceName: { + type: "string", + description: "Nome da instância (obrigatório se não especificar instanceId)", + }, + instanceId: { + type: "string", + description: "ID da instância (obrigatório se não especificar instanceName)", + }, + search: { + type: "string", + description: "Buscar por nome ou número", + }, + limit: { + type: "number", + description: "Número máximo de contatos (padrão: 100)", + }, + }, + }, + }, + { + name: "get_chats", + description: "Lista os chats ativos de uma instância com informações de última mensagem e mensagens não lidas", + inputSchema: { + type: "object", + properties: { + instanceName: { + type: "string", + description: "Nome da instância (obrigatório se não especificar instanceId)", + }, + instanceId: { + type: "string", + description: "ID da instância (obrigatório se não especificar instanceName)", + }, + onlyUnread: { + type: "boolean", + description: "Mostrar apenas chats com mensagens não lidas (padrão: false)", + }, + limit: { + type: "number", + description: "Número máximo de chats (padrão: 50)", + }, + }, + }, + }, + { + name: "get_instance_details", + description: "Obtém informações detalhadas de uma instância incluindo configurações, integrações e webhooks", + inputSchema: { + type: "object", + properties: { + instanceName: { + type: "string", + description: "Nome da instância (obrigatório se não especificar instanceId)", + }, + instanceId: { + type: "string", + description: "ID da instância (obrigatório se não especificar instanceName)", + }, + }, + }, + }, + { + name: "execute_query", + description: "Executa uma query SQL personalizada no banco de dados (uso avançado). ATENÇÃO: Apenas queries SELECT são permitidas por segurança", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Query SQL a ser executada (apenas SELECT)", + }, + params: { + type: "array", + items: { + type: "string", + }, + description: "Parâmetros para a query (opcional, para queries parametrizadas)", + }, + }, + required: ["query"], + }, + }, +]; + +// Classe do servidor MCP +class EvolutionMCPServer { + private server: Server; + + constructor() { + this.server = new Server( + { + name: "evolution-api-mcp-server", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + }, + } + ); + + this.setupHandlers(); + } + + private setupHandlers() { + // Handler para listar ferramentas + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: TOOLS, + })); + + // Handler para executar ferramentas + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + try { + const { name, arguments: args } = request.params; + + switch (name) { + case "list_instances": + return await this.listInstances(args); + case "get_messages": + return await this.getMessages(args); + case "search_messages": + return await this.searchMessages(args); + case "get_conversation": + return await this.getConversation(args); + case "get_message_stats": + return await this.getMessageStats(args); + case "get_contacts": + return await this.getContacts(args); + case "get_chats": + return await this.getChats(args); + case "get_instance_details": + return await this.getInstanceDetails(args); + case "execute_query": + return await this.executeQuery(args); + default: + throw new Error(`Ferramenta desconhecida: ${name}`); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text", + text: `Erro ao executar a ferramenta: ${errorMessage}`, + }, + ], + }; + } + }); + } + + private async listInstances(args: any) { + const { status, limit = 50 } = args; + + let query = ` + SELECT + id, + name, + "connectionStatus", + "ownerJid", + "profileName", + "profilePicUrl", + integration, + number, + "clientName", + "createdAt", + "updatedAt", + "disconnectionAt" + FROM "Instance" + `; + + const params: any[] = []; + const conditions: string[] = []; + + if (status) { + conditions.push(`"connectionStatus" = $${params.length + 1}`); + params.push(status); + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += ` ORDER BY "createdAt" DESC LIMIT $${params.length + 1}`; + params.push(limit); + + const result = await pool.query(query, params); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + total: result.rowCount, + instances: result.rows, + }, + null, + 2 + ), + }, + ], + }; + } + + private async getMessages(args: any) { + const { + instanceName, + instanceId, + remoteJid, + messageType, + startDate, + endDate, + limit = 100, + offset = 0, + orderBy = "desc", + } = args; + + if (!instanceName && !instanceId) { + throw new Error("É necessário fornecer instanceName ou instanceId"); + } + + // Primeiro, obter o instanceId se foi fornecido instanceName + let finalInstanceId = instanceId; + if (instanceName) { + const instanceResult = await pool.query( + 'SELECT id FROM "Instance" WHERE name = $1', + [instanceName] + ); + if (instanceResult.rows.length === 0) { + throw new Error(`Instância não encontrada: ${instanceName}`); + } + finalInstanceId = instanceResult.rows[0].id; + } + + let query = ` + SELECT + m.id, + m.key, + m."pushName", + m.participant, + m."messageType", + m.message, + m."contextInfo", + m.source, + m."messageTimestamp", + m.status, + m."createdAt", + med.id as "mediaId", + med."fileName" as "mediaFileName", + med.type as "mediaType", + med.mimetype as "mediaMimetype" + FROM "Message" m + LEFT JOIN "Media" med ON med."messageId" = m.id + WHERE m."instanceId" = $1 + `; + + const params: any[] = [finalInstanceId]; + let paramIndex = 2; + + if (remoteJid) { + query += ` AND m.key->>'remoteJid' = $${paramIndex}`; + params.push(remoteJid); + paramIndex++; + } + + if (messageType) { + query += ` AND m."messageType" = $${paramIndex}`; + params.push(messageType); + paramIndex++; + } + + if (startDate) { + const startTimestamp = Math.floor(new Date(startDate).getTime() / 1000); + query += ` AND m."messageTimestamp" >= $${paramIndex}`; + params.push(startTimestamp); + paramIndex++; + } + + if (endDate) { + const endTimestamp = Math.floor(new Date(endDate).getTime() / 1000); + query += ` AND m."messageTimestamp" <= $${paramIndex}`; + params.push(endTimestamp); + paramIndex++; + } + + query += ` ORDER BY m."messageTimestamp" ${orderBy.toUpperCase()}`; + query += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`; + params.push(Math.min(limit, 1000), offset); + + const result = await pool.query(query, params); + + // Contar total de mensagens que correspondem aos filtros + let countQuery = `SELECT COUNT(*) as total FROM "Message" m WHERE m."instanceId" = $1`; + const countParams: any[] = [finalInstanceId]; + let countParamIndex = 2; + + if (remoteJid) { + countQuery += ` AND m.key->>'remoteJid' = $${countParamIndex}`; + countParams.push(remoteJid); + countParamIndex++; + } + + if (messageType) { + countQuery += ` AND m."messageType" = $${countParamIndex}`; + countParams.push(messageType); + countParamIndex++; + } + + if (startDate) { + const startTimestamp = Math.floor(new Date(startDate).getTime() / 1000); + countQuery += ` AND m."messageTimestamp" >= $${countParamIndex}`; + countParams.push(startTimestamp); + countParamIndex++; + } + + if (endDate) { + const endTimestamp = Math.floor(new Date(endDate).getTime() / 1000); + countQuery += ` AND m."messageTimestamp" <= $${countParamIndex}`; + countParams.push(endTimestamp); + } + + const countResult = await pool.query(countQuery, countParams); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + total: parseInt(countResult.rows[0].total), + count: result.rowCount, + offset, + limit, + messages: result.rows, + }, + null, + 2 + ), + }, + ], + }; + } + + private async searchMessages(args: any) { + const { + searchText, + instanceName, + remoteJid, + caseSensitive = false, + limit = 50, + } = args; + + if (!searchText) { + throw new Error("searchText é obrigatório"); + } + + let query = ` + SELECT + m.id, + m.key, + m."pushName", + m."messageType", + m.message, + m."messageTimestamp", + i.name as "instanceName" + FROM "Message" m + INNER JOIN "Instance" i ON i.id = m."instanceId" + WHERE 1=1 + `; + + const params: any[] = []; + let paramIndex = 1; + + if (instanceName) { + query += ` AND i.name = $${paramIndex}`; + params.push(instanceName); + paramIndex++; + } + + if (remoteJid) { + query += ` AND m.key->>'remoteJid' = $${paramIndex}`; + params.push(remoteJid); + paramIndex++; + } + + // Busca no conteúdo da mensagem (conversation, extendedTextMessage, imageMessage caption, etc.) + const searchOperator = caseSensitive ? "LIKE" : "ILIKE"; + query += ` AND ( + m.message->>'conversation' ${searchOperator} $${paramIndex} OR + m.message->'extendedTextMessage'->>'text' ${searchOperator} $${paramIndex} OR + m.message->'imageMessage'->>'caption' ${searchOperator} $${paramIndex} OR + m.message->'videoMessage'->>'caption' ${searchOperator} $${paramIndex} OR + m.message->'documentMessage'->>'caption' ${searchOperator} $${paramIndex} + )`; + params.push(`%${searchText}%`); + paramIndex++; + + query += ` ORDER BY m."messageTimestamp" DESC LIMIT $${paramIndex}`; + params.push(Math.min(limit, 500)); + + const result = await pool.query(query, params); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + total: result.rowCount, + searchText, + messages: result.rows, + }, + null, + 2 + ), + }, + ], + }; + } + + private async getConversation(args: any) { + const { + instanceName, + instanceId, + remoteJid, + limit = 50, + beforeTimestamp, + } = args; + + if (!remoteJid) { + throw new Error("remoteJid é obrigatório"); + } + + if (!instanceName && !instanceId) { + throw new Error("É necessário fornecer instanceName ou instanceId"); + } + + let finalInstanceId = instanceId; + if (instanceName) { + const instanceResult = await pool.query( + 'SELECT id FROM "Instance" WHERE name = $1', + [instanceName] + ); + if (instanceResult.rows.length === 0) { + throw new Error(`Instância não encontrada: ${instanceName}`); + } + finalInstanceId = instanceResult.rows[0].id; + } + + let query = ` + SELECT + m.id, + m.key, + m."pushName", + m.participant, + m."messageType", + m.message, + m."contextInfo", + m.source, + m."messageTimestamp", + m.status, + med."fileName" as "mediaFileName", + med.type as "mediaType" + FROM "Message" m + LEFT JOIN "Media" med ON med."messageId" = m.id + WHERE m."instanceId" = $1 + AND m.key->>'remoteJid' = $2 + `; + + const params: any[] = [finalInstanceId, remoteJid]; + let paramIndex = 3; + + if (beforeTimestamp) { + query += ` AND m."messageTimestamp" < $${paramIndex}`; + params.push(beforeTimestamp); + paramIndex++; + } + + query += ` ORDER BY m."messageTimestamp" DESC LIMIT $${paramIndex}`; + params.push(Math.min(limit, 500)); + + const result = await pool.query(query, params); + + // Inverter para ordem cronológica + const messages = result.rows.reverse(); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + remoteJid, + count: result.rowCount, + messages, + }, + null, + 2 + ), + }, + ], + }; + } + + private async getMessageStats(args: any) { + const { instanceName, instanceId, startDate, endDate, groupBy } = args; + + if (!instanceName && !instanceId) { + throw new Error("É necessário fornecer instanceName ou instanceId"); + } + + let finalInstanceId = instanceId; + if (instanceName) { + const instanceResult = await pool.query( + 'SELECT id FROM "Instance" WHERE name = $1', + [instanceName] + ); + if (instanceResult.rows.length === 0) { + throw new Error(`Instância não encontrada: ${instanceName}`); + } + finalInstanceId = instanceResult.rows[0].id; + } + + const params: any[] = [finalInstanceId]; + let paramIndex = 2; + const conditions: string[] = [`"instanceId" = $1`]; + + if (startDate) { + const startTimestamp = Math.floor(new Date(startDate).getTime() / 1000); + conditions.push(`"messageTimestamp" >= $${paramIndex}`); + params.push(startTimestamp); + paramIndex++; + } + + if (endDate) { + const endTimestamp = Math.floor(new Date(endDate).getTime() / 1000); + conditions.push(`"messageTimestamp" <= $${paramIndex}`); + params.push(endTimestamp); + paramIndex++; + } + + const whereClause = conditions.join(" AND "); + + // Total de mensagens + const totalQuery = `SELECT COUNT(*) as total FROM "Message" WHERE ${whereClause}`; + const totalResult = await pool.query(totalQuery, params); + + // Mensagens enviadas vs recebidas + const sentReceivedQuery = ` + SELECT + key->>'fromMe' as "fromMe", + COUNT(*) as count + FROM "Message" + WHERE ${whereClause} + GROUP BY key->>'fromMe' + `; + const sentReceivedResult = await pool.query(sentReceivedQuery, params); + + // Por tipo de mensagem + const byTypeQuery = ` + SELECT + "messageType", + COUNT(*) as count + FROM "Message" + WHERE ${whereClause} + GROUP BY "messageType" + ORDER BY count DESC + `; + const byTypeResult = await pool.query(byTypeQuery, params); + + let groupedStats = null; + if (groupBy === "day") { + const byDayQuery = ` + SELECT + DATE(to_timestamp("messageTimestamp")) as date, + COUNT(*) as count + FROM "Message" + WHERE ${whereClause} + GROUP BY DATE(to_timestamp("messageTimestamp")) + ORDER BY date DESC + LIMIT 30 + `; + const byDayResult = await pool.query(byDayQuery, params); + groupedStats = { byDay: byDayResult.rows }; + } else if (groupBy === "hour") { + const byHourQuery = ` + SELECT + EXTRACT(HOUR FROM to_timestamp("messageTimestamp")) as hour, + COUNT(*) as count + FROM "Message" + WHERE ${whereClause} + GROUP BY EXTRACT(HOUR FROM to_timestamp("messageTimestamp")) + ORDER BY hour + `; + const byHourResult = await pool.query(byHourQuery, params); + groupedStats = { byHour: byHourResult.rows }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + total: parseInt(totalResult.rows[0].total), + sentReceived: sentReceivedResult.rows, + byType: byTypeResult.rows, + ...groupedStats, + }, + null, + 2 + ), + }, + ], + }; + } + + private async getContacts(args: any) { + const { instanceName, instanceId, search, limit = 100 } = args; + + if (!instanceName && !instanceId) { + throw new Error("É necessário fornecer instanceName ou instanceId"); + } + + let finalInstanceId = instanceId; + if (instanceName) { + const instanceResult = await pool.query( + 'SELECT id FROM "Instance" WHERE name = $1', + [instanceName] + ); + if (instanceResult.rows.length === 0) { + throw new Error(`Instância não encontrada: ${instanceName}`); + } + finalInstanceId = instanceResult.rows[0].id; + } + + let query = ` + SELECT + id, + "remoteJid", + "pushName", + "profilePicUrl", + "createdAt", + "updatedAt" + FROM "Contact" + WHERE "instanceId" = $1 + `; + + const params: any[] = [finalInstanceId]; + let paramIndex = 2; + + if (search) { + query += ` AND ("pushName" ILIKE $${paramIndex} OR "remoteJid" ILIKE $${paramIndex})`; + params.push(`%${search}%`); + paramIndex++; + } + + query += ` ORDER BY "updatedAt" DESC LIMIT $${paramIndex}`; + params.push(limit); + + const result = await pool.query(query, params); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + total: result.rowCount, + contacts: result.rows, + }, + null, + 2 + ), + }, + ], + }; + } + + private async getChats(args: any) { + const { instanceName, instanceId, onlyUnread = false, limit = 50 } = args; + + if (!instanceName && !instanceId) { + throw new Error("É necessário fornecer instanceName ou instanceId"); + } + + let finalInstanceId = instanceId; + if (instanceName) { + const instanceResult = await pool.query( + 'SELECT id FROM "Instance" WHERE name = $1', + [instanceName] + ); + if (instanceResult.rows.length === 0) { + throw new Error(`Instância não encontrada: ${instanceName}`); + } + finalInstanceId = instanceResult.rows[0].id; + } + + let query = ` + SELECT + c.id, + c."remoteJid", + c.name, + c."unreadMessages", + c.labels, + c."updatedAt", + c."createdAt" + FROM "Chat" c + WHERE c."instanceId" = $1 + `; + + const params: any[] = [finalInstanceId]; + let paramIndex = 2; + + if (onlyUnread) { + query += ` AND c."unreadMessages" > 0`; + } + + query += ` ORDER BY c."updatedAt" DESC LIMIT $${paramIndex}`; + params.push(limit); + + const result = await pool.query(query, params); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + total: result.rowCount, + chats: result.rows, + }, + null, + 2 + ), + }, + ], + }; + } + + private async getInstanceDetails(args: any) { + const { instanceName, instanceId } = args; + + if (!instanceName && !instanceId) { + throw new Error("É necessário fornecer instanceName ou instanceId"); + } + + let query = ` + SELECT + i.*, + w.url as "webhookUrl", + w.enabled as "webhookEnabled", + w.events as "webhookEvents", + s."rejectCall", + s."groupsIgnore", + s."alwaysOnline", + s."readMessages", + s."readStatus" + FROM "Instance" i + LEFT JOIN "Webhook" w ON w."instanceId" = i.id + LEFT JOIN "Setting" s ON s."instanceId" = i.id + `; + + const params: any[] = []; + + if (instanceId) { + query += ` WHERE i.id = $1`; + params.push(instanceId); + } else { + query += ` WHERE i.name = $1`; + params.push(instanceName); + } + + const result = await pool.query(query, params); + + if (result.rows.length === 0) { + throw new Error("Instância não encontrada"); + } + + // Buscar informações de integrações + const instance = result.rows[0]; + const integrations: any = {}; + + // Chatwoot + const chatwootResult = await pool.query( + 'SELECT enabled, url, "accountId", "nameInbox" FROM "Chatwoot" WHERE "instanceId" = $1', + [instance.id] + ); + if (chatwootResult.rows.length > 0) { + integrations.chatwoot = chatwootResult.rows[0]; + } + + // Typebot + const typebotResult = await pool.query( + 'SELECT COUNT(*) as count, SUM(CASE WHEN enabled THEN 1 ELSE 0 END) as enabled FROM "Typebot" WHERE "instanceId" = $1', + [instance.id] + ); + if (typebotResult.rows[0].count > 0) { + integrations.typebot = { + total: parseInt(typebotResult.rows[0].count), + enabled: parseInt(typebotResult.rows[0].enabled), + }; + } + + // OpenAI + const openaiResult = await pool.query( + 'SELECT COUNT(*) as count FROM "OpenaiBot" WHERE "instanceId" = $1 AND enabled = true', + [instance.id] + ); + if (parseInt(openaiResult.rows[0].count) > 0) { + integrations.openai = { enabledBots: parseInt(openaiResult.rows[0].count) }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + instance, + integrations, + }, + null, + 2 + ), + }, + ], + }; + } + + private async executeQuery(args: any) { + const { query, params = [] } = args; + + // Validação de segurança: apenas SELECT + const normalizedQuery = query.trim().toLowerCase(); + if (!normalizedQuery.startsWith("select")) { + throw new Error( + "Por segurança, apenas queries SELECT são permitidas" + ); + } + + // Verificar se não contém palavras-chave perigosas + const dangerousKeywords = [ + "insert", + "update", + "delete", + "drop", + "create", + "alter", + "truncate", + "grant", + "revoke", + ]; + + for (const keyword of dangerousKeywords) { + if (normalizedQuery.includes(keyword)) { + throw new Error( + `Por segurança, a palavra-chave '${keyword}' não é permitida` + ); + } + } + + const result = await pool.query(query, params); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + rowCount: result.rowCount, + rows: result.rows, + fields: result.fields.map((f) => ({ + name: f.name, + dataTypeID: f.dataTypeID, + })), + }, + null, + 2 + ), + }, + ], + }; + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error("Evolution API MCP Server rodando em stdio"); + } +} + +// Inicializar e executar o servidor +const server = new EvolutionMCPServer(); +server.run().catch((error) => { + console.error("Erro fatal:", error); + process.exit(1); +}); diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json new file mode 100644 index 00000000..ac33d11f --- /dev/null +++ b/mcp-server/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}