Harpia Omnichannel MCP
by raiffhigor
README.md
# Harpia Omnichannel MCP - versão produção
Projeto Node.js com núcleo desacoplado do canal de entrada, preparado para onion architecture e adapters plugáveis de canal, banco e LLM.
## O que já vem pronto
- Núcleo de aplicação independente do canal
- Webhook de verificação da Meta
- Reconhecimento de fala (audio recebido no WhatsApp)
- Resposta por voz (audio enviado no WhatsApp, com fallback para texto)
- Entrada HTTP genérica para qualquer origem
- Validação da assinatura `X-Hub-Signature-256`
- Estrutura multi-tenant
- Repositório local em arquivo JSON para começar rápido
- Adapter de dados mock para WMS/MCP
- Adapter de dados PostgreSQL
- Adapter de dados Oracle (edge-agent)
- Adapter de dados via edge-agent remoto por tenant
- Adapter de LLM compatível com OpenAI
- Dockerfile e docker-compose
- Logs simples em arquivo para rastreabilidade
## Estrutura
```txt
packages/
core-server/
src/
app.js
server.js
edge-agent/
src/
server.js
shared/
src/
edge/
operations.js
src/
application/
controllers/
domain/
infrastructure/
interfaces/
middlewares/
repositories/
routes/
services/
utils/
app.js
server.js
edgeAgentServer.js
data/
mcp-tools/
BaseToolDefinition.js
definitions/
```
Os arquivos em `src/app.js`, `src/server.js` e `src/edgeAgentServer.js` foram mantidos como shims de compatibilidade. Os runtimes distribuíveis agora vivem em `packages/core-server` e `packages/edge-agent`, com contrato comum em `packages/shared`.
## Instalação
```bash
npm install
copy .env.example .env
npm start
```
Runtimes disponíveis:
```bash
npm run start:core
npm run start:edge-agent
```
## Base de testes local
Para subir uma base local sem depender de tenant real, usuários reais ou credenciais reais:
```bash
npm run bootstrap:test-data
```
Isso popula o PostgreSQL local com a fixture em `data/test-bootstrap.json`, incluindo:
- tenant `tenant-teste-local`
- usuários OAuth `claude.teste`, `chatgpt.teste` e `admin.teste`
- clients OAuth `claude-local` e `chatgpt-local`
- perfis `user_access` para uso via MCP/HTTP
- credencial MCP `claude-mcp-local`
Senhas padrão da fixture:
- `claude.teste` -> `claude123`
- `chatgpt.teste` -> `chatgpt123`
- `admin.teste` -> `admin123`
Se quiser reaplicar automaticamente esses dados a cada startup local do core:
```env
TEST_DATA_BOOTSTRAP_ENABLED=true
TEST_DATA_FIXTURE_FILE=./data/test-bootstrap.json
DATA_BACKEND=mock
```
Observação:
- A fixture é pensada para ambiente local e usa backend `mock` no tenant de teste.
- O bootstrap é idempotente: faz insert/update sem depender de base real pré-populada.
Modo desenvolvimento (hot-reload):
```bash
npm run dev
npm run dev:edge-agent
```
## Desenvolvimento com Docker (sem rebuild a cada alteracao)
Use o compose de desenvolvimento para montar o codigo-fonte no container e aplicar alteracoes com hot-reload.
Subir ambiente dev:
```bash
docker compose -f docker-compose.dev.yml up -d --build
```
Ver logs:
```bash
docker compose -f docker-compose.dev.yml logs -f --tail=100
```
Parar ambiente dev:
```bash
docker compose -f docker-compose.dev.yml down
```
Observacoes:
- O build inicial ainda e necessario para instalar dependencias.
- Depois disso, alteracoes de codigo sao aplicadas sem rebuild da imagem.
- O comando usado no container e `npm run dev` (`node --watch`).
Desenvolvimento do edge-agent sem rebuild:
```bash
docker compose -f packages/edge-agent/docker-compose.dev.yml up -d --build
docker compose -f packages/edge-agent/docker-compose.dev.yml logs -f --tail=100
docker compose -f packages/edge-agent/docker-compose.dev.yml down
```
## Atualizacao rapida na VPS (producao)
No ambiente de producao (sem bind mount), o fluxo mais rapido e:
```bash
cd /caminho/do/projeto
git pull
docker compose up -d --build --no-deps harpia-whatsapp-mcp
docker compose ps
docker compose logs --tail=80 harpia-whatsapp-mcp
```
Esse comando reconstrói e reinicia somente o servico do core, sem derrubar dependencias desnecessariamente.
## Deploy no Portainer (Server)
Arquivos prontos para stack:
- `docker-compose.portainer.yml`
- `.env.portainer.example`
Passos:
1. Copie `.env.portainer.example` para `.env` e preencha tokens/chaves.
2. No Portainer, crie uma Stack usando o conteúdo de `docker-compose.portainer.yml`.
3. Garanta que a stack tenha acesso ao diretório `data/` (bind `./data:/app/data`).
4. Faça deploy e valide em `GET /health`.
Observação:
- Este compose é para o `core-server` no servidor.
- O `edge-agent` roda no cliente e se conecta ao core via `edge/poll` e `edge/result`.
Para gerar artefatos standalone de deploy:
```bash
npm run export:runtimes
```
O comando gera:
- `dist/core-server`: bundle para instalar no seu servidor
- `dist/edge-agent`: bundle para instalar no cliente
## Arquitetura
- `domain`: regras puras de linguagem e intenção.
- `application`: casos de uso independentes de canal.
- `infrastructure`: adapters de banco, LLM, logging e canais de saída.
- `interfaces`: adapters de entrada, como HTTP e webhook da Meta.
- `packages/core-server`: runtime do servidor central.
- `packages/edge-agent`: runtime local instalado no cliente.
- `packages/shared`: contratos compartilhados entre core e agente.
O webhook do WhatsApp agora é apenas um adapter. O mesmo caso de uso pode ser acionado por HTTP, CLI, fila, e-mail ou outro canal futuro.
As operacoes usadas pelas tools sao carregadas a partir das ToolDefinitions persistentes em `data/mcp-tools/definitions`
e refletidas no runtime sem depender de `data/tools.json`.
O provider usado pelo core continua sendo definido globalmente por `DATA_BACKEND`.
## Cadastro de tenant
Os tenants ficam persistidos no PostgreSQL, na tabela `tenants`, e podem ser gerenciados pelo painel admin.
Exemplo:
```json
[
{
"tenantId": "cliente-demo",
"name": "Cliente Demo",
"phoneNumberId": "123456789012345",
"businessAccountId": "987654321",
"accessToken": "EAAB...",
"verifyToken": "opcional_por_tenant",
"edgeAgent": {
"mode": "reverse-client",
"token": "token-compartilhado"
},
"orchestration": {
"mode": "llm-mcp"
},
"llm": {
"provider": "openai-compatible",
"baseUrl": "https://api.openai.com/v1",
"apiKey": "sk-...",
"model": "gpt-4o-mini",
"assistantName": "Ariane WMS",
"assistantIdentity": "Eu sou a Ariane WMS, assistente operacional oficial da Cliente Demo.",
"systemPrompt": "Sempre se identifique usando a mensagem oficial da empresa quando perguntarem sobre sua identidade.",
"audio": {
"provider": "elevenlabs",
"baseUrl": "https://api.elevenlabs.io/v1",
"apiKey": "elevenlabs-api-key",
"voiceId": "JBFqnCBsd6RMkjVDRZzb",
"modelId": "eleven_multilingual_v2",
"outputFormat": "mp3_44100_128",
"stability": 0.45,
"similarityBoost": 0.8,
"style": 0,
"speakerBoost": true,
"sttProvider": "openai-compatible",
"sttBaseUrl": "https://api.openai.com/v1",
"sttApiKey": "sk-...",
"sttModel": "whisper-1"
},
"mcpOrchestratorEnabled": true,
"mcpOrchestratorMaxToolRounds": 3
},
"wms": {
"type": "mock",
"oracleConnectString": "",
"oracleUser": "",
"oraclePassword": ""
},
"active": true
}
]
```
## Identidade e permissões de usuário
Para controlar quais usuários podem ver quais dados, use a tabela `user_access` no PostgreSQL.
Chave de identificação do usuário:
- `tenantId` + `channel` + `userId`
Exemplos de `userId` por canal:
- `whatsapp`: número/wa id (ex.: `5511999999999`)
- `http`: id interno do usuário
- `cli`: id técnico de operador
Campos principais do perfil:
- `scopes`: lista de escopos permitidos (`receiving_status`, `stock_summary`, `picking_status`)
- `maxInfoLevel`: nível máximo de informação permitido (numérico)
- `branchIds`: lista de filiais permitidas para o usuário
- Formato recomendado: `[{ "code": "SP01", "description": "Sao Paulo" }]`
- Compatibilidade: também aceita array de string (`["SP01", "RJ02"]`)
- `active`: habilita/desabilita o perfil
Variáveis:
- `USER_BRANCH_CONTEXT_TTL_MS` (tempo para manter a filial selecionada na sessão do usuário)
O core valida o usuário antes de responder e bloqueia acessos sem perfil/permissão.
Fluxo para usuário com múltiplas filiais:
1. Se o usuário tiver várias `branchIds`, o bot pede seleção da filial.
2. O usuário responde com o código da filial (ex: `SP01`).
3. O core mantém essa filial em contexto temporário por usuário/canal.
4. Consultas seguintes usam essa filial até expirar o contexto.
## URLs
- Runtime dos adapters:
- `GET /api/runtime`
- Entrada genérica:
- `POST /api/interactions`
- Verificação Meta:
- `GET /meta/webhook`
- Recebimento de mensagens:
- `POST /meta/webhook`
- Health:
- `GET /health`
- MCP JSON-RPC:
- `POST /mcp`
## Fluxo
1. Um canal de entrada envia a interação para o caso de uso.
2. O caso de uso resolve tenant, intenção e provider.
3. O adapter de dados consulta o backend configurado.
4. O adapter de LLM pode complementar respostas livres.
5. O adapter de saída devolve a resposta ao canal.
## Exemplo de entrada genérica
```bash
curl -X POST http://localhost:3000/api/interactions \
-H "Content-Type: application/json" \
-d '{
"tenantId": "cliente-demo",
"channel": "http",
"userId": "usuario-demo",
"userName": "Operador",
"text": "estoque"
}'
```
## Configuração de providers
- `DATA_BACKEND=mock`: usa o adapter mock atual.
- `DATA_BACKEND=postgres`: usa adapter PostgreSQL via `pg`.
- `DATA_BACKEND=oracle`: usa adapter Oracle via `oracledb`.
- `DATA_BACKEND=edge-agent`: cloud core consulta um edge-agent no cliente.
- `LLM_PROVIDER=disabled`: desliga LLM.
- `LLM_PROVIDER=openai-compatible`: usa qualquer endpoint compatível com OpenAI.
- `LLM_BASE_URL`, `LLM_API_KEY` e `LLM_MODEL`: definem o provider de LLM.
- `AUDIO_PROVIDER`, `AUDIO_BASE_URL` e `AUDIO_API_KEY`: permitem separar STT/TTS do provider de chat.
- `ELEVENLABS_VOICE_ID` e `ELEVENLABS_MODEL_ID`: configuram TTS via ElevenLabs.
Configuração por tenant com fallback para `.env`:
- Você pode definir na coluna `config` da tabela `tenants`, no bloco `llm`.
- Campos suportados: `provider`, `baseUrl`, `apiKey`, `model`, `systemPrompt`, `assistantName`, `assistantIdentity`, `mcpOrchestratorEnabled`, `mcpOrchestratorMaxToolRounds`.
- Se um campo não existir no tenant, o core usa o valor global do `.env`.
- Para áudio, use `llm.audio.provider`, `llm.audio.baseUrl`, `llm.audio.apiKey`, `llm.audio.sttProvider`, `llm.audio.sttBaseUrl`, `llm.audio.sttApiKey`, `llm.audio.sttModel`, `llm.audio.ttsModel`, `llm.audio.ttsVoice`, `llm.audio.ttsInstructions`, `llm.audio.voiceId`, `llm.audio.modelId`, `llm.audio.outputFormat`, `llm.audio.stability`, `llm.audio.similarityBoost`, `llm.audio.style`, `llm.audio.speakerBoost`.
Campos de identidade do assistente por tenant:
- `assistantName`: nome oficial do assistente da empresa.
- `assistantIdentity`: texto oficial para responder quando o usuário perguntar "quem é você", "qual seu nome" ou perguntas equivalentes sobre a identidade do assistente.
### Audio no WhatsApp
Para receber audio e responder por voz:
- `WA_VOICE_REPLY_ENABLED=true|false`
- `STT_MODEL` (default `whisper-1`)
- `TTS_MODEL` (default `tts-1`)
- `TTS_VOICE` (default `alloy`)
- `AUDIO_PROVIDER` (default `openai-compatible`)
- `AUDIO_BASE_URL` e `AUDIO_API_KEY` para separar o provider de audio do provider de chat
- `AUDIO_STT_PROVIDER`, `AUDIO_STT_BASE_URL` e `AUDIO_STT_API_KEY` para STT separado do TTS
- `ELEVENLABS_VOICE_ID`, `ELEVENLABS_MODEL_ID`, `ELEVENLABS_OUTPUT_FORMAT`
Observação: a transcrição e a síntese usam `LLM_BASE_URL` e `LLM_API_KEY` em endpoints compatíveis com OpenAI (`/audio/transcriptions` e `/audio/speech`).
Se o `LLM_BASE_URL` apontar para LiteLLM, configure aliases de áudio como `harpia-stt` e `harpia-tts` no `litellm-config.yaml` e use esses nomes em `STT_MODEL` e `TTS_MODEL`.
Se `AUDIO_BASE_URL` estiver configurado, STT/TTS usam esse endpoint em vez de compartilhar o provider de chat.
Para `AUDIO_PROVIDER=elevenlabs`, apenas o TTS vai para ElevenLabs; o STT continua via provider configurado em `AUDIO_STT_PROVIDER`/`AUDIO_STT_BASE_URL`.
Exemplo de `.env` com ElevenLabs para voz e OpenAI para transcricao:
```env
AUDIO_PROVIDER=elevenlabs
AUDIO_API_KEY=elevenlabs-api-key
ELEVENLABS_VOICE_ID=JBFqnCBsd6RMkjVDRZzb
ELEVENLABS_MODEL_ID=eleven_multilingual_v2
ELEVENLABS_OUTPUT_FORMAT=mp3_44100_128
AUDIO_STT_PROVIDER=openai-compatible
AUDIO_STT_BASE_URL=https://api.openai.com/v1
AUDIO_STT_API_KEY=sk-...
AUDIO_STT_MODEL=whisper-1
```
### Provider Edge Agent (cloud core)
No modo atual (reverso), o core não abre conexão para o cliente. O edge-agent do cliente faz long-poll no core e recebe comandos.
Defina no core:
- `EDGE_AGENT_SHARED_TOKEN`
- `EDGE_AGENT_TIMEOUT_MS`
- `EDGE_POLL_WAIT_MS`
Recomendado por tenant no campo `config.edgeAgent` da tabela `tenants`:
- `edgeAgent.token`
### MCP no Core
O core agora expõe um endpoint MCP (JSON-RPC 2.0) para LLMs de terceiros consumirem tools operacionais.
Endpoint:
- `POST /mcp`
Autenticação opcional:
- `MCP_SERVER_TOKEN`
- Envie em `Authorization: Bearer <token>` ou `x-mcp-token`
Autenticação recomendada para cliente externo:
- As credenciais por cliente/integrador ficam na tabela `mcp_clients`.
- Cada credencial pode definir:
- `id`
- `name`
- `token`
- `allowedTenantIds`
- `active`
- Quando `allowedTenantIds` contiver `*` ou estiver ausente/vazio, a credencial pode acessar qualquer tenant.
- Quando `allowedTenantIds` listar tenants explicitamente, `tools/call` rejeita chamadas fora dessa allowlist.
- `MCP_SERVER_TOKEN` continua válido como fallback global, sem escopo por tenant.
- Guia de entrega para cliente: `docs/mcp-client-onboarding.md`
Ativação de tools:
- As tools sao sincronizadas a partir das ToolDefinitions persistentes e do catalogo `mcp_tools` no banco.
- Defina `active: true|false` na ToolDefinition persistente ou no catalogo do banco.
- `tools/list` retorna apenas tools ativas.
- Tools inativas são rejeitadas em `tools/call`.
- `tools/call` valida os argumentos contra o `inputSchema` de cada tool e rejeita campos extras.
ToolDefinitions persistentes (`data/mcp-tools/definitions`):
- Permitem criar/alterar tools sem editar `mcpToolPolicyService.js`.
- Cada arquivo exporta uma ToolDefinition com `name`, `operation`, `active`, `description`, `inputSchema`, `labels` e `units`.
- `labels` e `units` sao opcionais e alimentam o formatter de resultado.
- Bootstrap inicial da pasta persistente: `npm run tools:bootstrap`.
Métodos suportados:
- `initialize`
- `tools/list`
- `tools/call`
Tools disponíveis:
- `get_inbound_status` (args: `tenantId`, `branchId?`, `userId?`, `channel?`, `traceId?`)
- `get_inbound_products` (args: `tenantId`, `branchId?`, `protocolId?`, `userId?`, `channel?`, `traceId?`)
- `get_stock_summary` (args: `tenantId`, `branchId?`, `userId?`, `channel?`, `traceId?`)
- `get_picking_status` (args: `tenantId`, `branchId?`, `userId?`, `channel?`, `traceId?`)
- `get_orders_flow_by_date` (args: `tenantId`, `branchId?`, `targetDate?`, `initialDate?`, `finalDate?`, `userId?`, `channel?`, `traceId?`)
- `get_billed_orders_count` (args: `tenantId`, `branchId?`, `targetDate?`, `initialDate?`, `finalDate?`, `userId?`, `channel?`, `traceId?`)
Formato padrao de retorno das tools:
- `schemaVersion`
- `toolName`
- `tenantId`
- `branchId`
- `metricas` (valores numericos)
- `labels` (descricao semantica de cada metrica)
- `units` (unidade de medida por metrica)
### Orquestração WhatsApp -> LLM -> MCP
Opcionalmente, no canal WhatsApp, você pode delegar a resposta para a LLM com uso de tools MCP.
Variáveis:
- `LLM_MCP_ORCHESTRATOR_ENABLED=true|false`
- `LLM_MCP_ORCHESTRATOR_MAX_TOOL_ROUNDS` (default `3`)
- `ORCHESTRATION_MODE_DEFAULT=rule-engine|llm-basic|llm-mcp`
Configuração por tenant em `data/tenants.json`:
- `orchestration.mode=rule-engine|llm-basic|llm-mcp`
Precedência de modo:
1. `tenant.orchestration.mode`
2. `ORCHESTRATION_MODE_DEFAULT`
3. fallback compatível: se `LLM_MCP_ORCHESTRATOR_ENABLED=true` no WhatsApp, usa `llm-mcp`
4. padrão final: `rule-engine`
Quando habilitado:
1. mensagem entra no WhatsApp
2. LLM decide e chama apenas as tools ativas (`get_inbound_status`, `get_inbound_products`, `get_stock_summary`, `get_picking_status`, `get_orders_flow_by_date`, `get_billed_orders_count`)
3. core executa tool por tenant e devolve para a LLM
4. resposta final padronizada volta ao WhatsApp
### Provider PostgreSQL
Defina `POSTGRES_URL` ou os campos `POSTGRES_HOST`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`.
As consultas padrão assumem tabelas:
- `inbound_tasks`
- `stock_daily_summary`
- `picking_orders`
Você pode sobrescrever qualquer consulta com:
- `POSTGRES_QUERY_INBOUND`
- `POSTGRES_QUERY_INBOUND_PRODUCTS`
- `POSTGRES_QUERY_STOCK`
- `POSTGRES_QUERY_PICKING`
- `POSTGRES_QUERY_ORDERS_FLOW_BY_DATE`
- `POSTGRES_QUERY_BILLED_ORDERS_COUNT`
### Provider Oracle
Para Oracle, instale a dependencia:
```bash
npm install oracledb
```
Defina `ORACLE_CONNECT_STRING` (ou `ORACLE_HOST`, `ORACLE_PORT`, `ORACLE_SERVICE_NAME`) e credenciais `ORACLE_USER` e `ORACLE_PASSWORD`.
Você pode sobrescrever qualquer consulta com:
- `ORACLE_QUERY_INBOUND`
- `ORACLE_QUERY_INBOUND_PRODUCTS`
- `ORACLE_QUERY_STOCK`
- `ORACLE_QUERY_PICKING`
- `ORACLE_QUERY_ORDERS_FLOW_BY_DATE`
- `ORACLE_QUERY_BILLED_ORDERS_COUNT`
As consultas padrao assumem tabelas:
- `inbound_tasks`
- `stock_daily_summary`
- `picking_orders`
- `orders`
Voce pode sobrescrever qualquer consulta com:
- `ORACLE_QUERY_INBOUND`
- `ORACLE_QUERY_STOCK`
- `ORACLE_QUERY_PICKING`
- `ORACLE_QUERY_ORDERS_FLOW_BY_DATE`
- `ORACLE_QUERY_BILLED_ORDERS_COUNT`
## Entrada por CLI
```bash
npm run chat
```
Parâmetros opcionais:
```bash
npm run chat -- --tenant cliente-demo --user operador-01 --name "Carlos"
```
## Edge Agent local (cliente)
Suba um processo separado no ambiente do cliente:
```bash
npm run start:edge-agent
```
Modo recomendado (`EDGE_AGENT_MODE=reverse-client`):
- o cliente conecta no core em `EDGE_CORE_URL`
- faz poll em `POST /edge/poll`
- responde em `POST /edge/result`
- executa queries no backend local (`EDGE_DATA_BACKEND=postgres|oracle|mock`)
- resolve operacoes dinamicamente por convencao (`snake_case` -> `getPascalCase`) para reduzir manutencao no server do agente
Variáveis do edge-agent local:
- `EDGE_AGENT_MODE=reverse-client`
- `EDGE_CORE_URL=http://host-do-core:3000`
- `EDGE_AGENT_TENANT_ID=cliente-demo`
- `EDGE_AGENT_TOKEN` (ou `EDGE_AGENT_SHARED_TOKEN`)
- `EDGE_AGENT_HTTP_TIMEOUT_MS`
- `EDGE_AGENT_RECONNECT_DELAY_MS`
- `EDGE_AGENT_CONCURRENCY` (quantidade de comandos processados em paralelo)
- `EDGE_AGENT_MAX_QUEUE_SIZE` (limite da fila local antes de rejeitar novos comandos)
Modo legado opcional (`EDGE_AGENT_MODE=legacy-server`):
- mantém rotas `GET /health` e `POST /agent/query` para core conectar direto no cliente.
## Comandos já suportados
- `recebimento`
- `estoque`
- `separacao`
- `minhas filiais`
- `trocar filial`
- `ajuda`
## Próximo passo recomendado
Trocar o adapter `mockOperationsDataProvider.js` por adapters reais de Oracle, MCP, PostgreSQL ou outro backend, sem alterar o caso de uso central.