Plano: MCP Server nativo no n8n para o Klingo

4 tasks: sub-workflow consultar_horarios (SLOT_MAPPING_UNAVAILABLE),
sub-workflow agendar (fluxo de 2 chamadas HTTP com autenticação em
dois estágios — X-APP-TOKEN estático + Bearer de sessão dinâmico via
expressão, 3 pontos de saída antecipada por erro/validação), servidor
MCP com as 2 tools, e revisão final + commit. Sem clínica Klingo real
disponível — validação só estrutural, workflows ficam inativos até
haver um workflow de agente cliente pra consumi-los.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-10 23:37:44 -03:00
co-authored by Claude Sonnet 5
parent acad79f175
commit b1790f8e37
@@ -0,0 +1,461 @@
# MCP Server Nativo no n8n para o Klingo Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task (não subagent-driven-development — as tasks são chamadas de API do n8n, não commits de código; não há diff de git pra revisar por task). Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Construir o "MCP Server" do Klingo inteiramente dentro do n8n (nodes oficiais `MCP Server Trigger` + `Call n8n Sub-Workflow Tool`), segunda aplicação do padrão já validado em produção pro Z-PRO.
**Architecture:** 3 workflows n8n — o servidor (`MCP Server Trigger` + 2 tools) e dois sub-workflows (`agendar`, `consultar_horarios`) que portam a lógica já validada de `klingo.adapter.ts`/`klingo.mapper.ts`/`klingo.errors.ts` para nodes `Code`/`HTTP Request`/`If`. Diferente do Z-PRO, `agendar` tem duas chamadas HTTP em sequência com dois mecanismos de autenticação (X-APP-TOKEN estático + Bearer de sessão dinâmico), e três pontos de saída antecipada por erro/validação.
**Tech Stack:** n8n (`devn8n.gtigrupo.com.br`), nodes oficiais `@n8n/n8n-nodes-langchain` (`mcpTrigger`, `toolWorkflow`), `n8n-nodes-base` (`executeWorkflowTrigger`, `httpRequest`, `code`, `set`, `if`).
## Global Constraints
- Nenhum código TypeScript é alterado — `gti-scheduling-mcp` fica intocado como referência.
- Qualquer HTTP 2xx de `POST /agenda/reservar` é sucesso operacional, sem exceção — nunca reportar erro numa operação que o Klingo já aceitou (risco de agendamento duplicado). `POST /paciente/identificar` é diferente: não cria nenhum dado no Klingo, então é seguro abortar com erro se essa etapa falhar, sem seguir adiante.
- `consultar_horarios` sempre retorna erro `SLOT_MAPPING_UNAVAILABLE` — decisão já registrada no `KlingoAdapter` (lacuna de contrato da API, não uma falha técnica), sem chamada HTTP.
- O Bearer de `POST /agenda/reservar` nunca é uma Credential do n8n — é construído por expressão a partir do `access_token` retornado por `POST /paciente/identificar`, dentro da mesma execução.
- Nenhuma credencial é inserida por quem executa este plano de forma automática — a credencial `X-APP-TOKEN` é criada manualmente pelo usuário na UI do n8n (mesmo sem valor real disponível ainda, ver Pré-requisito).
- Sem clínica Klingo real disponível nesta etapa: nenhuma chamada HTTP real é feita. Validação é só estrutural (`n8n_validate_workflow`). Os 3 workflows ficam **inativos** ao final (nada os aciona ainda — sem workflow de agente cliente).
Spec de referência: `docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-klingo-design.md`
---
## Pré-requisito (ação manual do usuário)
Criar no n8n uma Credential do tipo **Header Auth** (Settings → Credentials → New → "Header Auth"):
- **Name**: `X-APP-TOKEN`
- **Value**: um valor de rascunho por enquanto (ex.: `SUBSTITUIR_PELO_TOKEN_REAL_DA_CLINICA`) — sem clínica Klingo real ainda, não há token de verdade. Trocar pelo valor real assim que houver uma clínica configurada, do mesmo jeito que a credencial do Z-PRO foi corrigida nesta sessão.
Nomeie como `Klingo APP Token - clínica pendente`. Anote o ID gerado — será necessário na Task 2.
---
### Task 1: Sub-workflow "MCP Server - Klingo - consultar_horarios"
**Interfaces:**
- Produces: um workflow n8n chamado `MCP Server - Klingo - consultar_horarios`, com um `Execute Workflow Trigger` (modo passthrough) → `Code` que sempre retorna `{ success: false, error: { code: "SLOT_MAPPING_UNAVAILABLE", message: "..." } }`.
- [ ] **Step 1: Criar o workflow**
Chamar `n8n_create_workflow`:
```json
{
"name": "MCP Server - Klingo - consultar_horarios",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"parameters": { "inputSource": "passthrough" }
},
{
"id": "slot-mapping-unavailable",
"name": "Retornar SLOT_MAPPING_UNAVAILABLE",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [260, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "return {\n json: {\n success: false,\n error: {\n code: \"SLOT_MAPPING_UNAVAILABLE\",\n message: \"A API do Klingo responde com sucesso, mas o formato dos horários individuais não está documentado publicamente (ver docs/klingo-openapi.json). Não é possível consultar horários disponíveis por esta integração no momento.\",\n },\n },\n};"
}
}
],
"connections": {
"Execute Workflow Trigger": {
"main": [[{ "node": "Retornar SLOT_MAPPING_UNAVAILABLE", "type": "main", "index": 0 }]]
}
}
}
```
Guarde o `id` retornado como `<wf-klingo-consultar-horarios>`.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-klingo-consultar-horarios>"}`. Esperado: sem erros.
- [ ] **Step 3: Testar manualmente**
Na UI do n8n, abrir o workflow e clicar em "Execute Workflow". Esperado: o node "Retornar SLOT_MAPPING_UNAVAILABLE" produz `{ success: false, error: { code: "SLOT_MAPPING_UNAVAILABLE", ... } }`.
---
### Task 2: Sub-workflow "MCP Server - Klingo - agendar"
**Interfaces:**
- Consumes: credencial `Klingo APP Token - clínica pendente` do pré-requisito.
- Produces: um workflow n8n chamado `MCP Server - Klingo - agendar`, que recebe `patientPhone` (obrigatório), `patientDocument` (opcional), `slotId` (obrigatório), `specialtyId` (obrigatório), e retorna `{ success: true, data: {...} }` ou `{ success: false, error: {...} }`.
- [ ] **Step 1: Criar o workflow com todos os nodes e conexões**
Chamar `n8n_create_workflow`:
```json
{
"name": "MCP Server - Klingo - agendar",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{ "name": "patientPhone", "type": "string" },
{ "name": "patientDocument", "type": "string" },
{ "name": "slotId", "type": "string" },
{ "name": "specialtyId", "type": "string" }
]
}
}
},
{
"id": "config",
"name": "Config Klingo",
"type": "n8n-nodes-base.set",
"typeVersion": 3.5,
"position": [260, 0],
"parameters": {
"mode": "manual",
"includeOtherFields": true,
"assignments": {
"assignments": [
{ "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "https://SUBSTITUIR-PELA-URL-REAL-DA-CLINICA.klingo.com.br" }
]
}
}
},
{
"id": "montar-identificar",
"name": "Montar corpo identificar",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nconst phone = (input.patientPhone || \"\").trim();\n\nif (!phone) {\n return {\n json: {\n _outcome: \"final\",\n success: false,\n error: {\n code: \"VALIDATION_ERROR\",\n message: \"O telefone do paciente é obrigatório para identificação no Klingo.\",\n },\n },\n };\n}\n\nconst identificarBody = { telefone: phone };\nif (input.patientDocument && String(input.patientDocument).trim()) {\n identificarBody.cpf = String(input.patientDocument).trim();\n} else {\n identificarBody.apenas_telefone = true;\n}\n\nreturn {\n json: {\n _outcome: \"continue\",\n baseUrl: input.baseUrl,\n slotId: input.slotId,\n specialtyId: input.specialtyId,\n identificarBody,\n },\n};"
}
},
{
"id": "if-validacao-identificar",
"name": "Dados de identificação válidos?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [780, 0],
"parameters": {
"conditions": {
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
"combinator": "and",
"conditions": [
{
"id": "cond-outcome-continue-1",
"leftValue": "={{ $json._outcome }}",
"rightValue": "continue",
"operator": { "type": "string", "operation": "equals" }
}
]
},
"options": {}
}
},
{
"id": "post-identificar",
"name": "POST /paciente/identificar",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [1040, -120],
"parameters": {
"method": "POST",
"url": "={{ $json.baseUrl }}/paciente/identificar",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.identificarBody) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
},
"credentials": {
"httpHeaderAuth": { "id": "SUBSTITUIR_PELO_ID_DA_CREDENCIAL", "name": "Klingo APP Token - clínica pendente" }
}
},
{
"id": "processar-identificar",
"name": "Processar identificação",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1300, -120],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\nconst bodyText = typeof rawBody === \"string\" ? rawBody : JSON.stringify(rawBody ?? \"\");\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do Klingo ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do Klingo ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do Klingo rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"KLINGO_API_ERROR\", message: `A API do Klingo retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { _outcome: \"final\", success: false, error: classifyError(status, \"identificar o paciente\") } };\n}\n\nlet parsed = null;\ntry {\n parsed = JSON.parse(bodyText);\n} catch (e) {\n parsed = null;\n}\n\nconst accessToken = parsed && typeof parsed.access_token === \"string\" ? parsed.access_token.trim() : \"\";\n\nif (!accessToken) {\n return {\n json: {\n _outcome: \"final\",\n success: false,\n error: {\n code: \"AUTHENTICATION_ERROR\",\n message: \"A resposta de identificação do paciente no Klingo não trouxe um access_token válido.\",\n },\n },\n };\n}\n\nconst prev = $('Montar corpo identificar').item.json;\nreturn {\n json: {\n _outcome: \"continue\",\n baseUrl: prev.baseUrl,\n slotId: prev.slotId,\n specialtyId: prev.specialtyId,\n accessToken,\n },\n};"
}
},
{
"id": "if-identificacao-ok",
"name": "Identificação OK?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [1560, -120],
"parameters": {
"conditions": {
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
"combinator": "and",
"conditions": [
{
"id": "cond-outcome-continue-2",
"leftValue": "={{ $json._outcome }}",
"rightValue": "continue",
"operator": { "type": "string", "operation": "equals" }
}
]
},
"options": {}
}
},
{
"id": "montar-reservar",
"name": "Montar corpo reservar",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1820, -240],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nconst slotId = (input.slotId || \"\").toString().trim();\nconst specialtyId = (input.specialtyId || \"\").toString().trim();\n\nif (!slotId) {\n return {\n json: {\n _outcome: \"final\",\n success: false,\n error: { code: \"VALIDATION_ERROR\", message: \"slotId é obrigatório para agendar no Klingo (obtido via consultarHorarios()).\" },\n },\n };\n}\nif (!specialtyId) {\n return {\n json: {\n _outcome: \"final\",\n success: false,\n error: { code: \"VALIDATION_ERROR\", message: \"specialtyId é obrigatório para agendar no Klingo (código do procedimento do horário selecionado).\" },\n },\n };\n}\n\nconst reservarBody = {\n procedimento: specialtyId,\n id: slotId,\n confirmado: \"Confirmado via MCP Server - Klingo\",\n};\n\nreturn {\n json: {\n _outcome: \"continue\",\n baseUrl: input.baseUrl,\n accessToken: input.accessToken,\n slotId,\n specialtyId,\n reservarBody,\n },\n};"
}
},
{
"id": "if-validacao-reservar",
"name": "Dados de reserva válidos?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [2080, -240],
"parameters": {
"conditions": {
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
"combinator": "and",
"conditions": [
{
"id": "cond-outcome-continue-3",
"leftValue": "={{ $json._outcome }}",
"rightValue": "continue",
"operator": { "type": "string", "operation": "equals" }
}
]
},
"options": {}
}
},
{
"id": "post-reservar",
"name": "POST /agenda/reservar",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [2340, -360],
"parameters": {
"method": "POST",
"url": "={{ $json.baseUrl }}/agenda/reservar",
"authentication": "none",
"sendHeaders": true,
"specifyHeaders": "keypair",
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "={{ 'Bearer ' + $json.accessToken }}" }
]
},
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.reservarBody) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
}
},
{
"id": "classificar-resultado",
"name": "Classificar resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [2600, -360],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\nconst bodyText = typeof rawBody === \"string\" ? rawBody : JSON.stringify(rawBody ?? \"\");\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do Klingo ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do Klingo ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do Klingo rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"KLINGO_API_ERROR\", message: `A API do Klingo retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"reservar o horário\") } };\n}\n\n// Qualquer 2xx é sucesso operacional. Tenta mapear o envelope 'reserva'\n// documentado (id numérico, data, hora, profissional); nunca inventa campos\n// que o corpo não confirma.\nlet parsed = null;\ntry {\n parsed = JSON.parse(bodyText);\n} catch (e) {\n parsed = null;\n}\n\nconst reservaOk = parsed && typeof parsed.id === \"number\";\n\nif (reservaOk) {\n return {\n json: {\n success: true,\n data: {\n id: String(parsed.id),\n data: typeof parsed.data === \"string\" ? parsed.data : undefined,\n hora: typeof parsed.hora === \"string\" ? parsed.hora : undefined,\n professionalId: parsed.profissional && parsed.profissional.id != null ? String(parsed.profissional.id) : undefined,\n professionalName: parsed.profissional && typeof parsed.profissional.nome === \"string\" ? parsed.profissional.nome : undefined,\n status: \"confirmed\",\n responseMappingPending: false,\n },\n },\n };\n}\n\nconst input = $('Montar corpo reservar').item.json;\nreturn {\n json: {\n success: true,\n data: {\n slotId: input.slotId,\n specialtyId: input.specialtyId,\n status: \"pending\",\n responseMappingPending: true,\n },\n },\n};"
}
}
],
"connections": {
"Execute Workflow Trigger": {
"main": [[{ "node": "Config Klingo", "type": "main", "index": 0 }]]
},
"Config Klingo": {
"main": [[{ "node": "Montar corpo identificar", "type": "main", "index": 0 }]]
},
"Montar corpo identificar": {
"main": [[{ "node": "Dados de identificação válidos?", "type": "main", "index": 0 }]]
},
"Dados de identificação válidos?": {
"main": [
[{ "node": "POST /paciente/identificar", "type": "main", "index": 0 }],
[]
]
},
"POST /paciente/identificar": {
"main": [[{ "node": "Processar identificação", "type": "main", "index": 0 }]]
},
"Processar identificação": {
"main": [[{ "node": "Identificação OK?", "type": "main", "index": 0 }]]
},
"Identificação OK?": {
"main": [
[{ "node": "Montar corpo reservar", "type": "main", "index": 0 }],
[]
]
},
"Montar corpo reservar": {
"main": [[{ "node": "Dados de reserva válidos?", "type": "main", "index": 0 }]]
},
"Dados de reserva válidos?": {
"main": [
[{ "node": "POST /agenda/reservar", "type": "main", "index": 0 }],
[]
]
},
"POST /agenda/reservar": {
"main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]]
}
}
}
```
Antes de chamar a API, **substitua** `"SUBSTITUIR_PELO_ID_DA_CREDENCIAL"` pelo ID real da credencial criada no pré-requisito (buscar via `n8n_manage_credentials` `action: "list"`, procurando `"Klingo APP Token - clínica pendente"`).
Guarde o `id` do workflow retornado como `<wf-klingo-agendar>`.
**Nota sobre os três outputs "erro final" (`Dados de identificação válidos?`, `Identificação OK?`, `Dados de reserva válidos?` no ramo FALSE):** cada um desses ramos termina sem conexão de saída — o item que sai da branch FALSE de cada `If` já tem o formato final `{ success: false, error: {...} }` (produzido pelo Code node anterior), e é isso que o `Call n8n Sub-Workflow Tool` devolve como resultado da tool quando o sub-workflow termina por ali. Não precisa de nenhum node adicional depois.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-klingo-agendar>"}`. Esperado: sem erros de estrutura/conexão. Se a credencial não tiver sido resolvida corretamente, o erro vai apontar isso — corrigir o ID antes de continuar.
- [ ] **Step 3: Revisão manual das expressões (sem chamada HTTP real)**
Sem clínica Klingo real disponível, não há como testar com uma execução de verdade nesta etapa. Em vez disso, abrir cada node `Code` e `If` na UI do n8n e conferir visualmente:
- `Montar corpo identificar` lê `$json.patientPhone`/`patientDocument`/`slotId`/`specialtyId`/`baseUrl` (vindos do `Config Klingo`, que passa adiante os campos do trigger via `includeOtherFields`).
- `Processar identificação` referencia `$('Montar corpo identificar').item.json` corretamente (nome do node bate exatamente com o nome usado na expressão).
- `Montar corpo reservar` referencia `$json.baseUrl`/`accessToken` (saída de `Processar identificação`).
- `Classificar resultado` referencia `$('Montar corpo reservar').item.json` corretamente.
- O header `Authorization` de `POST /agenda/reservar` usa `={{ 'Bearer ' + $json.accessToken }}` — não é uma Credential.
---
### Task 3: Workflow "MCP Server - Klingo" (o servidor)
**Interfaces:**
- Consumes: `<wf-klingo-agendar>` (Task 2), `<wf-klingo-consultar-horarios>` (Task 1).
- Produces: endpoint MCP em `https://devn8n.gtigrupo.com.br/mcp/mcp-server-klingo` (path a ajustar quando houver uma clínica real — ver Step 1).
- [ ] **Step 1: Criar o workflow com o trigger e os dois nodes de tool**
```json
{
"name": "MCP Server - Klingo",
"nodes": [
{
"id": "mcp-trigger",
"name": "MCP Server Trigger",
"type": "@n8n/n8n-nodes-langchain.mcpTrigger",
"typeVersion": 2,
"position": [0, 0],
"parameters": { "path": "mcp-server-klingo", "authentication": "none" }
},
{
"id": "tool-agendar",
"name": "agendar",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, -100],
"parameters": {
"description": "Reserva e confirma um horário já escolhido na clínica (Klingo). Exige slotId (id do horário) e specialtyId (código do procedimento) — ambos obtidos previamente, fora desta tool, já que consultar_horarios não está disponível para este provider no momento. Exige também o telefone do paciente para identificação.",
"source": "database",
"workflowId": { "value": "<wf-klingo-agendar>", "mode": "id" }
}
},
{
"id": "tool-consultar",
"name": "consultar_horarios",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, 100],
"parameters": {
"description": "Consulta horários disponíveis. Este provider (Klingo) não suporta essa consulta no momento — sempre retorna erro SLOT_MAPPING_UNAVAILABLE (lacuna de contrato conhecida da API pública do Klingo, não uma falha temporária).",
"source": "database",
"workflowId": { "value": "<wf-klingo-consultar-horarios>", "mode": "id" }
}
}
],
"connections": {
"agendar": {
"ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]]
},
"consultar_horarios": {
"ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]]
}
}
}
```
Substitua `<wf-klingo-agendar>`/`<wf-klingo-consultar-horarios>` pelos IDs reais das Tasks 1 e 2.
Guarde o `id` deste workflow como `<wf-mcp-server-klingo>`.
- [ ] **Step 2: Mapear os parâmetros da tool "agendar" na UI (passo manual — resourceMapper não é confiável via API)**
Abrir o node `agendar` na UI do n8n. Em "Workflow Inputs", mapear cada campo que o sub-workflow espera (`patientPhone`, `patientDocument`, `slotId`, `specialtyId`) para uma expressão `$fromAI(...)` com uma descrição clara pro modelo, mesmo padrão já validado no Z-PRO (ver `docs/n8n-agendamento-runbook.md`, seções 9 e 12). Como `slotId`/`specialtyId` não vêm de uma consulta de horários funcional neste provider, deixar claro na descrição de cada `$fromAI(...)` que esses valores só existem se o cliente/atendente já os informou por outro meio (ex.: repassados manualmente por um atendente humano) — não inventar uma forma de obtê-los.
- [ ] **Step 3: Validar (sem ativar)**
`n8n_validate_workflow` com `{"id": "<wf-mcp-server-klingo>"}`. Esperado: sem erros. **Não ativar** nenhum dos 3 workflows nesta etapa — não há workflow de agente cliente que os chame ainda (ver Global Constraints), e sub-workflows chamados via `Call n8n Sub-Workflow Tool` precisam estar `active: true` só quando estiverem realmente em uso (lição aprendida na migração do Z-PRO, documentada em `docs/n8n-agendamento-runbook.md`, seção 13). Ativar os 3 quando um workflow de agente cliente for construído, junto com esse trabalho.
---
### Task 4: Revisão final e commit
**Interfaces:**
- Consumes: workflows das Tasks 1-3.
- [ ] **Step 1: Validação estrutural final dos 3 workflows**
Rodar `n8n_validate_workflow` nos três IDs (`<wf-klingo-consultar-horarios>`, `<wf-klingo-agendar>`, `<wf-mcp-server-klingo>`) com `{"options": {"profile": "ai-friendly"}}`. Esperado: sem erros nos três. Anotar os 3 IDs e o path do endpoint MCP (`mcp-server-klingo`) em algum lugar acessível para quando o Step 3 da Task 3 for revisitado com uma clínica real.
- [ ] **Step 2: Atualizar a spec com os IDs reais**
Editar `docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-klingo-design.md`, adicionando ao final uma seção curta "Status desta etapa" com os 3 IDs de workflow criados, o path do endpoint MCP, e uma nota reafirmando que nada está ativo/testado com dado real ainda — pendente de uma clínica Klingo real.
- [ ] **Step 3: Commit**
```bash
cd "D:\PROJETOS\KLINGO-CLINICAL-SYSLIFE-GCHAT-AGENDAMENTO"
git add docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-klingo-design.md
git commit -m "docs: registra IDs dos workflows do MCP Server nativo (Klingo)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"
```
(Os workflows em si vivem no n8n, não em arquivo — não há mais nada pra commitar além da atualização da spec com os IDs. Opcional: exportar os 3 workflows com `n8n_get_workflow` e salvar em `docs/n8n-workflows/`, mesmo processo já usado para o Z-PRO.)
---
## Observação sobre execução
Assim como no plano do Z-PRO, estas tasks são chamadas de API ao n8n, não commits de código — a skill `executing-plans` deve tratar cada Step como uma chamada de ferramenta + validação, não um ciclo de teste automatizado. Sem clínica Klingo real disponível, a Task 2/Step 3 e a Task 3/Step 3 são revisão manual/estrutural, não execução real — isso é intencional e está registrado em Global Constraints, não é um passo pulado por engano.