Files
KLINGO-CLINICAL-SYSLIFE-GCHAT/docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-clinicall.md
T
GustavoandClaude Sonnet 5 fa806c5ead Plano: MCP Server nativo no n8n para o ClinicAll
10 tasks: 5 sub-workflows de catálogo (unidade, especialidade,
convenio, profissional, procedimento) com esqueleto quase idêntico,
sub-workflow paciente, sub-workflow consultar_horarios (GET
/disponibilidade com route params), sub-workflow agendar (PUT
/agendamento com a mesma proteção 2xx=sucesso), servidor MCP com as
8 tools, e revisão final + commit. Nenhuma credencial criada
(mecanismo de auth desconhecido, authentication:none) -- sem
clínica ClinicAll real disponível, validação só estrutural.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 08:51:36 -03:00

1027 lines
48 KiB
Markdown

# MCP Server Nativo no n8n para o ClinicAll 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 ClinicAll inteiramente dentro do n8n (nodes oficiais `MCP Server Trigger` + `Call n8n Sub-Workflow Tool`), quarta aplicação do padrão já validado no Z-PRO e pronto estruturalmente no Klingo e no SysLife, cobrindo as 8 tools necessárias para um agendamento completo.
**Architecture:** 9 workflows n8n — o servidor (`MCP Server Trigger` + 8 tools) e oito sub-workflows portando os endpoints do `docs/API Clinicall.pdf` (Focus Tecnologia em Sistemas) para nodes `Code`/`HTTP Request`. Cinco tools de catálogo (`unidade`, `especialidade`, `convenio`, `profissional`, `procedimento`) seguem um esqueleto quase idêntico; `paciente` é parecida; `consultar_horarios` usa route params; `agendar` escreve de verdade (PUT) com a mesma proteção contra duplicidade (2xx sempre é sucesso) já usada nos outros três providers.
**Tech Stack:** n8n (`devn8n.gtigrupo.com.br`), nodes oficiais `@n8n/n8n-nodes-langchain` (`mcpTrigger`, `toolWorkflow`), `n8n-nodes-base` (`executeWorkflowTrigger`, `httpRequest`, `code`, `set`).
## Global Constraints
- Nenhum código TypeScript é alterado (não há adapter TypeScript pro ClinicAll — o manual PDF é a única fonte de verdade).
- Qualquer HTTP 2xx de `PUT /agendamento` é sucesso operacional, sem exceção — nunca reportar erro numa operação que o ClinicAll já aceitou (risco de agendamento duplicado/conflito).
- As 7 tools de leitura nunca reformatam o corpo de resposta em caso de sucesso — devolvem o corpo cru como veio.
- Classificação de erro (todas as 8 tools) é só por status HTTP, taxonomia genérica idêntica à já usada nos outros três providers — o manual não documenta nenhum código de erro específico do ClinicAll para estes controllers.
- **Nenhuma credencial é criada** — o mecanismo de autenticação real do ClinicAll é desconhecido (ver spec). Todos os nodes HTTP usam `authentication: none`. Isso é intencional, não um passo pulado.
- `/agendamento` usa **PUT** — suposição não verificada (o manual lista GET/POST/PUT/DELETE sem mapear qual verbo concilia um `idAgenda` existente). Documentado na spec; se ao testar contra uma clínica real `PUT` não funcionar, tentar `POST` antes de qualquer outra hipótese.
- As chaves de query string opcionais dos endpoints de catálogo (`nome`) são uma inferência razoável a partir do padrão de nomenclatura do manual (não há exemplo de URL com esse parâmetro no documento) — só `grp` (em `procedimentos`) tem exemplo literal (`?grp=consulta`). Documentado como suposição, não fato confirmado.
- Sem clínica ClinicAll real disponível: nenhuma chamada HTTP real é feita. Validação é só estrutural (`n8n_validate_workflow`). Os 9 workflows ficam **inativos** ao final.
Spec de referência: `docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-clinicall-design.md`
---
### Task 1: Sub-workflow "MCP Server - ClinicAll - unidade"
**Interfaces:**
- Produces: workflow `MCP Server - ClinicAll - unidade`, recebe `nome` (opcional), retorna o corpo cru de `GET /datasnap/rest/TEntityController/unidades` ou erro classificado.
- [ ] **Step 1: Criar o workflow**
```json
{
"name": "MCP Server - ClinicAll - unidade",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": { "values": [{ "name": "nome", "type": "string" }] }
}
},
{
"id": "config",
"name": "Config ClinicAll",
"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": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" }
]
}
}
},
{
"id": "montar-query",
"name": "Montar query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nconst queryParams = {};\nif (input.nome) queryParams.nome = input.nome;\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryParams,\n },\n};"
}
},
{
"id": "get-unidades",
"name": "GET /unidades",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "GET",
"url": "={{ $json.baseUrl }}/datasnap/rest/TEntityController/unidades",
"authentication": "none",
"sendQuery": true,
"specifyQuery": "json",
"jsonQuery": "={{ JSON.stringify($json.queryParams) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
}
},
{
"id": "classificar",
"name": "Classificar resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1040, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar unidades\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
}
}
],
"connections": {
"Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] },
"Config ClinicAll": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] },
"Montar query": { "main": [[{ "node": "GET /unidades", "type": "main", "index": 0 }]] },
"GET /unidades": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
Guarde o `id` retornado como `<wf-unidade>`.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-unidade>"}`. Esperado: sem erros.
---
### Task 2: Sub-workflow "MCP Server - ClinicAll - especialidade"
**Interfaces:**
- Produces: workflow `MCP Server - ClinicAll - especialidade`, recebe `nome` (opcional), retorna o corpo cru de `GET /datasnap/rest/TEntityController/especialidades` ou erro classificado.
- [ ] **Step 1: Criar o workflow**
Mesmo esqueleto exato da Task 1, trocando endpoint e mensagem de contexto:
```json
{
"name": "MCP Server - ClinicAll - especialidade",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": { "values": [{ "name": "nome", "type": "string" }] }
}
},
{
"id": "config",
"name": "Config ClinicAll",
"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": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" }
]
}
}
},
{
"id": "montar-query",
"name": "Montar query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nconst queryParams = {};\nif (input.nome) queryParams.nome = input.nome;\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryParams,\n },\n};"
}
},
{
"id": "get-especialidades",
"name": "GET /especialidades",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "GET",
"url": "={{ $json.baseUrl }}/datasnap/rest/TEntityController/especialidades",
"authentication": "none",
"sendQuery": true,
"specifyQuery": "json",
"jsonQuery": "={{ JSON.stringify($json.queryParams) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
}
},
{
"id": "classificar",
"name": "Classificar resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1040, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar especialidades\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
}
}
],
"connections": {
"Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] },
"Config ClinicAll": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] },
"Montar query": { "main": [[{ "node": "GET /especialidades", "type": "main", "index": 0 }]] },
"GET /especialidades": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
Guarde o `id` retornado como `<wf-especialidade>`.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-especialidade>"}`. Esperado: sem erros.
---
### Task 3: Sub-workflow "MCP Server - ClinicAll - convenio"
**Interfaces:**
- Produces: workflow `MCP Server - ClinicAll - convenio`, recebe `nome` (opcional), retorna o corpo cru de `GET /datasnap/rest/TEntityController/convenios` ou erro classificado.
- [ ] **Step 1: Criar o workflow**
Mesmo esqueleto exato da Task 1/2:
```json
{
"name": "MCP Server - ClinicAll - convenio",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": { "values": [{ "name": "nome", "type": "string" }] }
}
},
{
"id": "config",
"name": "Config ClinicAll",
"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": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" }
]
}
}
},
{
"id": "montar-query",
"name": "Montar query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nconst queryParams = {};\nif (input.nome) queryParams.nome = input.nome;\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryParams,\n },\n};"
}
},
{
"id": "get-convenios",
"name": "GET /convenios",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "GET",
"url": "={{ $json.baseUrl }}/datasnap/rest/TEntityController/convenios",
"authentication": "none",
"sendQuery": true,
"specifyQuery": "json",
"jsonQuery": "={{ JSON.stringify($json.queryParams) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
}
},
{
"id": "classificar",
"name": "Classificar resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1040, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar convênios\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
}
}
],
"connections": {
"Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] },
"Config ClinicAll": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] },
"Montar query": { "main": [[{ "node": "GET /convenios", "type": "main", "index": 0 }]] },
"GET /convenios": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
Guarde o `id` retornado como `<wf-convenio>`.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-convenio>"}`. Esperado: sem erros.
---
### Task 4: Sub-workflow "MCP Server - ClinicAll - profissional"
**Interfaces:**
- Produces: workflow `MCP Server - ClinicAll - profissional`, recebe `nome` (opcional), retorna o corpo cru de `GET /datasnap/rest/TEntityController/profissionais` (já inclui `especialidades[]`/`horarios[]` de cada profissional) ou erro classificado.
- [ ] **Step 1: Criar o workflow**
Mesmo esqueleto exato da Task 1/2/3:
```json
{
"name": "MCP Server - ClinicAll - profissional",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": { "values": [{ "name": "nome", "type": "string" }] }
}
},
{
"id": "config",
"name": "Config ClinicAll",
"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": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" }
]
}
}
},
{
"id": "montar-query",
"name": "Montar query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nconst queryParams = {};\nif (input.nome) queryParams.nome = input.nome;\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryParams,\n },\n};"
}
},
{
"id": "get-profissionais",
"name": "GET /profissionais",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "GET",
"url": "={{ $json.baseUrl }}/datasnap/rest/TEntityController/profissionais",
"authentication": "none",
"sendQuery": true,
"specifyQuery": "json",
"jsonQuery": "={{ JSON.stringify($json.queryParams) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
}
},
{
"id": "classificar",
"name": "Classificar resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1040, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar profissionais\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
}
}
],
"connections": {
"Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] },
"Config ClinicAll": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] },
"Montar query": { "main": [[{ "node": "GET /profissionais", "type": "main", "index": 0 }]] },
"GET /profissionais": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
Guarde o `id` retornado como `<wf-profissional>`.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-profissional>"}`. Esperado: sem erros.
---
### Task 5: Sub-workflow "MCP Server - ClinicAll - procedimento"
**Interfaces:**
- Produces: workflow `MCP Server - ClinicAll - procedimento`, recebe `nome` e `grp` (ambos opcionais), retorna o corpo cru de `GET /datasnap/rest/TEntityController/procedimentos` ou erro classificado.
- [ ] **Step 1: Criar o workflow**
```json
{
"name": "MCP Server - ClinicAll - procedimento",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{ "name": "nome", "type": "string" },
{ "name": "grp", "type": "string" }
]
}
}
},
{
"id": "config",
"name": "Config ClinicAll",
"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": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" }
]
}
}
},
{
"id": "montar-query",
"name": "Montar query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nconst queryParams = {};\nif (input.nome) queryParams.nome = input.nome;\nif (input.grp) queryParams.grp = input.grp;\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryParams,\n },\n};"
}
},
{
"id": "get-procedimentos",
"name": "GET /procedimentos",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "GET",
"url": "={{ $json.baseUrl }}/datasnap/rest/TEntityController/procedimentos",
"authentication": "none",
"sendQuery": true,
"specifyQuery": "json",
"jsonQuery": "={{ JSON.stringify($json.queryParams) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
}
},
{
"id": "classificar",
"name": "Classificar resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1040, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar procedimentos\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
}
}
],
"connections": {
"Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] },
"Config ClinicAll": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] },
"Montar query": { "main": [[{ "node": "GET /procedimentos", "type": "main", "index": 0 }]] },
"GET /procedimentos": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
Guarde o `id` retornado como `<wf-procedimento>`.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-procedimento>"}`. Esperado: sem erros.
---
### Task 6: Sub-workflow "MCP Server - ClinicAll - paciente"
**Interfaces:**
- Produces: workflow `MCP Server - ClinicAll - paciente`, recebe `busca` (nome ou id do paciente), retorna o corpo cru de `GET /datasnap/rest/TPatientController/paciente` ou erro classificado.
- [ ] **Step 1: Criar o workflow**
```json
{
"name": "MCP Server - ClinicAll - paciente",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": { "values": [{ "name": "busca", "type": "string" }] }
}
},
{
"id": "config",
"name": "Config ClinicAll",
"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": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" }
]
}
}
},
{
"id": "montar-query",
"name": "Montar query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryParams: { busca: input.busca },\n },\n};"
}
},
{
"id": "get-paciente",
"name": "GET /paciente",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "GET",
"url": "={{ $json.baseUrl }}/datasnap/rest/TPatientController/paciente",
"authentication": "none",
"sendQuery": true,
"specifyQuery": "json",
"jsonQuery": "={{ JSON.stringify($json.queryParams) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
}
},
{
"id": "classificar",
"name": "Classificar resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1040, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar paciente\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
}
}
],
"connections": {
"Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] },
"Config ClinicAll": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] },
"Montar query": { "main": [[{ "node": "GET /paciente", "type": "main", "index": 0 }]] },
"GET /paciente": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
Guarde o `id` retornado como `<wf-paciente>`.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-paciente>"}`. Esperado: sem erros.
---
### Task 7: Sub-workflow "MCP Server - ClinicAll - consultar_horarios"
**Interfaces:**
- Produces: workflow `MCP Server - ClinicAll - consultar_horarios`, recebe `idUnidade`, `idProfissional` (obrigatórios), `meses`, `procedimento`, `especialidade`, `convenio` (opcionais), retorna o corpo cru de `GET /datasnap/rest/TCalendarController/disponibilidade/{idUnidade}/{idProfissional}` ou erro classificado.
- [ ] **Step 1: Criar o workflow**
```json
{
"name": "MCP Server - ClinicAll - consultar_horarios",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{ "name": "idUnidade", "type": "string" },
{ "name": "idProfissional", "type": "string" },
{ "name": "meses", "type": "string" },
{ "name": "procedimento", "type": "string" },
{ "name": "especialidade", "type": "string" },
{ "name": "convenio", "type": "string" }
]
}
}
},
{
"id": "config",
"name": "Config ClinicAll",
"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": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" }
]
}
}
},
{
"id": "montar-url-query",
"name": "Montar URL e query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nconst queryParams = {};\nif (input.meses) queryParams.m = input.meses;\nif (input.procedimento) queryParams.p = input.procedimento;\nif (input.especialidade) queryParams.e = input.especialidade;\nif (input.convenio) queryParams.c = input.convenio;\n\nreturn {\n json: {\n url: `${input.baseUrl}/datasnap/rest/TCalendarController/disponibilidade/${input.idUnidade}/${input.idProfissional}`,\n queryParams,\n },\n};"
}
},
{
"id": "get-disponibilidade",
"name": "GET /disponibilidade",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "GET",
"url": "={{ $json.url }}",
"authentication": "none",
"sendQuery": true,
"specifyQuery": "json",
"jsonQuery": "={{ JSON.stringify($json.queryParams) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
}
},
{
"id": "classificar",
"name": "Classificar resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1040, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar horários disponíveis\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
}
}
],
"connections": {
"Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] },
"Config ClinicAll": { "main": [[{ "node": "Montar URL e query", "type": "main", "index": 0 }]] },
"Montar URL e query": { "main": [[{ "node": "GET /disponibilidade", "type": "main", "index": 0 }]] },
"GET /disponibilidade": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
Guarde o `id` retornado como `<wf-consultar-horarios>`.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-consultar-horarios>"}`. Esperado: sem erros.
---
### Task 8: Sub-workflow "MCP Server - ClinicAll - agendar"
**Interfaces:**
- Produces: workflow `MCP Server - ClinicAll - agendar`, recebe `idAgenda` (obrigatório, vem de `consultar_horarios`), `idUnidade`, `idPaciente`, `idProfissional`, `idProcedimento`, `dataAgenda`, `horaAgenda`, `idConvenio`, `carteiraConvenio`, `validadeCarteiraConvenio`, `usuario` (obrigatório), `senhaAutorizacao` (opcional), retorna `{ success: true, data: {...} }` ou `{ success: false, error: {...} }`.
- [ ] **Step 1: Criar o workflow**
```json
{
"name": "MCP Server - ClinicAll - 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": "idAgenda", "type": "string" },
{ "name": "idUnidade", "type": "string" },
{ "name": "idPaciente", "type": "string" },
{ "name": "idProfissional", "type": "string" },
{ "name": "idProcedimento", "type": "string" },
{ "name": "dataAgenda", "type": "string" },
{ "name": "horaAgenda", "type": "string" },
{ "name": "idConvenio", "type": "string" },
{ "name": "carteiraConvenio", "type": "string" },
{ "name": "validadeCarteiraConvenio", "type": "string" },
{ "name": "usuario", "type": "string" },
{ "name": "senhaAutorizacao", "type": "string" }
]
}
}
},
{
"id": "config",
"name": "Config ClinicAll",
"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": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" }
]
}
}
},
{
"id": "montar-corpo",
"name": "Montar corpo",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nconst requestBody = {\n idAgenda: input.idAgenda,\n idUnidade: input.idUnidade,\n idPaciente: input.idPaciente,\n idProfissional: input.idProfissional,\n idProcedimento: input.idProcedimento,\n dataAgenda: input.dataAgenda,\n horaAgenda: input.horaAgenda,\n idConvenio: input.idConvenio,\n carteiraConvenio: input.carteiraConvenio,\n validadeCarteiraConvenio: input.validadeCarteiraConvenio,\n usuario: input.usuario,\n};\nif (input.senhaAutorizacao) requestBody.senhaAutorizacao = input.senhaAutorizacao;\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n requestBody,\n },\n};"
}
},
{
"id": "put-agendamento",
"name": "PUT /agendamento",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "PUT",
"url": "={{ $json.baseUrl }}/datasnap/rest/TCalendarController/agendamento",
"authentication": "none",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.requestBody) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
}
},
{
"id": "classificar",
"name": "Classificar resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1040, 0],
"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 ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"criar o agendamento\") } };\n}\n\nlet parsed = null;\ntry {\n parsed = JSON.parse(bodyText);\n} catch (e) {\n parsed = null;\n}\n\nconst idAgendaConfirmado = parsed && (parsed.idAgenda !== undefined && parsed.idAgenda !== null) ? String(parsed.idAgenda) : null;\n\nif (idAgendaConfirmado) {\n return {\n json: {\n success: true,\n data: {\n id: idAgendaConfirmado,\n situacaoAgenda: typeof parsed.situacaoAgenda === \"string\" ? parsed.situacaoAgenda : undefined,\n estadoAgenda: typeof parsed.estadoAgenda === \"string\" ? parsed.estadoAgenda : undefined,\n responseMappingPending: false,\n },\n },\n };\n}\n\nconst input = $('Montar corpo').item.json;\nreturn {\n json: {\n success: true,\n data: {\n id: input.requestBody.idAgenda,\n dataAgenda: input.requestBody.dataAgenda,\n horaAgenda: input.requestBody.horaAgenda,\n responseMappingPending: true,\n },\n },\n};"
}
}
],
"connections": {
"Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] },
"Config ClinicAll": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] },
"Montar corpo": { "main": [[{ "node": "PUT /agendamento", "type": "main", "index": 0 }]] },
"PUT /agendamento": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
Guarde o `id` retornado como `<wf-agendar>`.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com `{"id": "<wf-agendar>"}`. Esperado: sem erros.
---
### Task 9: Workflow "MCP Server - ClinicAll" (o servidor)
**Interfaces:**
- Consumes: `<wf-unidade>` (Task 1), `<wf-especialidade>` (Task 2), `<wf-convenio>` (Task 3), `<wf-profissional>` (Task 4), `<wf-procedimento>` (Task 5), `<wf-paciente>` (Task 6), `<wf-consultar-horarios>` (Task 7), `<wf-agendar>` (Task 8).
- Produces: endpoint MCP em `https://devn8n.gtigrupo.com.br/mcp/mcp-server-clinicall`.
- [ ] **Step 1: Criar o workflow com o trigger e os oito nodes de tool**
```json
{
"name": "MCP Server - ClinicAll",
"nodes": [
{
"id": "mcp-trigger",
"name": "MCP Server Trigger",
"type": "@n8n/n8n-nodes-langchain.mcpTrigger",
"typeVersion": 2,
"position": [0, 0],
"parameters": { "path": "mcp-server-clinicall", "authentication": "none" }
},
{
"id": "tool-unidade",
"name": "unidade",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, -280],
"parameters": {
"description": "Lista as unidades físicas da clínica, com nome opcional para filtrar. Sem parâmetros obrigatórios.",
"source": "database",
"workflowId": { "value": "<wf-unidade>", "mode": "id" }
}
},
{
"id": "tool-especialidade",
"name": "especialidade",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, -200],
"parameters": {
"description": "Lista as especialidades médicas disponíveis, com nome opcional para filtrar. Sem parâmetros obrigatórios.",
"source": "database",
"workflowId": { "value": "<wf-especialidade>", "mode": "id" }
}
},
{
"id": "tool-convenio",
"name": "convenio",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, -120],
"parameters": {
"description": "Lista os convênios aceitos pela clínica, com nome opcional para filtrar. Sem parâmetros obrigatórios.",
"source": "database",
"workflowId": { "value": "<wf-convenio>", "mode": "id" }
}
},
{
"id": "tool-profissional",
"name": "profissional",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, -40],
"parameters": {
"description": "Lista os profissionais/médicos, com nome opcional para filtrar. Já retorna as especialidades e horários de atendimento de cada um.",
"source": "database",
"workflowId": { "value": "<wf-profissional>", "mode": "id" }
}
},
{
"id": "tool-procedimento",
"name": "procedimento",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, 40],
"parameters": {
"description": "Lista os procedimentos disponíveis, com nome e/ou grupo opcionais para filtrar. Sem parâmetros obrigatórios.",
"source": "database",
"workflowId": { "value": "<wf-procedimento>", "mode": "id" }
}
},
{
"id": "tool-paciente",
"name": "paciente",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, 120],
"parameters": {
"description": "Busca um paciente já cadastrado por nome ou id.",
"source": "database",
"workflowId": { "value": "<wf-paciente>", "mode": "id" }
}
},
{
"id": "tool-consultar-horarios",
"name": "consultar_horarios",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, 200],
"parameters": {
"description": "Consulta horários disponíveis para uma unidade e profissional específicos (ambos obrigatórios, códigos obtidos via as tools unidade e profissional). Retorna idAgenda e horaAgenda de cada horário livre — o idAgenda é usado depois na tool agendar.",
"source": "database",
"workflowId": { "value": "<wf-consultar-horarios>", "mode": "id" }
}
},
{
"id": "tool-agendar",
"name": "agendar",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [260, 280],
"parameters": {
"description": "Cria o agendamento real na clínica, conciliando um idAgenda livre já obtido via consultar_horarios. Use somente depois de já ter resolvido, com as outras tools, todos os códigos necessários (paciente, profissional, unidade, procedimento, convênio).",
"source": "database",
"workflowId": { "value": "<wf-agendar>", "mode": "id" }
}
}
],
"connections": {
"unidade": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
"especialidade": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
"convenio": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
"profissional": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
"procedimento": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
"paciente": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
"consultar_horarios": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
"agendar": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }
}
}
```
Substitua os 8 `<wf-...>` pelos IDs reais das Tasks 1-8.
Guarde o `id` deste workflow como `<wf-mcp-server-clinicall>`.
- [ ] **Step 2: Mapear os parâmetros de cada tool na UI (passo manual — resourceMapper não é confiável via API)**
Abrir os 7 nodes de tool com parâmetro (todos exceto `unidade`... na verdade `unidade` também tem `nome` opcional — todos os 8 têm ao menos um campo) na UI do n8n. Em "Workflow Inputs", mapear cada campo para uma expressão `$fromAI(...)` com descrição clara pro modelo. Exemplo para `agendar`:
```
idAgenda: ={{ $fromAI('idAgenda', 'Id do horário livre escolhido, retornado por consultar_horarios', 'string') }}
idUnidade: ={{ $fromAI('idUnidade', 'Código da unidade retornado pela tool unidade', 'string') }}
idPaciente: ={{ $fromAI('idPaciente', 'Código do paciente retornado pela tool paciente', 'string') }}
idProfissional: ={{ $fromAI('idProfissional', 'Código do profissional retornado pela tool profissional', 'string') }}
idProcedimento: ={{ $fromAI('idProcedimento', 'Código do procedimento retornado pela tool procedimento', 'string') }}
dataAgenda: ={{ $fromAI('dataAgenda', 'Data do agendamento, no mesmo formato retornado por consultar_horarios', 'string') }}
horaAgenda: ={{ $fromAI('horaAgenda', 'Hora do agendamento, no mesmo formato retornado por consultar_horarios', 'string') }}
idConvenio: ={{ $fromAI('idConvenio', 'Código do convênio retornado pela tool convenio', 'string') }}
carteiraConvenio: ={{ $fromAI('carteiraConvenio', 'Número da carteirinha do convênio do paciente', 'string') }}
validadeCarteiraConvenio: ={{ $fromAI('validadeCarteiraConvenio', 'Data de validade da carteirinha do convênio', 'string') }}
usuario: ={{ $fromAI('usuario', 'Nome de identificação de quem está realizando o agendamento (ex.: nome do assistente/atendente virtual)', 'string') }}
senhaAutorizacao: ={{ $fromAI('senhaAutorizacao', 'Senha de autorização do agendamento, se aplicável (opcional)', 'string') }}
```
Mesmo padrão pros demais 6 nodes com parâmetro, usando os nomes de campo já definidos nas Tasks 1-7 (`nome` para os de catálogo, `grp` adicional em `procedimento`, `busca` em `paciente`, `idUnidade`/`idProfissional`/`meses`/`procedimento`/`especialidade`/`convenio` em `consultar_horarios`).
- [ ] **Step 3: Validar (sem ativar)**
`n8n_validate_workflow` com `{"id": "<wf-mcp-server-clinicall>"}`. Esperado: sem erros. **Não ativar** nenhum dos 9 workflows nesta etapa — sem clínica real, sem workflow de agente cliente, e sem o mecanismo de autenticação resolvido (ver Global Constraints).
---
### Task 10: Revisão final e commit
**Interfaces:**
- Consumes: workflows das Tasks 1-9.
- [ ] **Step 1: Validação estrutural final**
Rodar `n8n_validate_workflow` nos 9 IDs com `{"options": {"profile": "ai-friendly"}}`. Esperado: sem erros em nenhum.
- [ ] **Step 2: Atualizar a spec com os IDs reais**
Editar `docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-clinicall-design.md`, adicionando ao final uma seção "Status desta etapa" com os 9 IDs de workflow criados, o path do endpoint MCP (`mcp-server-clinicall`), e reafirmando as duas pendências reais (autenticação e verbo HTTP do agendamento) como não resolvidas.
- [ ] **Step 3: Commit**
```bash
cd "D:\PROJETOS\KLINGO-CLINICAL-SYSLIFE-GCHAT-AGENDAMENTO"
git add "docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-clinicall-design.md"
git commit -m "docs: registra IDs dos workflows do MCP Server nativo (ClinicAll)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"
```
---
## Observação sobre execução
Mesmo padrão dos planos anteriores (Z-PRO, Klingo, SysLife): tasks são chamadas de API ao n8n, não commits de código. Sem clínica ClinicAll real e sem mecanismo de autenticação resolvido, o resultado desta etapa é estrutura pronta e validada, não um fluxo testado de ponta a ponta — isso é intencional (ver Global Constraints), não um passo pulado por engano. As Tasks 1-4 (unidade, especialidade, convenio, profissional) têm esqueleto quase idêntico entre si — proposital, mesmo padrão do manual, não duplicação acidental a refatorar.