Snapshot: gti-scheduling-mcp (Z-PRO + Klingo adapters) e workflow n8n de agendamento funcionando de ponta a ponta

- gti-scheduling-mcp: MCP server com ZProAdapter e KlingoAdapter (agendar() completo pros dois)
- Workflow n8n 'Agendamento - GChat - Z-PRO': webhook -> AI Agent -> MCP -> Z-PRO, testado com sucesso real
- Documentacao: specs, plano, runbook de problemas reais e export do workflow

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-10 17:23:10 -03:00
co-authored by Claude Sonnet 5
commit 257afe98c0
48 changed files with 12465 additions and 0 deletions
+263
View File
@@ -0,0 +1,263 @@
# 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)` (id `yG2mi6ECBpPv4Hmn`, servidor
> `srvdevn8ngti` / `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ém `docs/superpowers/specs/2026-08-10-n8n-agendamento-workflow-design.md`
> (design) e `docs/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 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.
## Status final (10/08/2026)
Testado de ponta a ponta com sucesso, com a chave real da OpenAI
(`gpt-4o-mini`): telefone correto, data correta, um único agendamento por
teste, confirmado visualmente no Z-PRO. Workflow deixado **desativado**
propositalmente — só ativar quando o Z-PRO de uma clínica real for
configurado para apontar pra esse webhook.
@@ -0,0 +1,188 @@
{
"name": "Agendamento - GChat - Z-PRO (clínica exemplo)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "agendamento-zpro-clinica-exemplo",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-zpro",
"name": "Webhook Z-PRO",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [-208, -176]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.output }}",
"options": {}
},
"id": "respond-stub",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [224, 48]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const body = $input.item.json.body || {};\n\n// Configuração fixa desta cópia do workflow. Ajustar por clínica ao\n// duplicar este workflow para uma nova instância.\nconst clinicConfig = {\n provider: \"zpro\",\n clinicId: \"clinica-exemplo\",\n};\n\nif (body.step !== \"process\") {\n return {\n json: {\n earlyReply: true,\n sessionId: body.protocol || \"\",\n mensagem: \"\",\n nomeContato: \"\",\n telefone: \"\",\n provider: clinicConfig.provider,\n clinicId: clinicConfig.clinicId,\n },\n };\n}\n\nreturn {\n json: {\n earlyReply: false,\n sessionId: body.protocol || \"\",\n mensagem: String(body.entrada || \"\").trim(),\n nomeContato: body.contactName || \"\",\n telefone: body.number || \"\",\n provider: clinicConfig.provider,\n clinicId: clinicConfig.clinicId,\n },\n};"
},
"id": "normalizar-entrada",
"name": "Normalizar entrada",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [-16, -176],
"onError": "continueErrorOutput"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"combinator": "and",
"conditions": [
{
"id": "cond-early-reply",
"leftValue": "={{ $json.earlyReply }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
]
},
"options": {}
},
"id": "if-early-reply",
"name": "É step=process?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [240, -336]
},
{
"parameters": {
"model": { "mode": "id", "value": "gpt-4o-mini" },
"builtInTools": {},
"options": {}
},
"id": "openai-chat-model",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.3,
"position": [512, -64],
"credentials": {
"openAiApi": { "id": "PRECISA_RECRIAR_NO_SERVIDOR_NOVO", "name": "GPT" }
}
},
{
"parameters": {
"sessionIdType": "customKey",
"sessionKey": "={{ $json.sessionId }}",
"contextWindowLength": 20
},
"id": "memoria-sessao",
"name": "Memória da Sessão",
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.4,
"position": [672, -96]
},
{
"parameters": {
"promptType": "define",
"text": "={{ $json.mensagem }}",
"options": {
"systemMessage": "=Você é o assistente de agendamento da clínica, conversando por WhatsApp. A data de hoje é {{ $now.setZone('America/Sao_Paulo').toFormat('yyyy-MM-dd (cccc)') }}, fuso America/Sao_Paulo — use isso pra calcular qualquer data relativa (\"amanhã\", \"sexta que vem\", etc.), nunca assuma outro ano. clinicId e telefone do paciente já são preenchidos automaticamente pelo sistema — você só precisa decidir data/hora e confirmar o nome completo.\n\nRegras:\n1. O nome de contato do WhatsApp é \"{{ $('Normalizar entrada').item.json.nomeContato }}\" e o telefone é \"{{ $('Normalizar entrada').item.json.telefone }}\" — mas SEMPRE confirme o nome completo do paciente com o cliente antes de agendar, mesmo que já tenha o nome de contato.\n2. Pergunte a data e o horário desejados para a consulta, e opcionalmente o motivo. O horário que o cliente disser (ex.: \"10h\") é sempre horário local de Brasília (America/Sao_Paulo, UTC-3) — use esse mesmo número de hora ao montar startAt/endAt, com offset -03:00 (nunca converta pra UTC/Z, nunca some ou subtraia horas).\n3. Nunca invente informação que o cliente não deu.\n4. Responda sempre em português, em tom cordial e objetivo, adequado para WhatsApp (mensagens curtas).\n5. Quando tiver o nome completo confirmado e a data/hora desejada, você pode agendar de verdade.\n6. NUNCA agende mais de uma vez na mesma conversa — se o cliente já confirmou um agendamento nesta conversa, não agende de novo; informe que o agendamento já foi feito.\n7. Se o agendamento falhar, explique o problema ao cliente em linguagem simples e pergunte se ele quer tentar de novo com outros dados, ou avise que vai encaminhar para um atendente."
}
},
"id": "agente-agendamento",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [560, -320],
"onError": "continueErrorOutput"
},
{
"parameters": {
"operation": "executeTool",
"toolName": "agendar",
"toolParameters": "={{ JSON.stringify({ clinicId: $('Normalizar entrada').item.json.clinicId, startAt: $fromAI('startAt', 'Data e hora de inicio da consulta, no horario local pedido pelo cliente, em ISO 8601 com offset -03:00 (America/Sao_Paulo) -- NUNCA use Z/UTC. Exemplo: cliente pediu 10h -> 2026-08-14T10:00:00-03:00', 'string'), endAt: $fromAI('endAt', 'Data e hora de termino da consulta, mesmo formato do startAt (offset -03:00, nunca Z/UTC), geralmente 30 minutos apos o startAt', 'string'), patient: { name: $fromAI('patientName', 'Nome completo do paciente, ja confirmado com o cliente na conversa', 'string'), phone: $('Normalizar entrada').item.json.telefone } }) }}"
},
"id": "mcp-tool-agendar",
"name": "MCP - gti-scheduling-mcp",
"type": "n8n-nodes-mcp.mcpClientTool",
"typeVersion": 1,
"position": [1024, -128],
"credentials": {
"mcpClientApi": { "id": "PRECISA_RECRIAR_NO_SERVIDOR_NOVO", "name": "Client (STDIO) account" }
}
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "erro-output",
"name": "output",
"type": "string",
"value": "Desculpe, tive um problema técnico agora e não consegui continuar. Pode tentar de novo em alguns minutos, ou aguardar que alguém da nossa equipe vai te atender."
}
]
},
"options": {}
},
"id": "erro-resposta-padrao",
"name": "Erro - Resposta padrão",
"type": "n8n-nodes-base.set",
"typeVersion": 3.5,
"position": [240, -176]
}
],
"connections": {
"Webhook Z-PRO": {
"main": [[{ "node": "Normalizar entrada", "type": "main", "index": 0 }]]
},
"Normalizar entrada": {
"main": [
[{ "node": "É step=process?", "type": "main", "index": 0 }],
[{ "node": "Erro - Resposta padrão", "type": "main", "index": 0 }]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]]
},
"Memória da Sessão": {
"ai_memory": [[{ "node": "AI Agent", "type": "ai_memory", "index": 0 }]]
},
"É step=process?": {
"main": [
[],
[{ "node": "AI Agent", "type": "main", "index": 0 }]
]
},
"AI Agent": {
"main": [
[{ "node": "Respond to Webhook", "type": "main", "index": 0 }],
[{ "node": "Erro - Resposta padrão", "type": "main", "index": 0 }]
]
},
"MCP - gti-scheduling-mcp": {
"ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]]
},
"Erro - Resposta padrão": {
"main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,469 @@
# Workflow n8n de Agendamento (GChat/Z-PRO → gti-scheduling-mcp) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Construir, no n8n (`https://devn8n.gtigrupo.com.br`), o primeiro workflow que recebe mensagens de clientes via webhook do Z-PRO/GChat, conduz a conversa com um AI Agent para capturar os dados do agendamento, e chama a tool `agendar` do `gti-scheduling-mcp` (via `ZProAdapter`) para realizar o agendamento de fato.
**Architecture:** Webhook (Z-PRO) → node `Code` de normalização/guarda de `step` → `AI Agent` (OpenAI + memória de sessão por `protocol` + tool MCP `agendar`) → `Respond to Webhook` (texto puro). Construído incrementalmente: cada task adiciona uma camada e é validada isoladamente antes da próxima.
**Tech Stack:** n8n (self-hosted, `devn8n.gtigrupo.com.br`), node `n8n-nodes-mcp` (community, transporte stdio), `gti-scheduling-mcp` (já implementado, sem alterações de código nesta etapa), OpenAI Chat Model.
## Global Constraints
- Nenhuma alteração de código/arquitetura no `gti-scheduling-mcp` é necessária ou permitida nesta etapa — ele já expõe a tool `agendar` exatamente como o workflow precisa consumir.
- O webhook precisa **sempre responder de imediato**, uma execução por mensagem — nunca segurar a resposta esperando mais mensagens (Z-PRO descarta mensagens se isso acontecer).
- A resposta ao Z-PRO é **sempre texto puro**, nunca JSON.
- Nenhuma credencial (token do Z-PRO, API key da OpenAI) é inserida por quem executa este plano de forma automática — todas vêm de Credentials do n8n, criadas manualmente pelo usuário na UI.
- `sessionId` da memória do agente = `protocol` do payload do Z-PRO (nunca o telefone — o mesmo telefone pode ter vários atendimentos/protocolos).
- Escopo: só `agendar()` via `ZProAdapter`. Nada de `consultar_horarios`, cancelamento, remarcação ou múltiplas clínicas nesta etapa (ver spec, seção "Fora de escopo").
Spec de referência: `docs/superpowers/specs/2026-08-10-n8n-agendamento-workflow-design.md`
---
## Pré-requisitos (ação manual do usuário — fora do escopo de execução automática)
Estes itens **não podem ser feitos pelas tasks abaixo** (exigem acesso à infraestrutura do n8n/credenciais reais) e precisam estar prontos antes da Task 5:
1. **Instalar o node comunitário `n8n-nodes-mcp`** no n8n de vocês, seguindo
`https://docs.n8n.io/integrations/community-nodes/installation/`.
2. **Definir a variável de ambiente `N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true`**
no processo do n8n (ex.: no `docker-compose.yml` do serviço n8n) e reiniciar o n8n — sem isso, o node comunitário não pode ser usado como tool dentro do AI Agent.
3. **Criar uma Credential do tipo OpenAI** no n8n (Settings → Credentials → OpenAI API), com uma API key válida.
4. **Criar uma Credential do tipo "MCP Client (Command Line) API"** (fornecida pelo node `n8n-nodes-mcp` após instalado), com:
- **Command**: `node`
- **Arguments**: caminho absoluto para `dist/index.js` do `gti-scheduling-mcp` no servidor onde o n8n roda (ex.: `/opt/gti-scheduling-mcp/dist/index.js`) — depende de onde o projeto for implantado, que é uma decisão de infraestrutura fora do escopo deste plano.
- **Environment Variables** (uma por linha, formato `NOME=valor`):
```
SCHEDULING_PROVIDER=zpro
ZPRO_BASE_URL=<url real da clínica>
ZPRO_API_TOKEN=<token real da clínica>
ZPRO_API_ID=<ApiID real da clínica — o GUID, não o número do registro>
```
Sem os 4 itens acima, a Task 5 (conectar a tool MCP ao Agent) não pode ser validada de ponta a ponta — as Tasks 1 a 4 não dependem deles.
---
### Task 1: Build do `gti-scheduling-mcp` e localização do `dist/index.js`
**Files:**
- Nenhum arquivo novo — apenas build do projeto existente.
**Interfaces:**
- Produces: caminho absoluto de `dist/index.js`, usado nas Tasks 5 e no pré-requisito 4 acima.
- [ ] **Step 1: Rodar o build**
```bash
cd "D:\PROJETOS\KLINGO-CLINICAL-SYSLIFE-GCHAT-AGENDAMENTO\gti-scheduling-mcp"
npm run build
```
Esperado: `tsc -p tsconfig.json` sem erros, e a pasta `dist/` criada/atualizada.
- [ ] **Step 2: Confirmar que `dist/index.js` existe e roda**
```bash
node "D:\PROJETOS\KLINGO-CLINICAL-SYSLIFE-GCHAT-AGENDAMENTO\gti-scheduling-mcp\dist\index.js" --help 2>&1 | head -5
```
Esperado: o processo inicia (mesmo que não reconheça `--help`, não deve dar erro de módulo não encontrado). Se o servidor esperar variáveis de ambiente obrigatórias e falhar por causa disso, isso confirma que o build está correto (o processo rodou, só não tinha config — comportamento esperado sem env vars).
Anote o caminho absoluto de `dist/index.js` — será necessário nos pré-requisitos e na Task 5.
---
### Task 2: Esqueleto base do webhook (Webhook → Respond to Webhook)
**Files:** Nenhum arquivo local — cria o workflow no n8n via `n8n_create_workflow`.
**Interfaces:**
- Produces: `workflowId` (retornado pela chamada) — usado em todas as tasks seguintes.
- [ ] **Step 1: Criar o workflow com dois nodes ligados diretamente**
Chame a tool `n8n_create_workflow` com:
```json
{
"name": "Agendamento - GChat - Z-PRO (clínica exemplo)",
"nodes": [
{
"id": "webhook-zpro",
"name": "Webhook Z-PRO",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [0, 0],
"parameters": {
"httpMethod": "POST",
"path": "agendamento-zpro-clinica-exemplo",
"responseMode": "responseNode"
}
},
{
"id": "respond-stub",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [300, 0],
"parameters": {
"respondWith": "text",
"responseBody": "teste ok"
}
}
],
"connections": {
"Webhook Z-PRO": {
"main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]]
}
}
}
```
Guarde o `id` do workflow retornado — chamado de `<workflowId>` no restante deste plano.
- [ ] **Step 2: Validar o workflow**
Chame `n8n_validate_workflow` com `{"id": "<workflowId>"}`. Esperado: sem erros (pode haver avisos sobre o workflow estar inativo — normal, só ativamos na Task 6).
- [ ] **Step 3: Ativar temporariamente e testar a resposta em texto puro**
Ative o workflow (`n8n_update_partial_workflow` com operação `{"type": "activateWorkflow"}`), pegue a URL de produção do webhook (visível no node Webhook Z-PRO / na resposta da criação), e chame:
```bash
curl -i -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
-H "Content-Type: application/json" \
-d '{"step":"process","entrada":"oi","number":"5511999999999","contactName":"Teste","protocol":"protocolo-teste-1"}'
```
Esperado: HTTP 200, corpo **exatamente** `teste ok` (texto puro, sem aspas de JSON ao redor).
- [ ] **Step 4: Desativar o workflow de novo**
`n8n_update_partial_workflow` com `{"type": "deactivateWorkflow"}` — mantemos desativado até a Task 6, para não responder de verdade a mensagens reais enquanto ainda está incompleto.
---
### Task 3: Node de normalização + guarda de `step`
**Files:** Nenhum arquivo local — edita o workflow `<workflowId>` via `n8n_update_partial_workflow`.
**Interfaces:**
- Consumes: `<workflowId>` da Task 2.
- Produces: campos `sessionId`, `mensagem`, `nomeContato`, `telefone`, `provider`, `clinicId`, `earlyReply` — consumidos pela Task 4 (AI Agent) e pelo `If` desta task.
- [ ] **Step 1: Adicionar o node `Code` de normalização**
```json
{
"type": "addNode",
"node": {
"id": "normalizar-entrada",
"name": "Normalizar entrada",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [150, -120],
"parameters": {
"mode": "runOnceForEachItem",
"language": "javaScript",
"jsCode": "const body = $input.item.json.body || {};\n\n// Configuração fixa desta cópia do workflow. Ajustar por clínica ao\n// duplicar este workflow para uma nova instância.\nconst clinicConfig = {\n provider: \"zpro\",\n clinicId: \"clinica-exemplo\",\n};\n\nif (body.step !== \"process\") {\n return {\n json: {\n earlyReply: true,\n sessionId: body.protocol || \"\",\n mensagem: \"\",\n nomeContato: \"\",\n telefone: \"\",\n provider: clinicConfig.provider,\n clinicId: clinicConfig.clinicId,\n },\n };\n}\n\nreturn {\n json: {\n earlyReply: false,\n sessionId: body.protocol || \"\",\n mensagem: String(body.entrada || \"\").trim(),\n nomeContato: body.contactName || \"\",\n telefone: body.number || \"\",\n provider: clinicConfig.provider,\n clinicId: clinicConfig.clinicId,\n },\n};"
}
}
}
```
- [ ] **Step 2: Adicionar o node `If` que decide entre resposta antecipada e fluxo normal**
```json
{
"type": "addNode",
"node": {
"id": "if-early-reply",
"name": "É step=process?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [400, -120],
"parameters": {
"conditions": {
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" },
"combinator": "and",
"conditions": [
{
"id": "cond-early-reply",
"leftValue": "={{ $json.earlyReply }}",
"rightValue": true,
"operator": { "type": "boolean", "operation": "true", "singleValue": true }
}
]
}
}
}
}
```
- [ ] **Step 3: Rewire — Webhook → Normalizar entrada → If**
```json
[
{ "type": "removeConnection", "source": "Webhook Z-PRO", "target": "Respond to Webhook" },
{ "type": "addConnection", "source": "Webhook Z-PRO", "target": "Normalizar entrada" },
{ "type": "addConnection", "source": "Normalizar entrada", "target": "É step=process?" }
]
```
- [ ] **Step 4: Ligar a saída TRUE do If (índice 0) direto ao Respond to Webhook existente, com texto vazio**
```json
[
{ "type": "patchNodeField", "nodeName": "Respond to Webhook", "fieldPath": "parameters.responseBody", "patches": [{ "find": "teste ok", "replace": "={{ $json.mensagem || '' }}" }] },
{ "type": "addConnection", "source": "É step=process?", "sourceOutput": 0, "target": "Respond to Webhook" }
]
```
A saída FALSE (índice 1) do `If` fica sem conexão por enquanto — será ligada ao AI Agent na Task 4.
- [ ] **Step 5: Validar**
`n8n_validate_workflow` com `{"id": "<workflowId>"}`. Esperado: sem erros. Pode haver aviso de node com saída não conectada (`É step=process?` saída FALSE) — esperado nesta task, resolvido na Task 4.
- [ ] **Step 6: Testar os dois caminhos**
Ative o workflow, chame o webhook duas vezes:
```bash
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
-H "Content-Type: application/json" \
-d '{"step":"outro","entrada":"x","number":"1","contactName":"y","protocol":"p1"}'
```
Esperado: corpo vazio (early reply, `step` diferente de `process`).
```bash
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
-H "Content-Type: application/json" \
-d '{"step":"process","entrada":"Olá, quero marcar uma consulta","number":"5511999999999","contactName":"Fulano","protocol":"p2"}'
```
Esperado: corpo = `Olá, quero marcar uma consulta` (o `Respond to Webhook` está ecoando `mensagem` só para confirmar que os campos normalizados chegaram certos — será substituído pela resposta do Agent na Task 4).
Desative o workflow ao final (`deactivateWorkflow`), como na Task 2.
---
### Task 4: AI Agent com memória de sessão (sem tool ainda)
**Files:** Nenhum arquivo local — edita `<workflowId>`.
**Interfaces:**
- Consumes: `earlyReply` (falso), `sessionId`, `mensagem`, `nomeContato`, `telefone` da Task 3.
- Produces: texto de resposta do Agent, consumido pelo `Respond to Webhook`.
- [ ] **Step 1: Adicionar o node OpenAI Chat Model**
```json
{
"type": "addNode",
"node": {
"id": "openai-chat-model",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1,
"position": [650, 100],
"parameters": {
"model": { "value": "gpt-4o-mini" },
"options": {}
},
"credentials": {
"openAiApi": { "id": "<ID da Credential OpenAI criada no pré-requisito 3>", "name": "OpenAI account" }
}
}
}
```
Substitua `<ID da Credential OpenAI criada no pré-requisito 3>` pelo ID real da credencial (visível na UI do n8n, em Settings → Credentials, ou via `n8n_manage_credentials` se disponível). Se preferir, este campo pode ser deixado para o usuário selecionar manualmente na UI do n8n após a criação do node — não bloqueia os steps seguintes.
- [ ] **Step 2: Adicionar o node Simple Memory**
```json
{
"type": "addNode",
"node": {
"id": "memoria-sessao",
"name": "Memória da Sessão",
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.4,
"position": [650, 250],
"parameters": {
"sessionIdType": "customKey",
"sessionKey": "={{ $('Normalizar entrada').item.json.sessionId }}",
"contextWindowLength": 20
}
}
}
```
- [ ] **Step 3: Adicionar o node AI Agent**
```json
{
"type": "addNode",
"node": {
"id": "agente-agendamento",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [650, -120],
"parameters": {
"promptType": "define",
"text": "={{ $json.mensagem }}",
"options": {
"systemMessage": "Você é o assistente de agendamento da clínica, conversando por WhatsApp. Regras:\n1. O nome de contato do WhatsApp é \"{{ $('Normalizar entrada').item.json.nomeContato }}\" e o telefone é \"{{ $('Normalizar entrada').item.json.telefone }}\" — mas SEMPRE confirme o nome completo do paciente com o cliente antes de agendar, mesmo que já tenha o nome de contato.\n2. Pergunte a data e o horário desejados para a consulta, e opcionalmente o motivo.\n3. Nunca invente informação que o cliente não deu.\n4. Responda sempre em português, em tom cordial e objetivo, adequado para WhatsApp (mensagens curtas).\n5. Quando tiver o nome completo confirmado e a data/hora desejada, você pode agendar de verdade.\n6. NUNCA agende mais de uma vez na mesma conversa — se o cliente já confirmou um agendamento nesta conversa, não agende de novo; informe que o agendamento já foi feito.\n7. Se o agendamento falhar, explique o problema ao cliente em linguagem simples e pergunte se ele quer tentar de novo com outros dados, ou avise que vai encaminhar para um atendente."
}
}
}
}
```
- [ ] **Step 4: Conectar Chat Model e Memory ao Agent, e a saída FALSE do If ao Agent**
```json
[
{ "type": "addConnection", "source": "OpenAI Chat Model", "sourceOutput": "ai_languageModel", "target": "AI Agent", "targetInput": "ai_languageModel" },
{ "type": "addConnection", "source": "Memória da Sessão", "sourceOutput": "ai_memory", "target": "AI Agent", "targetInput": "ai_memory" },
{ "type": "addConnection", "source": "É step=process?", "sourceOutput": 1, "target": "AI Agent" },
{ "type": "addConnection", "source": "AI Agent", "target": "Respond to Webhook" }
]
```
- [ ] **Step 5: Ajustar o Respond to Webhook para usar a saída do Agent**
```json
{
"type": "patchNodeField",
"nodeName": "Respond to Webhook",
"fieldPath": "parameters.responseBody",
"patches": [{ "find": "={{ $json.mensagem || '' }}", "replace": "={{ $json.output }}" }]
}
```
(`$json.output` é o campo de texto padrão que o node AI Agent produz.)
- [ ] **Step 6: Validar**
`n8n_validate_workflow` com `{"id": "<workflowId>"}`. Esperado: sem erros. Se aparecer erro sobre a credencial OpenAI ausente/inválida, ajuste o `credentials.openAiApi.id` (Step 1) ou peça para o usuário selecionar a credencial manualmente na UI antes de prosseguir.
- [ ] **Step 7: Testar a memória de sessão**
Ative o workflow. Envie duas mensagens com o **mesmo** `protocol`:
```bash
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
-H "Content-Type: application/json" \
-d '{"step":"process","entrada":"Meu nome completo é João da Silva Teste","number":"5511999999999","contactName":"João","protocol":"p-memoria-1"}'
```
```bash
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
-H "Content-Type: application/json" \
-d '{"step":"process","entrada":"Qual é o meu nome completo que eu te falei?","number":"5511999999999","contactName":"João","protocol":"p-memoria-1"}'
```
Esperado: a segunda resposta menciona "João da Silva Teste" — confirma que a memória por `sessionId=protocol` está funcionando entre execuções separadas do webhook.
Desative o workflow ao final.
---
### Task 5: Conectar a tool MCP `agendar` ao Agent
**Pré-requisito:** os 4 itens da seção "Pré-requisitos" no topo deste plano precisam estar prontos (node comunitário instalado, env var setada e n8n reiniciado, credenciais OpenAI e MCP criadas).
**Files:** Nenhum arquivo local — edita `<workflowId>`.
**Interfaces:**
- Consumes: credencial MCP stdio criada no pré-requisito 4.
- Produces: capacidade do Agent de chamar `agendar` de verdade.
- [ ] **Step 1: Adicionar o node MCP Client Tool**
```json
{
"type": "addNode",
"node": {
"id": "mcp-tool-agendar",
"name": "MCP - gti-scheduling-mcp",
"type": "n8n-nodes-mcp.mcpClientTool",
"typeVersion": 1,
"position": [900, 250],
"parameters": {
"connectionType": "cmd",
"include": "selected",
"includeTools": ["agendar"]
},
"credentials": {
"mcpClientCmdApi": { "id": "<ID da Credential MCP stdio criada no pré-requisito 4>", "name": "gti-scheduling-mcp (stdio)" }
}
}
}
```
Os nomes exatos do tipo de credencial (`mcpClientCmdApi`) e da propriedade `connectionType` podem variar conforme a versão instalada do pacote `n8n-nodes-mcp` — se a validação do Step 3 apontar um nome de campo diferente, ajuste conforme o que a própria UI do n8n mostrar ao adicionar o node manualmente (Settings do node → aba Credentials).
- [ ] **Step 2: Conectar o node como tool do Agent**
```json
{
"type": "addConnection",
"source": "MCP - gti-scheduling-mcp",
"sourceOutput": "ai_tool",
"target": "AI Agent",
"targetInput": "ai_tool"
}
```
- [ ] **Step 3: Validar**
`n8n_validate_workflow` com `{"id": "<workflowId>", "options": {"profile": "ai-friendly"}}`. Corrija qualquer erro de nome de campo/credencial apontado (ver nota no Step 1).
- [ ] **Step 4: Teste manual controlado, com dados fictícios, seguindo a mesma disciplina de segurança contra duplicidade do teste do `ZProAdapter`**
Ative o workflow. Envie, com um `protocol` novo e único:
```bash
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
-H "Content-Type: application/json" \
-d '{"step":"process","entrada":"Oi, quero marcar uma consulta pra amanhã às 14h, meu nome completo é TESTE-N8N Paciente","number":"5511900000001","contactName":"Teste N8N","protocol":"p-e2e-1"}'
```
Continue a conversa (repetindo a chamada com o mesmo `protocol`, mudando só `entrada`) até o Agent confirmar o agendamento. **Antes de cada nova tentativa, se algo parecer ter falhado, confira visualmente no Z-PRO se o agendamento já foi criado** (mesma regra usada no teste manual do `ZProAdapter`) — não repita mensagens de confirmação "às cegas".
Confirme visualmente no Z-PRO que o agendamento fictício foi criado.
- [ ] **Step 5: Desativar o workflow ao final do teste**
`n8n_update_partial_workflow` com `{"type": "deactivateWorkflow"}` — só reativar definitivamente depois que o usuário revisar o resultado (Task 6).
---
### Task 6: Revisão final e ativação
**Files:** Nenhum arquivo local.
- [ ] **Step 1: Validação final completa**
`n8n_validate_workflow` com `{"id": "<workflowId>", "options": {"profile": "strict"}}`. Resolver qualquer erro/aviso restante.
- [ ] **Step 2: Apresentar o resumo ao usuário**
Antes de ativar definitivamente, mostrar ao usuário: URL do webhook de produção, nome do workflow, e pedir confirmação de que o Z-PRO já está configurado para apontar o fluxo de agendamento desta clínica para essa URL.
- [ ] **Step 3: Ativar**
Só ativar (`activateWorkflow`) depois da confirmação explícita do usuário no Step 2 — ativar aqui significa que mensagens reais de clientes passam a ser processadas.
---
## Observação sobre "commit"
Nem `D:\PROJETOS\KLINGO-CLINICAL-SYSLIFE-GCHAT-AGENDAMENTO` nem `gti-scheduling-mcp` são repositórios git — não há passo de `git commit` neste plano. O estado do workflow vive inteiramente no n8n; cada task é validada via `n8n_validate_workflow` e teste funcional (chamada real ao webhook), no lugar de testes automatizados de código.
@@ -0,0 +1,164 @@
# Design — Workflow n8n de Agendamento via GChat/Z-PRO + gti-scheduling-mcp
**Data:** 2026-08-10
**Status:** Aprovado pelo usuário, aguardando plano de implementação.
## Objetivo
Construir o primeiro workflow n8n que recebe mensagens de clientes via
GChat (webhook do Z-PRO), conduz a conversa com um agente de IA para
capturar os dados necessários e realiza o agendamento de fato, chamando a
tool `agendar` do `gti-scheduling-mcp` (que por sua vez delega ao Adapter
do sistema da clínica — Z-PRO neste MVP).
Escopo do MVP: **só agendamento** (`agendar`). Consulta de disponibilidade,
cancelamento e remarcação ficam fora desta etapa.
## Contexto verificado (não inventado)
Estes fatos vêm de um workflow real já em produção no n8n de referência
("Enforcer - GLPI"/fluxo de suporte via Z-PRO, código do node `1 - Decide`
compartilhado pelo usuário) e de consultas diretas ao n8n
(`devn8n.gtigrupo.com.br`) e ao registro de nodes via `n8n-mcp`:
- O Z-PRO chama o webhook do n8n **uma vez por mensagem do cliente** — não
agrupa/enfileira mensagens.
- O corpo da requisição (`req = $json.body`) tem os campos: `step`
(esperado `"process"`), `entrada` (texto da mensagem do cliente),
`number` (telefone), `contactName`, `protocol` (identificador único do
**atendimento**, não do telefone — o mesmo cliente pode ter vários
atendimentos/protocolos ao longo do tempo).
- **Se o workflow segurar a resposta** (esperando mais mensagens antes de
responder), o Z-PRO **descarta** as mensagens seguintes do cliente
(confirmado em teste real: 5 mensagens em sequência → 1 execução no
n8n). O workflow precisa responder a cada execução, sempre.
- A resposta ao Z-PRO é **texto puro** (não JSON), via node `Respond to
Webhook` em modo texto.
- O node oficial `MCP Client Tool` (`@n8n/n8n-nodes-langchain`) só suporta
transporte HTTP Streamable/SSE — **não** suporta rodar um comando local
via stdio.
- O node comunitário `n8n-nodes-mcp` (`mcpClient`/`mcpClientTool`) suporta
conexão via linha de comando (stdio) — é o caminho escolhido para não
precisar alterar o transporte do `gti-scheduling-mcp` (que hoje só
implementa `StdioServerTransport`).
- O `gti-scheduling-mcp` (`ZProAdapter`/`KlingoAdapter`) já está pronto e
testado para `agendar()` — nenhuma mudança de arquitetura nele é
necessária para este workflow.
## Arquitetura
```
[Cliente WhatsApp]
│
▼
[Z-PRO/GChat] ──webhook──▶ [n8n: Webhook] → [Normalizar entrada] → [AI Agent] → [Respond to Webhook]
│ ▲
(tool call) │ │ (memória de sessão)
▼ │
[MCP Client Tool] [Simple Memory]
(stdio, agendar) (sessionId = protocol)
│
▼
[gti-scheduling-mcp: tool "agendar"]
│
▼
[AdapterFactory → ZProAdapter/KlingoAdapter]
│
▼
[Sistema real da clínica: Z-PRO/Klingo]
```
O `gti-scheduling-mcp` não sofre nenhuma alteração de arquitetura para
este workflow — o n8n é só mais um cliente MCP, como qualquer outro.
## Componentes (nós do n8n)
1. **Webhook (Z-PRO)** — `POST`, recebe `{step, entrada, number,
contactName, protocol}`.
2. **Normalizar entrada** (`Set`/`Code`) —
- Valida `step === "process"`; caso contrário, responde com um texto
mínimo e encerra (mesma defesa do fluxo de referência).
- Monta as variáveis usadas a seguir: `sessionId = protocol`,
`mensagem = entrada`, `nomeContato = contactName`, `telefone =
number`.
- **Nó de configuração da clínica** (fixo por cópia do workflow, não
dinâmico em runtime): `provider` (ex.: `"zpro"`), `clinicId`, e
referência de qual Credential do n8n / config do MCP Client Tool
usar para esta clínica. Cada clínica nova = uma cópia deste workflow
com esses valores ajustados e seu próprio webhook cadastrado no
Z-PRO — não há branching dinâmico de provider dentro de uma mesma
execução.
3. **AI Agent** (`@n8n/n8n-nodes-langchain.agent`) —
- Chat Model: OpenAI (credencial a configurar no n8n).
- System prompt cobre: confirmar o nome completo do paciente (mesmo já
tendo `contactName`), perguntar data/horário desejado, motivo
(opcional), nunca chamar `agendar` mais de uma vez por atendimento,
responder sempre em português, tom cordial, nunca inventar dado que
o cliente não informou.
- **Memória**: `Simple Memory` (`memoryBufferWindow`), `sessionId =
protocol`. Trade-off aceito para o MVP: essa memória vive na RAM do
processo do n8n — um restart do n8n no meio de um atendimento perde
o histórico da conversa. Evolução futura, se necessário: Postgres ou
Redis Chat Memory (durável, mas exige infraestrutura adicional).
- **Tool**: `MCP Client Tool` (via `n8n-nodes-mcp`, transporte stdio),
expondo a tool `agendar` do `gti-scheduling-mcp` (comando: `node
dist/index.js`, variáveis de ambiente = credenciais da clínica,
vindas de uma Credential do n8n — nunca hardcoded no node/JSON do
workflow).
4. **Respond to Webhook** — modo texto simples; devolve só a resposta em
linguagem natural produzida pelo AI Agent.
## Tratamento de erro
Quando `agendar` falha (`VALIDATION_ERROR`, `AUTHENTICATION_ERROR`,
`NOT_FOUND`, `ZPRO_API_ERROR`/`KLINGO_API_ERROR`, timeout, etc.), o MCP já
devolve uma resposta estruturada (`success:false` + código/mensagem) — o
AI Agent recebe isso como resultado da tool e decide, em linguagem
natural, como comunicar isso ao cliente (pedir os dados de novo, avisar
que vai encaminhar para um atendente humano, etc.), dentro do que o
system prompt permitir. Não há tratamento hard-coded por código de erro
neste workflow — isso fica deliberadamente a cargo do agente, para manter
o MVP simples.
## Risco de agendamento duplicado (mitigação no nível do workflow)
O `ZProAdapter`/`KlingoAdapter` já trata qualquer HTTP 2xx do provider
como sucesso operacional (sem risco de duplicidade por reinterpretação de
resposta ambígua — ver `docs/zpro-api-mapping.md`). O risco adicional
neste workflow é de outra natureza: o **próprio agente** decidir chamar a
tool `agendar` mais de uma vez na mesma conversa (ex.: o cliente confirma
duas vezes, ou o agente se confunde). Mitigação adotada para o MVP:
instrução explícita no system prompt para nunca chamar `agendar` mais de
uma vez por atendimento (`protocol`). Isto é uma mitigação por prompt, não
uma garantia determinística — se isso se mostrar insuficiente em uso real,
a evolução natural é adicionar uma marcação de "já agendado" no estado da
sessão, verificada por código antes de permitir uma nova chamada da tool.
## Fora de escopo (explicitamente adiado)
- Consulta de disponibilidade (`consultar_horarios`) — não implementada
para nenhum provider neste MVP (Z-PRO não suporta; Klingo tem lacuna de
mapeamento de resposta).
- Cancelamento/remarcação de agendamento.
- Transferência automática de fila/atendimento humano no Z-PRO após o
agendamento (padrão existente no fluxo de referência via
`updateticketinfo`) — pode ser adicionado depois, não é necessário para
o MVP funcionar.
- Suporte a múltiplas clínicas com providers diferentes atendidas por uma
única execução/webhook compartilhado — cada clínica é uma cópia
dedicada do workflow.
- Memória de conversa durável entre restarts do n8n (Postgres/Redis Chat
Memory).
## Testes
Este workflow vive no n8n, fora do repositório `gti-scheduling-mcp` — não
há testes automatizados de código aqui. A validação é funcional/manual:
- Testar o fluxo completo com uma conversa real (ou simulada via chamada
direta ao webhook) contra o Z-PRO, com dados fictícios, seguindo a mesma
disciplina de segurança contra duplicidade já usada nos testes manuais
do `ZProAdapter` (execução única, verificação visual no Z-PRO antes de
qualquer nova tentativa).
- Confirmar que o `Respond to Webhook` devolve texto puro, sem envelope
JSON, compatível com o que o Z-PRO espera exibir ao cliente.