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