From fa806c5eadf69e40de0d4bb66e72e9e6b96ac6c4 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Tue, 11 Aug 2026 08:51:36 -0300 Subject: [PATCH] Plano: MCP Server nativo no n8n para o ClinicAll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...6-08-11-mcp-server-nativo-n8n-clinicall.md | 1026 +++++++++++++++++ 1 file changed, 1026 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-clinicall.md diff --git a/docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-clinicall.md b/docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-clinicall.md new file mode 100644 index 0000000..c944766 --- /dev/null +++ b/docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-clinicall.md @@ -0,0 +1,1026 @@ +# 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 ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. 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 ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. 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 ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. 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 ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. 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 ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. 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 ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. 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 ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. 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 ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. Esperado: sem erros. + +--- + +### Task 9: Workflow "MCP Server - ClinicAll" (o servidor) + +**Interfaces:** +- Consumes: `` (Task 1), `` (Task 2), `` (Task 3), `` (Task 4), `` (Task 5), `` (Task 6), `` (Task 7), `` (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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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 `` pelos IDs reais das Tasks 1-8. + +Guarde o `id` deste workflow como ``. + +- [ ] **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": ""}`. 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 " +``` + +--- + +## 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.