- Seção 13 do runbook: arquitetura nova (MCP Server Trigger + Call n8n Sub-Workflow Tool, sem node comunitário/processo externo pro Z-PRO) - Documenta os 2 gotchas reais de ativação e a causa raiz do 403 em produção (ApiID e token vêm de integrações diferentes no Z-PRO, não é bug de formato de header nem token expirado) - Teste real de ponta a ponta confirmado (webhook -> AI Agent -> MCP nativo -> Z-PRO), agendamento fictício criado e apagado - Status final atualizado: workflow + 3 sub-workflows do MCP nativo ativos em produção Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
20 KiB
Runbook — Workflow de Agendamento no n8n (GChat/Z-PRO + gti-scheduling-mcp)
Registra problemas reais encontrados ao montar o workflow
Agendamento - GChat - Z-PRO (clínica exemplo)(idyG2mi6ECBpPv4Hmn, servidorsrvdevn8ngti/devn8n.gtigrupo.com.br), e como foram resolvidos — para não repetir o mesmo processo de descoberta ao duplicar este workflow para uma nova clínica. Ver tambémdocs/superpowers/specs/2026-08-10-n8n-agendamento-workflow-design.md(design) edocs/superpowers/plans/2026-08-10-n8n-agendamento-workflow.md(plano).
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 ogti-scheduling-mcpcomo subprocesso local, use o pacote comunitárion8n-nodes-mcp(autornerding-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ãon8n-nodes-mcp.mcpClient) — é uma variante "Tool" que o n8n gera automaticamente para nodes marcados como usáveis em AI Agents. UsarmcpClientna conexãoai_toolfalha 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):
Nesse servidor, o n8n roda via systemd (
N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=truen8n.service), comEnvironmentFile=/opt/n8n.env. A variável foi adicionada nesse arquivo, seguido desystemctl restart n8n. (Se o n8n rodasse em Docker, seria emenvironment:nodocker-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 umPATHmais restrito que um shell interativo. Fix: usar o caminho absoluto (which nodeno 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.tsno pacoten8n-nodes-mcp) espera o formatoNOME=VALORseparado 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
\nde verdade (não passa pelo campo quebrado da UI):# 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 dogti-scheduling-mcp(Falha ao carregar a configuração: SCHEDULING_PROVIDER deve ser "zpro" ou "klingo".), seguida deMCP 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étruepor 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: setarresponsesApiEnabled: falseexplicitamente.- Campo
modelé umresourceLocator— precisa de{mode: "id", value: "nome-do-modelo"}explícito. Omitirmodepode 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}—stepdeve ser"process";protocolé o identificador do atendimento (não do telefone). - Resposta esperada: texto puro, nunca JSON — node
Respond to Webhookem modotext.
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. — verhttps://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/responsesApiEnabledde 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) + 2Call n8n Sub-Workflow Tool(agendar,consultar_horarios).MCP Server - ZPRO - agendar(5ZEOqTOk4Xex6jUQ) — porta fiel da lógica já testada emgti-scheduling-mcp(zpro.mapper.ts/zpro.adapter.ts/zpro.errors.ts) para nodesSet/Code/HTTP Request: monta o corpo, chamaPOST /v2/api/external/{apiId}/appointment/createcomneverError: 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 retornaNOT_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árioMCP - gti-scheduling-mcp(n8n-nodes-mcp.mcpClientTool, stdio), entrouMCP - ZPRO (nativo n8n)(@n8n/n8n-nodes-langchain.mcpClientTool,serverTransport: httpStreamable, apontando pra URL doMCP Server Triggeracima). 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:
# 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 (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.