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
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
npm install
copy .env.example .env
npm start
Runtimes disponíveis:
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:
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.testeeadmin.teste - clients OAuth
claude-localechatgpt-local - perfis
user_accesspara uso via MCP/HTTP - credencial MCP
claude-mcp-local
Senhas padrão da fixture:
claude.teste->claude123chatgpt.teste->chatgpt123admin.teste->admin123
Se quiser reaplicar automaticamente esses dados a cada startup local do core:
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
mockno tenant de teste. - O bootstrap é idempotente: faz insert/update sem depender de base real pré-populada.
Modo desenvolvimento (hot-reload):
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:
docker compose -f docker-compose.dev.yml up -d --build
Ver logs:
docker compose -f docker-compose.dev.yml logs -f --tail=100
Parar ambiente dev:
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:
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:
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:
- Copie
.env.portainer.examplepara.enve preencha tokens/chaves. - No Portainer, crie uma Stack usando o conteúdo de
docker-compose.portainer.yml. - Garanta que a stack tenha acesso ao diretório
data/(bind./data:/app/data). - Faça deploy e valide em
GET /health.
Observação:
- Este compose é para o
core-serverno servidor. - O
edge-agentroda no cliente e se conecta ao core viaedge/polleedge/result.
Para gerar artefatos standalone de deploy:
npm run export:runtimes
O comando gera:
dist/core-server: bundle para instalar no seu servidordist/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:
[
{
"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áriocli: 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:
- Se o usuário tiver várias
branchIds, o bot pede seleção da filial. - O usuário responde com o código da filial (ex:
SP01). - O core mantém essa filial em contexto temporário por usuário/canal.
- 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
- Um canal de entrada envia a interação para o caso de uso.
- O caso de uso resolve tenant, intenção e provider.
- O adapter de dados consulta o backend configurado.
- O adapter de LLM pode complementar respostas livres.
- O adapter de saída devolve a resposta ao canal.
Exemplo de entrada genérica
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 viapg.DATA_BACKEND=oracle: usa adapter Oracle viaoracledb.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_KEYeLLM_MODEL: definem o provider de LLM.AUDIO_PROVIDER,AUDIO_BASE_URLeAUDIO_API_KEY: permitem separar STT/TTS do provider de chat.ELEVENLABS_VOICE_IDeELEVENLABS_MODEL_ID: configuram TTS via ElevenLabs.
Configuração por tenant com fallback para .env:
- Você pode definir na coluna
configda tabelatenants, no blocollm. - 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|falseSTT_MODEL(defaultwhisper-1)TTS_MODEL(defaulttts-1)TTS_VOICE(defaultalloy)AUDIO_PROVIDER(defaultopenai-compatible)AUDIO_BASE_URLeAUDIO_API_KEYpara separar o provider de audio do provider de chatAUDIO_STT_PROVIDER,AUDIO_STT_BASE_URLeAUDIO_STT_API_KEYpara STT separado do TTSELEVENLABS_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:
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_TOKENEDGE_AGENT_TIMEOUT_MSEDGE_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>oux-mcp-token
Autenticação recomendada para cliente externo:
- As credenciais por cliente/integrador ficam na tabela
mcp_clients. - Cada credencial pode definir:
idnametokenallowedTenantIdsactive
- Quando
allowedTenantIdscontiver*ou estiver ausente/vazio, a credencial pode acessar qualquer tenant. - Quando
allowedTenantIdslistar tenants explicitamente,tools/callrejeita chamadas fora dessa allowlist. MCP_SERVER_TOKENcontinua 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_toolsno banco. - Defina
active: true|falsena ToolDefinition persistente ou no catalogo do banco. tools/listretorna apenas tools ativas.- Tools inativas são rejeitadas em
tools/call. tools/callvalida os argumentos contra oinputSchemade 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,labelseunits. labelseunitssao opcionais e alimentam o formatter de resultado.- Bootstrap inicial da pasta persistente:
npm run tools:bootstrap.
Métodos suportados:
initializetools/listtools/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:
schemaVersiontoolNametenantIdbranchIdmetricas(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|falseLLM_MCP_ORCHESTRATOR_MAX_TOOL_ROUNDS(default3)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:
tenant.orchestration.modeORCHESTRATION_MODE_DEFAULT- fallback compatível: se
LLM_MCP_ORCHESTRATOR_ENABLED=trueno WhatsApp, usallm-mcp - padrão final:
rule-engine
Quando habilitado:
- mensagem entra no WhatsApp
- 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) - core executa tool por tenant e devolve para a LLM
- 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_tasksstock_daily_summarypicking_orders
Você pode sobrescrever qualquer consulta com:
POSTGRES_QUERY_INBOUNDPOSTGRES_QUERY_INBOUND_PRODUCTSPOSTGRES_QUERY_STOCKPOSTGRES_QUERY_PICKINGPOSTGRES_QUERY_ORDERS_FLOW_BY_DATEPOSTGRES_QUERY_BILLED_ORDERS_COUNT
Provider Oracle
Para Oracle, instale a dependencia:
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_INBOUNDORACLE_QUERY_INBOUND_PRODUCTSORACLE_QUERY_STOCKORACLE_QUERY_PICKINGORACLE_QUERY_ORDERS_FLOW_BY_DATEORACLE_QUERY_BILLED_ORDERS_COUNT
As consultas padrao assumem tabelas:
inbound_tasksstock_daily_summarypicking_ordersorders
Voce pode sobrescrever qualquer consulta com:
ORACLE_QUERY_INBOUNDORACLE_QUERY_STOCKORACLE_QUERY_PICKINGORACLE_QUERY_ORDERS_FLOW_BY_DATEORACLE_QUERY_BILLED_ORDERS_COUNT
Entrada por CLI
npm run chat
Parâmetros opcionais:
npm run chat -- --tenant cliente-demo --user operador-01 --name "Carlos"
Edge Agent local (cliente)
Suba um processo separado no ambiente do cliente:
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-clientEDGE_CORE_URL=http://host-do-core:3000EDGE_AGENT_TENANT_ID=cliente-demoEDGE_AGENT_TOKEN(ouEDGE_AGENT_SHARED_TOKEN)EDGE_AGENT_HTTP_TIMEOUT_MSEDGE_AGENT_RECONNECT_DELAY_MSEDGE_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 /healthePOST /agent/querypara core conectar direto no cliente.
Comandos já suportados
recebimentoestoqueseparacaominhas filiaistrocar filialajuda
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.