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:
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.
|
||||
Reference in New Issue
Block a user