Skip to main content
Glama
raiffhigor

Harpia Omnichannel MCP

by raiffhigor

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.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:

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):

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:

  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:

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:

[
  {
    "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

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:

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:

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

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-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.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/raiffhigor/harpia-whatsapp-mcp-production'

If you have feedback or need assistance with the MCP directory API, please join our Discord server