639 lines
38 KiB
Markdown
639 lines
38 KiB
Markdown
# Runbook — Workflow de Agendamento no n8n (Z-PRO, Klingo, SysLife, ClinicAll)
|
||
|
||
> Registra problemas reais encontrados ao montar os workflows de
|
||
> agendamento no n8n (servidor `srvdevn8ngti` / `devn8n.gtigrupo.com.br`)
|
||
> e como foram resolvidos — para não repetir o mesmo processo de
|
||
> descoberta ao duplicar para uma nova clínica ou provider. As seções 1-13
|
||
> cobrem o histórico completo do **Z-PRO** (o único provider em produção
|
||
> real até agora, incluindo a migração do node comunitário pro MCP Server
|
||
> nativo do n8n). A seção 14 cobre os outros três providers — **Klingo**,
|
||
> **SysLife** e **ClinicAll** — todos construídos com o mesmo padrão de
|
||
> MCP Server nativo, mas ainda sem clínica real pra testar (ver cada
|
||
> subseção pras specs/planos completos e pendências específicas).
|
||
|
||
## 1. Node MCP: qual usar e como instalar
|
||
|
||
- O node **oficial** do n8n (`MCP Client Tool`, `@n8n/n8n-nodes-langchain`) só
|
||
conecta em MCP remoto via HTTP Streamable/SSE — **não** roda comando local
|
||
(stdio). Para rodar o `gti-scheduling-mcp` como subprocesso local, use o
|
||
pacote **comunitário** `n8n-nodes-mcp` (autor `nerding-io`).
|
||
- Instalação: **Settings → Community Nodes → Install a community node** →
|
||
`n8n-nodes-mcp`.
|
||
- Ao conectar como tool de um AI Agent, o **tipo do node no JSON do workflow**
|
||
precisa ser `n8n-nodes-mcp.mcpClientTool` (não `n8n-nodes-mcp.mcpClient`) —
|
||
é uma variante "Tool" que o n8n gera automaticamente para nodes marcados
|
||
como usáveis em AI Agents. Usar `mcpClient` na conexão `ai_tool` falha a
|
||
validação com "cannot output ai_tool connections".
|
||
- É necessário setar a variável de ambiente do **processo do n8n**
|
||
(não é configurável pela UI):
|
||
```
|
||
N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
|
||
```
|
||
Nesse servidor, o n8n roda via **systemd** (`n8n.service`), com
|
||
`EnvironmentFile=/opt/n8n.env`. A variável foi adicionada nesse arquivo,
|
||
seguido de `systemctl restart n8n`. (Se o n8n rodasse em Docker, seria em
|
||
`environment:` no `docker-compose.yml`.)
|
||
|
||
## 2. Credencial MCP (STDIO) — dois bugs reais
|
||
|
||
Tipo de credencial: `mcpClientApi` ("MCP Client (STDIO) API"), campos:
|
||
`command`, `args`, `environments`.
|
||
|
||
- **Bug 1 — `command: "node"` sozinho falhou** com "Connection closed". O
|
||
serviço systemd tem um `PATH` mais restrito que um shell interativo. Fix:
|
||
usar o **caminho absoluto** (`which node` no servidor → `/usr/bin/node`).
|
||
|
||
- **Bug 2 — o campo "Environments" é um `<input>` de uma linha só (mascarado
|
||
como senha) na UI do n8n.** O código real do node
|
||
(`nodes/McpClient/McpClient.node.ts` no pacote `n8n-nodes-mcp`) espera o
|
||
formato `NOME=VALOR` **separado por quebra de linha real** (`split('\n')`),
|
||
mas navegadores removem quebras de linha ao digitar OU colar em campos de
|
||
uma linha — então múltiplas variáveis nunca conseguem ser inseridas
|
||
corretamente pela UI (tentamos: uma por linha, vírgula, espaço — todas
|
||
colapsam a mesma forma, e a primeira variável acaba recebendo o resto do
|
||
texto como valor, quebrando a validação).
|
||
|
||
**Fix**: criar/recriar a credencial via **API REST do n8n**, que aceita o
|
||
JSON com `\n` de verdade (não passa pelo campo quebrado da UI):
|
||
|
||
```bash
|
||
# 1. Gerar uma API key: Settings → n8n API → Create an API key
|
||
# 2. DELETE na credencial antiga (se existir), depois POST na nova:
|
||
curl -X POST "https://devn8n.gtigrupo.com.br/api/v1/credentials" \
|
||
-H "X-N8N-API-KEY: <chave>" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"name": "Client (STDIO) account",
|
||
"type": "mcpClientApi",
|
||
"data": {
|
||
"command": "/usr/bin/node",
|
||
"args": "/opt/gti-scheduling-mcp/dist/index.js",
|
||
"environments": "SCHEDULING_PROVIDER=zpro\nZPRO_BASE_URL=https://...\nZPRO_API_TOKEN=...\nZPRO_API_ID=..."
|
||
}
|
||
}'
|
||
```
|
||
|
||
Sintoma quando esse bug está presente: o log do n8n
|
||
(`journalctl -u n8n -f`) mostra exatamente a mensagem de erro do
|
||
`gti-scheduling-mcp` (`Falha ao carregar a configuração: SCHEDULING_PROVIDER
|
||
deve ser "zpro" ou "klingo".`), seguida de `MCP error -32000: Connection
|
||
closed` — o processo sobe e cai na validação de config antes de completar o
|
||
handshake stdio.
|
||
|
||
## 3. Deploy do `gti-scheduling-mcp` no servidor do n8n
|
||
|
||
O `node dist/index.js` executado pela credencial precisa estar no **mesmo
|
||
host** onde o n8n roda (o node comunitário spawna o processo localmente).
|
||
Processo usado (sem repositório git configurado ainda neste projeto):
|
||
`tar` local (excluindo `node_modules`/`dist`) → `scp` pro servidor → `npm
|
||
install && npm run build` lá. Publicado em `/opt/gti-scheduling-mcp`.
|
||
|
||
## 4. Modelo de IA (OpenAI Chat Model node) — gotchas de configuração
|
||
|
||
Mesmo usando um provider "compatível com OpenAI" (ex.: Z.ai/GLM), o node
|
||
`OpenAI Chat Model` (`@n8n/n8n-nodes-langchain.lmChatOpenAi`, v1.3) tem
|
||
comportamento por padrão que quebra com endpoints não-OpenAI:
|
||
|
||
- **`responsesApiEnabled` é `true` por padrão** nessa versão — usa a
|
||
"Responses API" da OpenAI (`/responses`), um endpoint proprietário que
|
||
providers OpenAI-compatíveis geralmente não implementam (eles só têm
|
||
`/chat/completions`). Sintoma: `404 Not Found` /
|
||
`MODEL_NOT_FOUND`. **Fix**: setar `responsesApiEnabled: false`
|
||
explicitamente.
|
||
- **Campo `model`** é um `resourceLocator` — precisa de `{mode: "id", value:
|
||
"nome-do-modelo"}` explícito. Omitir `mode` pode fazer o n8n tentar listar
|
||
modelos via uma chamada de API (`GET /models`) que o provider alternativo
|
||
pode não suportar do mesmo jeito, gerando erro parecido com o anterior.
|
||
- **Base URL da credencial**: o campo correto na credencial `openAiApi` é
|
||
`url` (aparece como "Base URL" na UI, geralmente atrás de "Add Option").
|
||
|
||
## 5. Expressões em sub-nodes de IA (Memory/Tools) dentro do AI Agent
|
||
|
||
Dentro de nodes conectados via `ai_memory`/`ai_tool`/`ai_languageModel`
|
||
(sub-nodes do AI Agent), referenciar um node upstream explicitamente
|
||
(`={{ $('Normalizar entrada').item.json.sessionId }}`) falhou com "Key
|
||
parameter is empty" — mesmo com o dado presente no item que chegou no AI
|
||
Agent. **Fix**: usar `={{ $json.sessionId }}` (referência ao item atual, sem
|
||
nome de node) — é o padrão que o próprio n8n usa por padrão nesse campo, e
|
||
funciona porque sub-nodes de IA herdam o item do node pai (AI Agent) na
|
||
execução.
|
||
|
||
## 6. Contrato do webhook do Z-PRO/GChat (verificado, não documentado
|
||
oficialmente em lugar nenhum)
|
||
|
||
- Uma execução do workflow **por mensagem** do cliente — o Z-PRO não
|
||
enfileira mensagens seguintes se o workflow não responder imediatamente.
|
||
- Corpo recebido: `{step, entrada, number, contactName, protocol}` — `step`
|
||
deve ser `"process"`; `protocol` é o identificador do **atendimento**
|
||
(não do telefone).
|
||
- Resposta esperada: **texto puro**, nunca JSON — node `Respond to Webhook`
|
||
em modo `text`.
|
||
|
||
## 7. Z.ai/GLM como modelo temporário — limitação real de tool-calling
|
||
|
||
Usado como substituto temporário enquanto a chave da OpenAI não chegava.
|
||
Dois endpoints testados:
|
||
|
||
- **`https://api.z.ai/api/coding/paas/v4`** ("GLM Coding Plan"): funciona
|
||
bem para chat simples, mas é **restrito a ferramentas oficialmente
|
||
suportadas** (Claude Code, Goose, etc. — ver
|
||
`https://docs.z.ai/devpack/faq`). Ao anexar uma tool MCP genérica ao AI
|
||
Agent, o modelo passou a devolver respostas **vazias silenciosamente**
|
||
(sem erro) em qualquer mensagem, mesmo sem chamar a tool. Confirmado
|
||
desabilitando a tool: a conversa voltou a funcionar normalmente.
|
||
- **`https://api.z.ai/api/paas/v4/`** (endpoint geral, mesma conta): suporta
|
||
tool-calling arbitrário segundo a documentação, mas apresentou timeout
|
||
consistente nesta conta/plano (possivelmente saldo separado do "Coding
|
||
Plan").
|
||
- **Conclusão**: Z.ai/GLM não é uma substituição direta e confiável da
|
||
OpenAI para um AI Agent com tools neste projeto. Recomendado usar a chave
|
||
real da OpenAI (ou outro provider com suporte robusto e testado de
|
||
function-calling) assim que disponível — só trocar a credencial e o
|
||
`model`/`responsesApiEnabled` de volta, sem tocar no resto do workflow.
|
||
|
||
## 8. Tratamento de erro adicionado
|
||
|
||
`Normalizar entrada` e `AI Agent` têm `onError: "continueErrorOutput"`,
|
||
ambos roteados para um node `Set` ("Erro - Resposta padrão") com uma
|
||
mensagem fixa amigável, que alimenta o `Respond to Webhook`. Isso garante
|
||
que o Z-PRO **sempre** recebe alguma resposta, mesmo se a normalização
|
||
lançar uma exceção ou o AI Agent falhar (timeout, erro de modelo, etc.) —
|
||
testado com um timeout real do endpoint geral do Z.ai, confirmado
|
||
funcionando.
|
||
|
||
## 9. Bug crítico — `toolParameters` do node MCP nunca era preenchido pelo modelo
|
||
|
||
**Sintoma**: toda chamada de `agendar` falhava com
|
||
`MCP error -32602: Input validation error: Invalid arguments for tool
|
||
agendar: Required at clinicId, Required at startAt, Required at endAt,
|
||
Required at patient.` — mesmo com o prompt do sistema explicando o formato
|
||
esperado em detalhe.
|
||
|
||
**Causa raiz**: o node `n8n-nodes-mcp` (operação `executeTool`) tem um
|
||
campo próprio **`toolParameters`** (JSON, obrigatório, valor padrão
|
||
`'{}'`) que carrega os argumentos reais enviados à tool. Ele **não é
|
||
preenchido automaticamente** só por a tool estar conectada a um AI Agent —
|
||
é preciso configurá-lo explicitamente com o helper `$fromAI()` do n8n, que
|
||
é o mecanismo real usado para "deixar o modelo decidir este valor em cada
|
||
chamada". Sem isso, o campo fica travado no `{}` padrão, e nenhum valor
|
||
que o modelo "pensa" que está enviando chega de fato ao MCP.
|
||
|
||
**Fix**: configurar `toolParameters` assim (mistura valores fixos vindos
|
||
do próprio workflow com `$fromAI()` só para o que o modelo precisa mesmo
|
||
decidir):
|
||
|
||
```
|
||
={{ JSON.stringify({
|
||
clinicId: $('Normalizar entrada').item.json.clinicId,
|
||
startAt: $fromAI('startAt', 'Data e hora de início, ISO 8601 completo', 'string'),
|
||
endAt: $fromAI('endAt', 'Data e hora de término, ISO 8601 completo', 'string'),
|
||
patient: {
|
||
name: $fromAI('patientName', 'Nome completo confirmado do paciente', 'string'),
|
||
phone: $('Normalizar entrada').item.json.telefone
|
||
}
|
||
}) }}
|
||
```
|
||
|
||
## 10. Bug crítico — `systemMessage` do AI Agent nunca era avaliado como expressão
|
||
|
||
**Sintoma**: um agendamento de teste foi criado com o campo de telefone
|
||
contendo literalmente o texto `{{ $('Normalizar entrada').item.json.telefone
|
||
}}` em vez de um número de telefone real.
|
||
|
||
**Causa raiz**: o campo `parameters.options.systemMessage` do node AI Agent
|
||
só é avaliado como expressão do n8n se o **valor inteiro do campo** começar
|
||
com `=`. Sem esse prefixo, tudo — inclusive trechos `{{ ... }}` escritos
|
||
"como exemplo" dentro do texto — é enviado ao modelo como **texto literal**,
|
||
e o modelo, sem saber que aquilo deveria virar um valor real, às vezes
|
||
copia o placeholder ao pé da letra.
|
||
|
||
**Fix**: sempre prefixar o campo inteiro com `=` quando ele precisar
|
||
referenciar dados reais do workflow (`={{ $('Normalizar entrada')... }}`).
|
||
|
||
**Consequência que isso causou (transparência total)**: dois agendamentos
|
||
de teste fictícios ficaram duplicados no Z-PRO (`Consulta - TESTE-N8N
|
||
Paciente Final`, mesma data/hora) antes desse bug ser identificado — uma
|
||
chamada que o agente reportou como "erro" na verdade teve sucesso
|
||
silenciosamente no lado do Z-PRO. Os testes fictícios foram apagados
|
||
manualmente depois. Isso reforça por que a disciplina de "uma execução por
|
||
vez, conferir visualmente antes de tentar de novo" existe em todos os
|
||
testes reais deste projeto — mesmo seguindo ela, um bug de configuração
|
||
ainda pode gerar um resultado inesperado, e só a conferência visual pega
|
||
isso.
|
||
|
||
## 11. Bug — modelo não sabe a data de hoje
|
||
|
||
**Sintoma**: pedido de agendamento para "amanhã" resultou em uma data no
|
||
passado (2023) sendo enviada ao Z-PRO.
|
||
|
||
**Causa raiz**: modelos de linguagem não têm acesso à data/hora real do
|
||
momento da conversa — sem essa informação no prompt, o modelo "chuta" uma
|
||
data (geralmente relacionada à data de corte do treinamento).
|
||
|
||
**Fix**: incluir a data/hora atual no `systemMessage` via expressão do
|
||
próprio n8n:
|
||
```
|
||
A data de hoje é {{ $now.setZone('America/Sao_Paulo').toFormat('yyyy-MM-dd (cccc)') }}
|
||
```
|
||
|
||
## 12. Bug — horário criado 3h adiantado/atrasado (fuso horário)
|
||
|
||
**Sintoma**: cliente pediu "sexta às 10h", o agendamento foi criado em produção
|
||
às 7h — exatamente 3h a menos (o offset de America/Sao_Paulo, UTC-3).
|
||
|
||
**Causa raiz**: o exemplo de formato dado ao modelo usava sufixo `Z` (UTC)
|
||
— ex. `2026-04-20T18:00:00.000Z`. O modelo tratou o horário local que o
|
||
cliente pediu (10h) como se já fosse UTC, sem converter, e o Z-PRO (que
|
||
aparentemente armazena/exibe em UTC) mostrou 10h UTC como 7h local.
|
||
|
||
**Fix**: instruir o modelo a **nunca usar `Z`/UTC** e sempre escrever o
|
||
mesmo número de hora que o cliente pediu, com offset `-03:00` explícito
|
||
(ex.: cliente pediu 10h → `2026-08-14T10:00:00-03:00`). Isso evita
|
||
qualquer aritmética de fuso horário pelo modelo — ele só copia o número
|
||
que o cliente já disse, sem calcular nada. Ajustado tanto na descrição do
|
||
`$fromAI('startAt', ...)`/`$fromAI('endAt', ...)` quanto numa regra
|
||
explícita no `systemMessage`.
|
||
|
||
**Se isso acontecer nas próximas clínicas**: o offset `-03:00` só está
|
||
certo para fuso de Brasília. Se uma clínica futura estiver em outro fuso,
|
||
ajustar o offset (e o `$now.setZone(...)` do prompt) de acordo.
|
||
|
||
## 13. Migração — MCP Server nativo no n8n (substitui o node comunitário, só Z-PRO)
|
||
|
||
**Data:** 11/08/2026. Ver design completo em
|
||
`docs/superpowers/specs/2026-08-10-mcp-server-nativo-n8n-zpro-design.md` e
|
||
plano em `docs/superpowers/plans/2026-08-10-mcp-server-nativo-n8n-zpro.md`.
|
||
|
||
**Motivação**: o node comunitário (`n8n-nodes-mcp`, seções 1–2 acima) exigiu
|
||
depuração pesada (bug de credencial STDIO, variável de ambiente do processo,
|
||
`toolParameters`/`$fromAI` não documentado) e não é editável visualmente —
|
||
qualquer manutenção de memória/prompt/tool exige mexer em processo externo.
|
||
A alternativa: reconstruir o mesmo "MCP Server" **inteiramente dentro do
|
||
n8n**, com nodes **oficiais** (`MCP Server Trigger` + `Call n8n Sub-Workflow
|
||
Tool` do lado servidor, `MCP Client Tool` oficial via HTTP Streamable do
|
||
lado cliente) — sem processo Node.js externo, sem pacote comunitário.
|
||
|
||
**Arquitetura nova** (3 workflows adicionais, o workflow de agendamento
|
||
existente só trocou o node MCP):
|
||
|
||
- `MCP Server - ZPRO` (`qjSaKllvneBIZ7mC`) — `MCP Server Trigger`
|
||
(`path: mcp-server-zpro-clinica-exemplo`) + 2 `Call n8n Sub-Workflow Tool`
|
||
(`agendar`, `consultar_horarios`).
|
||
- `MCP Server - ZPRO - agendar` (`5ZEOqTOk4Xex6jUQ`) — porta fiel da lógica
|
||
já testada em `gti-scheduling-mcp` (`zpro.mapper.ts`/`zpro.adapter.ts`/
|
||
`zpro.errors.ts`) para nodes `Set`/`Code`/`HTTP Request`: monta o corpo,
|
||
chama `POST /v2/api/external/{apiId}/appointment/create` com
|
||
`neverError: true` + `fullResponse: true` (captura manual de status/body,
|
||
nunca deixa o n8n abortar em 4xx/5xx), classifica o resultado (qualquer
|
||
2xx é sucesso operacional — mesma proteção contra duplicidade de sempre).
|
||
- `MCP Server - ZPRO - consultar_horarios` (`yfKkhSTDZaIYCPqP`) — sempre
|
||
retorna `NOT_SUPPORTED` (mesma decisão de produto de sempre, Z-PRO não
|
||
expõe esse endpoint).
|
||
- No workflow de agendamento (`yG2mi6ECBpPv4Hmn`): saiu o node comunitário
|
||
`MCP - gti-scheduling-mcp` (`n8n-nodes-mcp.mcpClientTool`, stdio), entrou
|
||
`MCP - ZPRO (nativo n8n)` (`@n8n/n8n-nodes-langchain.mcpClientTool`,
|
||
`serverTransport: httpStreamable`, apontando pra URL do `MCP Server
|
||
Trigger` acima). Nada mais no workflow mudou (prompt, memória, fallback de
|
||
erro).
|
||
|
||
**Gotcha 1 — todos os workflows envolvidos precisam estar `active: true`**,
|
||
inclusive os dois sub-workflows (`agendar`/`consultar_horarios`) chamados
|
||
via `Call n8n Sub-Workflow Tool` — não só o servidor MCP e o workflow de
|
||
agendamento. O erro sintoma é `"Workflow is not active and cannot be
|
||
executed."` no node `MCP Client Tool` do lado do AI Agent.
|
||
|
||
**Gotcha 2 — diagnóstico via chamada MCP crua (`curl`), sem depender do AI
|
||
Agent**: pra isolar se o problema está na cadeia de sub-workflows ou no
|
||
transporte/roteamento do AI Agent, dá pra chamar o endpoint MCP diretamente:
|
||
|
||
```bash
|
||
# 1. Handshake — captura o mcp-session-id no header da resposta
|
||
curl -s -i "https://devn8n.gtigrupo.com.br/mcp/mcp-server-zpro-clinica-exemplo" -X POST \
|
||
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
|
||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}'
|
||
|
||
# 2. tools/call — usa o mcp-session-id do passo 1
|
||
curl -s "https://devn8n.gtigrupo.com.br/mcp/mcp-server-zpro-clinica-exemplo" -X POST \
|
||
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
|
||
-H "mcp-session-id: <session-id>" \
|
||
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"agendar","arguments":{"startAt":"...","endAt":"...","patientName":"...","patientPhone":"..."}}}'
|
||
```
|
||
|
||
Isso foi o que revelou que a cadeia de sub-workflows já funcionava (chegava
|
||
até o Z-PRO de verdade) enquanto o caminho via AI Agent ainda reportava
|
||
"not active" — depois de reativar os 3 workflows envolvidos e aguardar um
|
||
pouco, o caminho via AI Agent voltou a funcionar igual ao teste direto
|
||
(não ficou claro se foi só o tempo/cache do n8n resolvendo, ou a
|
||
reativação — mas o sintoma não voltou a aparecer depois disso).
|
||
|
||
**Gotcha 3 — causa raiz real do primeiro `403` em produção**: não foi
|
||
formato de header nem token expirado — foi **ApiID e token de integrações
|
||
diferentes**. A clínica tem mais de uma integração externa cadastrada no
|
||
Z-PRO (ex.: uma pro canal de WhatsApp "Canal QR", outra pro agendamento),
|
||
cada uma com seu próprio `ApiID` (segmento da URL,
|
||
`/v2/api/external/{ApiID}/...`) **e** seu próprio Bearer token — não são
|
||
intercambiáveis. O node `Config ZPRO` precisa ter o `apiId` da **mesma**
|
||
integração de onde veio o token salvo na credencial `httpHeaderAuth`. Os
|
||
sintomas por tipo de descasamento, na ordem em que apareceram:
|
||
|
||
| Corpo de erro do Z-PRO (403) | Causa |
|
||
| --- | --- |
|
||
| `{"error": "ERR_SESSION_NOT_AUTH_TOKEN"}` | token nunca usado/nunca válido pra nenhuma integração |
|
||
| `{"error": "Invalid token."}` | token de uma integração, `apiId` de outra (ou vice-versa) |
|
||
| (sucesso, `200`) | `apiId` e token da **mesma** integração |
|
||
|
||
Ao duplicar este padrão pra uma nova clínica: sempre confirmar, na tela de
|
||
integrações do Z-PRO daquela clínica, que o `ApiID` copiado pra URL e o
|
||
Bearer token copiado pra credencial vêm da **mesma linha/integração**.
|
||
|
||
**Teste real de validação (11/08/2026)**: um agendamento fictício foi
|
||
criado de ponta a ponta pelo caminho de produção completo (webhook →
|
||
Normalizar entrada → AI Agent → `MCP - ZPRO (nativo n8n)` → `MCP Server -
|
||
ZPRO` → sub-workflow `agendar` → Z-PRO), com `startAt` local correto
|
||
(`2026-08-12T11:00:00-03:00`, sem bug de timezone), retorno `200` do
|
||
Z-PRO, `id` de agendamento real mapeado, e resposta final coerente pro
|
||
cliente. Conferido visualmente no Z-PRO e apagado logo em seguida.
|
||
|
||
**Resultado**: o node comunitário `n8n-nodes-mcp` não é mais necessário
|
||
pro Z-PRO (segue instalado no servidor, mas sem uso nesse workflow). As
|
||
seções 1–2 deste runbook (instalação do node comunitário, bugs da
|
||
credencial STDIO) ficam como **referência histórica** — não se aplicam
|
||
mais a este workflow, mas continuam relevantes se algum dia o padrão nativo
|
||
não for suficiente e for necessário voltar ao processo externo
|
||
(`gti-scheduling-mcp`, monorepo TypeScript pausado).
|
||
|
||
## Status final — Z-PRO (11/08/2026)
|
||
|
||
Testado de ponta a ponta com sucesso, tanto com a chave real da OpenAI
|
||
(`gpt-4o-mini`) quanto com o MCP Server nativo do n8n (seção 13): telefone
|
||
correto, data correta, um único agendamento por teste, confirmado
|
||
visualmente no Z-PRO. Workflow **ativo em produção** (`yG2mi6ECBpPv4Hmn`),
|
||
junto com os 3 workflows do MCP Server nativo (`qjSaKllvneBIZ7mC`,
|
||
`5ZEOqTOk4Xex6jUQ`, `yfKkhSTDZaIYCPqP`) — todos precisam permanecer
|
||
`active: true`. Desde a seção 15, o `systemMessage` também depende de um
|
||
Google Doc externo (leitura pública, `docId` fixo em "Config Clínica") —
|
||
se o Doc for despublicado ou o `docId` mudar, o atendimento continua
|
||
funcionando (fallback seguro), só perde a personalização da clínica.
|
||
|
||
## 14. Providers adicionais via MCP Server nativo (Klingo, SysLife, ClinicAll)
|
||
|
||
Mesmo padrão arquitetural validado no Z-PRO (seção 13) — `MCP Server
|
||
Trigger` + `Call n8n Sub-Workflow Tool`, sem processo Node.js externo —
|
||
aplicado a mais três providers nesta sessão (11/08/2026). Nenhum dos três
|
||
tem clínica real disponível ainda: todos os workflows foram criados e
|
||
validados estruturalmente (`n8n_validate_workflow`, 0 erros), mas ficam
|
||
**inativos**, sem nenhuma chamada HTTP real feita, e sem workflow de
|
||
agente cliente (AI Agent + prompt) — isso fica para quando houver uma
|
||
clínica real e um canal de mensagens definido para cada um.
|
||
|
||
### 14.1 - Klingo
|
||
|
||
- Spec: `docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-klingo-design.md`
|
||
- Plano: `docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-klingo.md`
|
||
- Tools de apoio (spec/plano adicional): `docs/superpowers/specs/2026-08-11-klingo-tools-de-apoio-design.md` / `docs/superpowers/plans/2026-08-11-klingo-tools-de-apoio.md`
|
||
- Fonte: `gti-scheduling-mcp/docs/klingo-openapi.json` (Swagger público) — único provider dos 4 com adapter TypeScript prévio testado (`KlingoAdapter`), portado fielmente.
|
||
- **7 tools** no servidor `MCP Server - Klingo` (`3eiFEnfOTLnutP78`): `agendar` (`0PXwQZ3EXJUtuRgN`), `consultar_horarios` (`7ikOIk6mdoBwClC3`), `especialidade` (`VhDRpgVxC7Bq9ZMg`), `profissional` (`27HOTfmlTH0r5UiX`), `convenio` (`v8VGt7Du0zfbc2FT`), `unidade` (`Sms5egwOtmZkeIkw`), `preco` (`tSq0p8fRxS39CSWE`).
|
||
- **Diferença de autenticação em relação ao Z-PRO**: dois estágios — `X-APP-TOKEN` estático (credencial `Klingo APP Token - clínica pendente`, `lwmRgDYe7W1zrKP5`) para identificar o paciente e consultar catálogo, e um **Bearer de sessão dinâmico** (obtido na resposta de `/paciente/identificar`, nunca armazenado como credential) só para `/agenda/reservar`.
|
||
- **Limitação conhecida, sem solução**: não existe endpoint público de listagem de procedimentos no Klingo — `specialtyId`/`procedimento` (exigido por `agendar` e opcionalmente por `preco`) não tem nenhuma tool que resolva. `consultar_horarios` sempre retorna `SLOT_MAPPING_UNAVAILABLE` pelo mesmo motivo já registrado no `KlingoAdapter` original (formato de horário individual não documentado no Swagger público).
|
||
|
||
### 14.2 - SysLife
|
||
|
||
- Spec: `docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-syslife-design.md`
|
||
- Plano: `docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-syslife.md`
|
||
- Fonte: `Manual WS SysLife V3.4.pdf` (fornecido pelo usuário) — sem adapter TypeScript prévio, único provider cuja fonte de verdade é só o manual.
|
||
- **7 tools** no servidor `MCP Server - SysLife` (`TYu78UqIUr2yCBHK`): `especialidade`, `medico`, `convenio`, `paciente`, `procedimento`, `consultar_horarios` (GET `/Horario`), `agendar` (PUT `/Agenda`).
|
||
- **Autenticação**: Basic Auth fixo do WebService (usuário/senha documentados literalmente no manual, não por clínica) — credencial `SysLife Basic Auth - clínica pendente` (`u8XXJ6wyrKuGJp4Y`), reaproveitada nas 7 tools. Só o `baseUrl` (IP:porta) muda por clínica.
|
||
- **Ponto forte deste provider**: o `Horario` GET retorna `nuConsulta`, que corresponde ao `NumeroConsulta` esperado no `Agenda` PUT — ciclo consultar→agendar fecha de verdade, diferente do Klingo.
|
||
|
||
### 14.3 - ClinicAll
|
||
|
||
- Spec: `docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-clinicall-design.md`
|
||
- Plano: `docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-clinicall.md`
|
||
- Fonte: `docs/API Clinicall.pdf` ("Documentação da API de Integração com ClinicAll para Módulo de Agendamento", v1.1.16, Focus Tecnologia em Sistemas) — a documentação mais completa e explícita dos 4 providers (campos obrigatórios, exemplos reais de request/response).
|
||
- **8 tools** no servidor `MCP Server - ClinicAll` (`igaKjoHcPNYRAxPS`): `unidade`, `especialidade`, `convenio`, `profissional`, `procedimento`, `paciente`, `consultar_horarios` (GET `/disponibilidade/{idUnidade}/{idProfissional}`), `agendar` (PUT `/agendamento`).
|
||
- **Duas pendências reais, sem credencial nem verbo assumido por invenção**:
|
||
1. **Autenticação desconhecida** — o manual não documenta nenhum header/token nas chamadas de `TEntityController`/`TPatientController`/`TCalendarController`. Nenhuma credencial foi criada; todos os nodes HTTP usam `authentication: none`. Existe um `TAuthenticateController/login` separado, mas não está claro se é pré-requisito nem que token ele gera.
|
||
2. **Verbo HTTP do agendamento não confirmado** — `/agendamento` aceita GET/POST/PUT/DELETE sem o manual mapear qual verbo concilia um `idAgenda` livre já existente (o fluxo real é atualizar um registro pré-existente, não criar do zero). Assumido `PUT`; se não funcionar contra uma clínica real, tentar `POST` antes de qualquer outra hipótese.
|
||
- **Ponto forte deste provider**: `/disponibilidade` retorna `idAgenda` de cada horário livre, e esse mesmo `idAgenda` é usado para "conciliar" o agendamento via `/agendamento` — mesmo padrão de ciclo fechado do SysLife, com a vantagem de já vir com nome do profissional/especialidade/unidade no mesmo payload.
|
||
|
||
### 14.4 - Padrão comum aos três (e ao Z-PRO)
|
||
|
||
Reforçando o que já vale desde a seção 13, agora confirmado em mais três implementações:
|
||
|
||
- Cada provider = 1 workflow servidor (`MCP Server Trigger` + N `Call n8n Sub-Workflow Tool`) + N sub-workflows (1 por tool).
|
||
- Sub-workflows chamados via `Call n8n Sub-Workflow Tool` **precisam estar `active: true`** para funcionar no caminho real do AI Agent (gotcha 1 da seção 13) — nenhum dos três providers desta seção foi ativado ainda, então isso só será re-confirmado quando houver teste real.
|
||
- HTTP Request nodes sempre com `neverError: true` + `fullResponse: true` — captura manual de status/corpo, nunca deixa o n8n abortar em 4xx/5xx.
|
||
- Tools de escrita (`agendar`) sempre com a regra "qualquer HTTP 2xx é sucesso operacional" — proteção contra duplicidade, nunca reportar erro numa operação que a API já aceitou.
|
||
- Tools de leitura nunca reformatam o corpo de sucesso — devolvem cru, sem inventar um envelope que não foi confirmado por um exemplo real de resposta.
|
||
- Mapeamento de parâmetros `$fromAI(...)` nas tools do servidor é sempre um passo manual na UI (resourceMapper não é confiável via API do n8n) — feito pelo usuário, não pela automação.
|
||
- Nenhuma credencial é criada automaticamente pela automação — sempre pré-requisito manual do usuário na UI do n8n, mesmo quando o valor é só um placeholder de rascunho.
|
||
|
||
## 15. Configuração da IA via Google Docs — autonomia do gestor (só Z-PRO)
|
||
|
||
**Data:** 11/08/2026. Ver design completo em
|
||
`docs/superpowers/specs/2026-08-11-config-ia-google-docs-design.md` e
|
||
plano em `docs/superpowers/plans/2026-08-11-config-ia-google-docs.md`.
|
||
|
||
**Motivação**: o `systemMessage` do AI Agent era texto fixo hardcoded no
|
||
workflow. Com o número de clientes crescendo (1 workflow de agente por
|
||
clínica), editar manualmente o prompt de cada um toda vez que um gestor
|
||
quisesse ajustar tom de voz, saudação, horário de funcionamento ou uma
|
||
regra específica não escala. Solução: um Google Doc por clínica,
|
||
público só para leitura (sem exigir credencial no n8n), editável
|
||
diretamente pela conta do gestor — o `systemMessage` passa a concatenar
|
||
as regras fixas de segurança (inalteradas) com o conteúdo do Doc.
|
||
|
||
**Arquitetura implementada** (4 nodes novos no workflow
|
||
`yG2mi6ECBpPv4Hmn`):
|
||
|
||
```
|
||
"É step=process?" → Config Clínica → Buscar config da clínica ─┬→ Doc OK ──────┐
|
||
(docId fixo) (GET export?format=txt, └→ Doc falhou ─┴→ AI Agent
|
||
sem credencial)
|
||
```
|
||
|
||
- **Config Clínica** (`Set`): `docId` do Google Doc desta clínica,
|
||
fixo por cópia do workflow (mesmo padrão de `clinicId`/`baseUrl` dos
|
||
outros providers) — setado uma vez na instalação, nunca mais mexido
|
||
depois disso.
|
||
- **Buscar config da clínica** (`HTTP Request`): `GET
|
||
https://docs.google.com/document/d/{docId}/export?format=txt`, sem
|
||
autenticação (Doc público só para leitura), `onError:
|
||
continueErrorOutput`.
|
||
- **Doc OK** / **Doc falhou** (`Set`): reconstroem o item explicitamente
|
||
a partir de `$('Config Clínica').item.json` + `docContent` (do corpo
|
||
da resposta ou vazio, respectivamente).
|
||
- **`systemMessage` do AI Agent**: ganhou uma seção final
|
||
`"Instruções específicas desta clínica:\n{{ $json.docContent }}"`,
|
||
depois de todas as 7 regras fixas originais (que continuam idênticas,
|
||
sem alteração de texto).
|
||
|
||
**Bug real encontrado e corrigido durante a implementação — perda de
|
||
contexto depois de um `HTTP Request`**: o node `HTTP Request` **substitui
|
||
o `$json` inteiro** pelo corpo da resposta, descartando qualquer campo
|
||
que viesse do item anterior (`sessionId`, `mensagem`, `telefone`,
|
||
`nomeContato`, `clinicId`). Isso quebrou a "Memória da Sessão"
|
||
(`Key parameter is empty`, porque `sessionKey` depende de
|
||
`$json.sessionId`) logo na primeira tentativa. Mesma classe de bug já
|
||
registrada na seção 5 deste runbook (expressões em sub-nodes de IA), só
|
||
que desta vez a causa foi upstream do AI Agent, não dentro dele.
|
||
|
||
**Fix**: os nodes `Doc OK`/`Doc falhou` **não podem confiar em
|
||
`includeOtherFields`** quando há um `HTTP Request` no meio do caminho —
|
||
precisam reconstruir cada campo explicitamente referenciando o node
|
||
anterior ao HTTP Request por nome
|
||
(`={{ $('Config Clínica').item.json.sessionId }}`, etc.), exatamente
|
||
como já era feito em `Classificar resultado` nos sub-workflows de
|
||
`agendar` dos MCP Servers (seção 13/14) — a lição "depois de um HTTP
|
||
Request, resgate os campos anteriores pelo nome do node, nunca confie
|
||
em passthrough automático" vale pra qualquer node no meio do fluxo
|
||
principal, não só dentro de sub-workflows de tool.
|
||
|
||
**Teste real de validação (11/08/2026)**, com um Google Doc de teste
|
||
contendo a regra fictícia `"não atendemos às segundas-feiras"`:
|
||
|
||
1. Conteúdo do Doc apareceu naturalmente na resposta do agente
|
||
(pergunta genérica sobre dias de atendimento).
|
||
2. Regras fixas continuaram valendo (agente pediu confirmação do nome
|
||
completo antes de agendar).
|
||
3. Caminho de falha testado de propósito (`docId` inválido) — Google
|
||
Docs respondeu `404`, capturado pelo `onError: continueErrorOutput`,
|
||
roteado pro branch `Doc falhou`, atendimento **não quebrou**
|
||
(resposta normal, só sem a personalização da clínica).
|
||
4. `docId` revertido pro valor real de teste depois da validação.
|
||
|
||
**Resultado**: workflow ativo em produção com a configuração de negócio
|
||
já externalizada para o Google Doc de teste
|
||
(`1NmYRIoF7yD_B6N160_6Bpfv4NyJiryTTO2LFr9xNlYY`). Falta trocar pelo Doc
|
||
real de produção da clínica exemplo quando o gestor tiver o conteúdo
|
||
definitivo. Padrão pronto pra replicar em Klingo/SysLife/ClinicAll
|
||
quando esses tiverem workflow de agente.
|
||
|
||
## 16. Memória da sessão via Postgres (substitui o buffer em RAM, só Z-PRO)
|
||
|
||
**Data:** 11/08/2026. Ver design completo em
|
||
`docs/superpowers/specs/2026-08-11-memoria-sessao-postgres-design.md` e
|
||
plano em `docs/superpowers/plans/2026-08-11-memoria-sessao-postgres.md`.
|
||
|
||
**Motivação**: o node `Memória da Sessão` (`memoryBufferWindow`/"Simple
|
||
Memory") guardava o histórico da conversa só em RAM do processo do
|
||
n8n — não sobrevive a um reinício (deploy, crash, atualização) e não
|
||
funcionaria em setups com múltiplos workers (queue mode). Trocado pelo
|
||
node oficial `Postgres Chat Memory`, que persiste o histórico num banco.
|
||
|
||
**Infraestrutura**: o usuário provisionou um **container Postgres**, com
|
||
**um banco por cliente/clínica** dentro dele (mesma filosofia de
|
||
isolamento já usada pra `docId`, `baseUrl`/`apiId` dos MCP Servers).
|
||
Credencial no n8n: `Postgres Agendamento - (clínica exemplo)`
|
||
(`xq3QAN3MZSOCVgdY`), apontando pro banco do Z-PRO. Cada cliente futuro
|
||
terá sua própria credencial, apontando pro banco dele no mesmo
|
||
container.
|
||
|
||
**Troca implementada** (operação atômica — adicionar + conectar +
|
||
remover, mesma lição da seção 15 sobre nós desconectados): saiu
|
||
`Memória da Sessão` (`memoryBufferWindow`), entrou `Memória da Sessão
|
||
(Postgres)` (`@n8n/n8n-nodes-langchain.memoryPostgresChat`, v1.4), com
|
||
os mesmos parâmetros de sessão de sempre (`sessionKey: ={{
|
||
$json.sessionId }}`, `contextWindowLength: 20`). A tabela
|
||
`n8n_chat_histories` foi criada **automaticamente pelo node** na
|
||
primeira execução — sem setup manual de schema.
|
||
|
||
**Sem migração de histórico**: o histórico anterior só existia em RAM,
|
||
não havia nada a migrar — mesmo efeito que já acontecia a cada reinício
|
||
do n8n, só que agora deixa de acontecer daqui pra frente.
|
||
|
||
**Teste real de validação (11/08/2026)**: duas mensagens enviadas em
|
||
sequência ao webhook de produção, mesmo `protocol`
|
||
(`teste-memoria-postgres-01`) — primeira mensagem informando um nome
|
||
fictício, segunda perguntando "qual nome você anotou". O agente
|
||
respondeu corretamente com o nome da primeira mensagem, confirmando que
|
||
o contexto persistiu **entre duas execuções separadas do workflow**
|
||
(prova de que não é mais RAM do processo, já que cada chamada webhook é
|
||
uma execução isolada). Confirmação direta via `SELECT` na tabela
|
||
`n8n_chat_histories` não foi feita (sem necessidade de dar acesso ao
|
||
banco pra automação — a prova conversacional já foi considerada
|
||
suficiente).
|
||
|
||
**Resultado**: workflow ativo em produção com memória de sessão
|
||
persistida em Postgres. Padrão pronto pra replicar em
|
||
Klingo/SysLife/ClinicAll quando esses tiverem workflow de agente — cada
|
||
um com seu próprio banco dentro do mesmo container e sua própria
|
||
credencial.
|
||
|
||
## 17. MCP Server standalone de Mensageria (Z-PRO) — reusável por qualquer provider
|
||
|
||
**Data:** 11/08/2026. Ver design completo em
|
||
`docs/superpowers/specs/2026-08-11-mcp-server-mensageria-zpro-design.md`
|
||
e plano em
|
||
`docs/superpowers/plans/2026-08-11-mcp-server-mensageria-zpro.md`.
|
||
|
||
**Motivação**: a feature de "confirmação de agenda" (Klingo) precisa
|
||
disparar mensagens de WhatsApp de forma proativa — capacidade que ainda
|
||
não existia no projeto (tudo até então era resposta reativa a webhook).
|
||
Decisão explícita do usuário: em vez de acoplar o envio ao MCP de
|
||
agendamento de um provider específico, construir um **MCP Server
|
||
standalone só de mensageria**, já que diferentes clientes usam canais
|
||
diferentes (WABA oficial, não oficial, híbrido) — qualquer workflow
|
||
futuro (confirmação, campanha, o que for, de qualquer provider) pode
|
||
chamar essas tools sem depender de qual sistema de agendamento o
|
||
cliente usa.
|
||
|
||
**Escopo**: 4 tools, reaproveitando a API de mensageria do Z-PRO (mesmo
|
||
host `apigchat.gtigrupo.com.br`, endpoints diferentes dos de
|
||
agendamento):
|
||
|
||
| Tool | Endpoint | Uso |
|
||
| --- | --- | --- |
|
||
| `enviar_texto` | `POST /v2/api/external/{ApiID}/` | Texto simples — canal não oficial ou dentro da janela de 24h do WABA oficial |
|
||
| `enviar_template` | `POST .../template` | Template WABA aprovado, sem parâmetros dinâmicos |
|
||
| `enviar_template_com_parametros` | `POST .../templateBody` | Template WABA aprovado, com parâmetros dinâmicos no corpo |
|
||
| `enviar_template_marketing` | `POST .../templateMarketingBody` | Template WABA de marketing, com componente de header (ex.: imagem) |
|
||
|
||
**Arquitetura**: mesmo padrão de sempre — `MCP Server - Mensageria ZPRO`
|
||
(`WsL4RlSD0hZ34gpB`) + 4 sub-workflows (`enviar_texto`
|
||
`Y4ndG7GuIUrmVRxV`, `enviar_template` `E9lsmu8mlgzKFemk`,
|
||
`enviar_template_com_parametros` `8zj1V7yXOrdCAWVq`,
|
||
`enviar_template_marketing` `WYakJ2GmV6fNW2iq`). ApiID confirmado pelo
|
||
usuário como o mesmo já validado pro agendamento
|
||
(`ab2d51db-54e3-48c2-8b97-114a20340809`, credencial `ZPRO API Token -
|
||
Canal QR - MC AGENDAMENTO`).
|
||
|
||
**Bug real — `$fromAI` com `==` duplicado no início da expressão**: ao
|
||
mapear manualmente os campos de "Workflow Inputs" nos 4 nodes
|
||
`Call n8n Sub-Workflow Tool` do servidor, os valores foram salvos como
|
||
`"=={{ $fromAI(...) }}"` (dois `=` em vez de um) — a expressão nunca era
|
||
avaliada, então todo argumento chegava `null` no sub-workflow e a API do
|
||
Z-PRO rejeitava com 400 (`body must be a string type... null`).
|
||
**Fix**: reescrever os campos com um único `=` no início
|
||
(`"={{ $fromAI(...) }}"`), via `patchNodeField`/`updateNode`. Depois de
|
||
corrigir, apareceu um segundo erro (`Required → at isClosed`): o
|
||
`$fromAI` do campo opcional `isClosed` (boolean) não tinha valor
|
||
padrão, então o schema da tool o tratava como obrigatório. **Fix**:
|
||
adicionar o 4º argumento de default ao `$fromAI` (ex.:
|
||
`$fromAI('isClosed', '...', 'boolean', false)`), mesma correção
|
||
aplicada ao `headerImageUrl` opcional do template de marketing (default
|
||
`''`). Lição nova: **sempre conferir visualmente o texto final salvo no
|
||
campo depois de colar uma expressão `$fromAI` na UI** — um `=` a mais
|
||
não dá erro de validação estrutural do workflow, só falha silenciosamente
|
||
em runtime com campos `null`.
|
||
|
||
**Teste real de validação (11/08/2026)**: chamada `tools/call` direta
|
||
via MCP (raw, sem AI Agent) pro tool `enviar_texto`, mandando uma
|
||
mensagem de teste pro número pessoal do usuário
|
||
(`5575999091780`, autorização explícita dada em chat). Resultado:
|
||
`{"success": true, "data": {"message": "Message sent successfully",
|
||
"ticketId": 461590}}` — mensagem confirmada recebida no celular de
|
||
teste. Tools de template (`enviar_template`,
|
||
`enviar_template_com_parametros`, `enviar_template_marketing`) ainda
|
||
**não testadas de verdade** — precisam de um nome de template WABA
|
||
real aprovado, não disponível no momento.
|
||
|
||
**Fora de escopo desta rodada**: envio de mídia (multipart/URL/Base64),
|
||
busca de mensagem por `wamid`, e a própria feature de "confirmação de
|
||
agenda" do Klingo (que motivou a criação deste MCP) — fica pro usuário
|
||
montar depois, atribuindo estas tools no workflow que ele decidir.
|
||
|
||
**Resultado**: MCP Server de mensageria standalone, validado e testado
|
||
com envio real, pronto para ser consumido por qualquer workflow futuro
|
||
(confirmação de agenda ou outro), de qualquer provider.
|