From 1968a6ae281782348f2492b5f57041561c2d62fb Mon Sep 17 00:00:00 2001 From: Gustavo Date: Tue, 11 Aug 2026 00:07:55 -0300 Subject: [PATCH] Plano: MCP Server nativo no n8n para o SysLife MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 9 tasks: 6 sub-workflows de consulta/apoio (especialidade, medico, convenio, paciente, procedimento, consultar_horarios) com esqueleto quase idêntico entre si, sub-workflow agendar (PUT /Agenda com a mesma proteção contra duplicidade 2xx=sucesso), servidor MCP com as 7 tools, e revisão final + commit. Sem clínica SysLife 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 --- ...026-08-11-mcp-server-nativo-n8n-syslife.md | 934 ++++++++++++++++++ 1 file changed, 934 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-syslife.md diff --git a/docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-syslife.md b/docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-syslife.md new file mode 100644 index 0000000..39e58bc --- /dev/null +++ b/docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-syslife.md @@ -0,0 +1,934 @@ +# MCP Server Nativo no n8n para o SysLife 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 SysLife inteiramente dentro do n8n (nodes oficiais `MCP Server Trigger` + `Call n8n Sub-Workflow Tool`), terceira aplicação do padrão já validado no Z-PRO e pronto estruturalmente no Klingo, cobrindo as 7 tools necessárias para um agendamento completo. + +**Architecture:** 8 workflows n8n — o servidor (`MCP Server Trigger` + 7 tools) e sete sub-workflows portando os recursos do `Manual WS SysLife V3.4.pdf` para nodes `Code`/`HTTP Request`. Seis tools de leitura (`especialidade`, `medico`, `convenio`, `paciente`, `procedimento`, `consultar_horarios`) seguem um esqueleto quase idêntico; a sétima (`agendar`) escreve de verdade e usa a mesma proteção contra duplicidade (2xx sempre é sucesso) já usada no Z-PRO/Klingo. + +**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 SysLife — o manual PDF é a única fonte de verdade). +- Qualquer HTTP 2xx de `PUT /Agenda` é sucesso operacional, sem exceção — nunca reportar erro numa operação que o SysLife já aceitou (risco de agendamento duplicado). +- As 6 tools de leitura nunca reformatam o corpo de resposta em caso de sucesso — devolvem o corpo cru como veio, porque o manual não garante nenhum envelope fixo além do que documenta e não há exemplo de resposta real capturado. +- Classificação de erro (todas as 7 tools) é só por status HTTP, taxonomia genérica idêntica à já usada no Z-PRO/Klingo — o manual não documenta nenhum código de erro específico do SysLife. +- Nenhuma credencial é inserida por quem executa este plano de forma automática — a credencial Basic Auth do SysLife é criada manualmente pelo usuário na UI do n8n. +- Sem clínica SysLife real disponível: nenhuma chamada HTTP real é feita. Validação é só estrutural (`n8n_validate_workflow`). Os 8 workflows ficam **inativos** ao final. + +Spec de referência: `docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-syslife-design.md` + +--- + +## Pré-requisito (ação manual do usuário) + +Criar no n8n uma Credential do tipo **Basic Auth** (Settings → Credentials → New → "Basic Auth"): +- **User**: `WebSysLife` (documentado no manual) +- **Password**: `$Sys10Life` (documentado no manual — texto fixo do WebService, não é segredo por clínica) + +Nomeie como `SysLife Basic Auth - clínica pendente`. Anote o ID gerado — será necessário nas Tasks 1-7. + +--- + +### Task 1: Sub-workflow "MCP Server - SysLife - especialidade" + +**Interfaces:** +- Consumes: credencial `SysLife Basic Auth - clínica pendente` do pré-requisito. +- Produces: um workflow n8n chamado `MCP Server - SysLife - especialidade`, sem parâmetros de entrada, que retorna o corpo cru de `GET /Especialidade` ou um erro classificado. + +- [ ] **Step 1: Criar o workflow** + +```json +{ + "name": "MCP Server - SysLife - especialidade", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.2, + "position": [0, 0], + "parameters": { "inputSource": "passthrough" } + }, + { + "id": "config", + "name": "Config SysLife", + "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-IP-REAL-DA-CLINICA:9000" } + ] + } + } + }, + { + "id": "get-especialidade", + "name": "GET /Especialidade", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [520, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/Especialidade", + "authentication": "genericCredentialType", + "genericAuthType": "httpBasicAuth", + "sendBody": false, + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + }, + "credentials": { + "httpBasicAuth": { "id": "SUBSTITUIR_PELO_ID_DA_CREDENCIAL", "name": "SysLife Basic Auth - clínica pendente" } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [780, 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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife 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\n// Sucesso: devolve o corpo cru, sem reformatar (sem envelope fixo documentado).\nreturn { json: { success: true, data: rawBody } };" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config SysLife", "type": "main", "index": 0 }]] }, + "Config SysLife": { "main": [[{ "node": "GET /Especialidade", "type": "main", "index": 0 }]] }, + "GET /Especialidade": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Substitua `"SUBSTITUIR_PELO_ID_DA_CREDENCIAL"` pelo ID real da credencial (buscar via `n8n_manage_credentials` `action: "list"`, procurando `"SysLife Basic Auth - clínica pendente"`). + +Guarde o `id` retornado como ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. Esperado: sem erros. + +--- + +### Task 2: Sub-workflow "MCP Server - SysLife - medico" + +**Interfaces:** +- Consumes: credencial do pré-requisito. +- Produces: workflow `MCP Server - SysLife - medico`, recebe `codigoEspecialidade`, `codigoOrigem`, retorna o corpo cru de `GET /Medico` ou erro classificado. + +- [ ] **Step 1: Criar o workflow** + +```json +{ + "name": "MCP Server - SysLife - medico", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.2, + "position": [0, 0], + "parameters": { + "inputSource": "workflowInputs", + "workflowInputs": { + "values": [ + { "name": "codigoEspecialidade", "type": "string" }, + { "name": "codigoOrigem", "type": "string" } + ] + } + } + }, + { + "id": "config", + "name": "Config SysLife", + "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-IP-REAL-DA-CLINICA:9000" } + ] + } + } + }, + { + "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 queryBody: {\n CodigoEspecialidade: input.codigoEspecialidade,\n CodigoOrigem: input.codigoOrigem,\n },\n },\n};" + } + }, + { + "id": "get-medico", + "name": "GET /Medico", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [780, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/Medico", + "authentication": "genericCredentialType", + "genericAuthType": "httpBasicAuth", + "sendBody": true, + "contentType": "json", + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.queryBody) }}", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + }, + "credentials": { + "httpBasicAuth": { "id": "SUBSTITUIR_PELO_ID_DA_CREDENCIAL", "name": "SysLife Basic Auth - clínica pendente" } + } + }, + { + "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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar médicos\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config SysLife", "type": "main", "index": 0 }]] }, + "Config SysLife": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] }, + "Montar query": { "main": [[{ "node": "GET /Medico", "type": "main", "index": 0 }]] }, + "GET /Medico": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Substitua a credencial. Guarde o `id` retornado como ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. Esperado: sem erros. + +--- + +### Task 3: Sub-workflow "MCP Server - SysLife - convenio" + +**Interfaces:** +- Consumes: credencial do pré-requisito. +- Produces: workflow `MCP Server - SysLife - convenio`, sem parâmetros de entrada, retorna o corpo cru de `GET /Convenio` ou erro classificado. + +- [ ] **Step 1: Criar o workflow** + +Mesmo esqueleto exato da Task 1 (`especialidade`), só trocando nome do workflow, node HTTP e mensagem de contexto do erro: + +```json +{ + "name": "MCP Server - SysLife - convenio", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.2, + "position": [0, 0], + "parameters": { "inputSource": "passthrough" } + }, + { + "id": "config", + "name": "Config SysLife", + "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-IP-REAL-DA-CLINICA:9000" } + ] + } + } + }, + { + "id": "get-convenio", + "name": "GET /Convenio", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [520, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/Convenio", + "authentication": "genericCredentialType", + "genericAuthType": "httpBasicAuth", + "sendBody": false, + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + }, + "credentials": { + "httpBasicAuth": { "id": "SUBSTITUIR_PELO_ID_DA_CREDENCIAL", "name": "SysLife Basic Auth - clínica pendente" } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [780, 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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife 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 SysLife", "type": "main", "index": 0 }]] }, + "Config SysLife": { "main": [[{ "node": "GET /Convenio", "type": "main", "index": 0 }]] }, + "GET /Convenio": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Substitua a credencial. Guarde o `id` retornado como ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. Esperado: sem erros. + +--- + +### Task 4: Sub-workflow "MCP Server - SysLife - paciente" + +**Interfaces:** +- Consumes: credencial do pré-requisito. +- Produces: workflow `MCP Server - SysLife - paciente`, recebe `cpf` (aceita CPF, matrícula ou celular, conforme documentado), retorna o corpo cru de `GET /Paciente` ou erro classificado. + +- [ ] **Step 1: Criar o workflow** + +```json +{ + "name": "MCP Server - SysLife - 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": "cpf", "type": "string" }] } + } + }, + { + "id": "config", + "name": "Config SysLife", + "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-IP-REAL-DA-CLINICA:9000" } + ] + } + } + }, + { + "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 queryBody: { CPF: input.cpf },\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 }}/Paciente", + "authentication": "genericCredentialType", + "genericAuthType": "httpBasicAuth", + "sendBody": true, + "contentType": "json", + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.queryBody) }}", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + }, + "credentials": { + "httpBasicAuth": { "id": "SUBSTITUIR_PELO_ID_DA_CREDENCIAL", "name": "SysLife Basic Auth - clínica pendente" } + } + }, + { + "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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife 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 SysLife", "type": "main", "index": 0 }]] }, + "Config SysLife": { "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 }]] } + } +} +``` + +Substitua a credencial. Guarde o `id` retornado como ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. Esperado: sem erros. + +--- + +### Task 5: Sub-workflow "MCP Server - SysLife - procedimento" + +**Interfaces:** +- Consumes: credencial do pré-requisito. +- Produces: workflow `MCP Server - SysLife - procedimento`, recebe `codigoConvenio`, `codigoPlano`, `codigoMedico`, retorna o corpo cru de `GET /Procedimento` ou erro classificado. + +- [ ] **Step 1: Criar o workflow** + +```json +{ + "name": "MCP Server - SysLife - 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": "codigoConvenio", "type": "string" }, + { "name": "codigoPlano", "type": "string" }, + { "name": "codigoMedico", "type": "string" } + ] + } + } + }, + { + "id": "config", + "name": "Config SysLife", + "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-IP-REAL-DA-CLINICA:9000" } + ] + } + } + }, + { + "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 queryBody: {\n CodigoConvenio: input.codigoConvenio,\n CodigoPlano: input.codigoPlano,\n CodigoMedico: input.codigoMedico,\n },\n },\n};" + } + }, + { + "id": "get-procedimento", + "name": "GET /Procedimento", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [780, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/Procedimento", + "authentication": "genericCredentialType", + "genericAuthType": "httpBasicAuth", + "sendBody": true, + "contentType": "json", + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.queryBody) }}", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + }, + "credentials": { + "httpBasicAuth": { "id": "SUBSTITUIR_PELO_ID_DA_CREDENCIAL", "name": "SysLife Basic Auth - clínica pendente" } + } + }, + { + "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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife 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 SysLife", "type": "main", "index": 0 }]] }, + "Config SysLife": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] }, + "Montar query": { "main": [[{ "node": "GET /Procedimento", "type": "main", "index": 0 }]] }, + "GET /Procedimento": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Substitua a credencial. Guarde o `id` retornado como ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. Esperado: sem erros. + +--- + +### Task 6: Sub-workflow "MCP Server - SysLife - consultar_horarios" + +**Interfaces:** +- Consumes: credencial do pré-requisito. +- Produces: workflow `MCP Server - SysLife - consultar_horarios`, recebe `codigoEspecialidade`, `codigoOrigem`, `codigoMedico`, retorna o corpo cru de `GET /Horario` (inclui `nuConsulta`, usado depois em `agendar`) ou erro classificado. + +- [ ] **Step 1: Criar o workflow** + +```json +{ + "name": "MCP Server - SysLife - 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": "codigoEspecialidade", "type": "string" }, + { "name": "codigoOrigem", "type": "string" }, + { "name": "codigoMedico", "type": "string" } + ] + } + } + }, + { + "id": "config", + "name": "Config SysLife", + "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-IP-REAL-DA-CLINICA:9000" } + ] + } + } + }, + { + "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 queryBody: {\n CodigoEspecialidade: input.codigoEspecialidade,\n CodigoOrigem: input.codigoOrigem,\n CodigoMedico: input.codigoMedico,\n },\n },\n};" + } + }, + { + "id": "get-horario", + "name": "GET /Horario", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [780, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/Horario", + "authentication": "genericCredentialType", + "genericAuthType": "httpBasicAuth", + "sendBody": true, + "contentType": "json", + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.queryBody) }}", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + }, + "credentials": { + "httpBasicAuth": { "id": "SUBSTITUIR_PELO_ID_DA_CREDENCIAL", "name": "SysLife Basic Auth - clínica pendente" } + } + }, + { + "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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife 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 SysLife", "type": "main", "index": 0 }]] }, + "Config SysLife": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] }, + "Montar query": { "main": [[{ "node": "GET /Horario", "type": "main", "index": 0 }]] }, + "GET /Horario": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Substitua a credencial. Guarde o `id` retornado como ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. Esperado: sem erros. + +--- + +### Task 7: Sub-workflow "MCP Server - SysLife - agendar" + +**Interfaces:** +- Consumes: credencial do pré-requisito. +- Produces: workflow `MCP Server - SysLife - agendar`, recebe todos os códigos já resolvidos pelas outras 6 tools, retorna `{ success: true, data: {...} }` ou `{ success: false, error: {...} }`. + +- [ ] **Step 1: Criar o workflow** + +```json +{ + "name": "MCP Server - SysLife - 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": "dataAtendimento", "type": "string" }, + { "name": "horaAtendimento", "type": "string" }, + { "name": "numeroConsulta", "type": "string" }, + { "name": "codigoOrigem", "type": "string" }, + { "name": "codigoPaciente", "type": "string" }, + { "name": "nomePaciente", "type": "string" }, + { "name": "codigoConvenio", "type": "string" }, + { "name": "codigoPlano", "type": "string" }, + { "name": "codigoTabelaProcedimento", "type": "string" }, + { "name": "codigoProcedimento", "type": "string" }, + { "name": "codigoMedico", "type": "string" }, + { "name": "codigoEspecialidade", "type": "string" }, + { "name": "numeroCelular", "type": "string" }, + { "name": "numeroCelularDDD", "type": "string" } + ] + } + } + }, + { + "id": "config", + "name": "Config SysLife", + "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-IP-REAL-DA-CLINICA:9000" } + ] + } + } + }, + { + "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 DataAtendimento: input.dataAtendimento,\n HoraAtendimento: input.horaAtendimento,\n NumeroConsulta: input.numeroConsulta,\n CodigoOrigem: input.codigoOrigem,\n CodigoPaciente: input.codigoPaciente,\n NomePaciente: input.nomePaciente,\n CodigoConvenio: input.codigoConvenio,\n CodigoPlano: input.codigoPlano,\n CodigoTabelaProcedimento: input.codigoTabelaProcedimento,\n CodigoProcedimento: input.codigoProcedimento,\n CodigoMedico: input.codigoMedico,\n CodigoEspecialidade: input.codigoEspecialidade,\n NumeroCelular: input.numeroCelular,\n NumeroCelularDDD: input.numeroCelularDDD,\n};\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n requestBody,\n },\n};" + } + }, + { + "id": "put-agenda", + "name": "PUT /Agenda", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [780, 0], + "parameters": { + "method": "PUT", + "url": "={{ $json.baseUrl }}/Agenda", + "authentication": "genericCredentialType", + "genericAuthType": "httpBasicAuth", + "sendBody": true, + "contentType": "json", + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.requestBody) }}", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + }, + "credentials": { + "httpBasicAuth": { "id": "SUBSTITUIR_PELO_ID_DA_CREDENCIAL", "name": "SysLife Basic Auth - clínica pendente" } + } + }, + { + "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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife 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\n// Qualquer 2xx é sucesso operacional. Tenta mapear o envelope documentado\n// ({ Status, CO_AGENDA }); nunca inventa campos que o corpo não confirma.\nlet parsed = null;\ntry {\n parsed = JSON.parse(bodyText);\n} catch (e) {\n parsed = null;\n}\n\nconst coAgenda = parsed && (parsed.CO_AGENDA !== undefined && parsed.CO_AGENDA !== null) ? String(parsed.CO_AGENDA) : null;\n\nif (coAgenda) {\n return {\n json: {\n success: true,\n data: {\n id: coAgenda,\n status: typeof parsed.Status === \"string\" ? parsed.Status : undefined,\n responseMappingPending: false,\n },\n },\n };\n}\n\nconst input = $('Montar corpo').item.json;\nreturn {\n json: {\n success: true,\n data: {\n dataAtendimento: input.requestBody.DataAtendimento,\n horaAtendimento: input.requestBody.HoraAtendimento,\n responseMappingPending: true,\n },\n },\n};" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config SysLife", "type": "main", "index": 0 }]] }, + "Config SysLife": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] }, + "Montar corpo": { "main": [[{ "node": "PUT /Agenda", "type": "main", "index": 0 }]] }, + "PUT /Agenda": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Substitua a credencial. Guarde o `id` retornado como ``. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com `{"id": ""}`. Esperado: sem erros. + +--- + +### Task 8: Workflow "MCP Server - SysLife" (o servidor) + +**Interfaces:** +- Consumes: `` (Task 1), `` (Task 2), `` (Task 3), `` (Task 4), `` (Task 5), `` (Task 6), `` (Task 7). +- Produces: endpoint MCP em `https://devn8n.gtigrupo.com.br/mcp/mcp-server-syslife`. + +- [ ] **Step 1: Criar o workflow com o trigger e os sete nodes de tool** + +```json +{ + "name": "MCP Server - SysLife", + "nodes": [ + { + "id": "mcp-trigger", + "name": "MCP Server Trigger", + "type": "@n8n/n8n-nodes-langchain.mcpTrigger", + "typeVersion": 2, + "position": [0, 0], + "parameters": { "path": "mcp-server-syslife", "authentication": "none" } + }, + { + "id": "tool-especialidade", + "name": "especialidade", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [260, -240], + "parameters": { + "description": "Lista as especialidades médicas disponíveis na clínica, com os códigos de especialidade e origem necessários para as demais tools (medico, horario, agendar). Sem parâmetros de entrada.", + "source": "database", + "workflowId": { "value": "", "mode": "id" } + } + }, + { + "id": "tool-medico", + "name": "medico", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [260, -160], + "parameters": { + "description": "Lista os médicos de uma especialidade e origem específicas (códigos obtidos via a tool especialidade). Retorna o código do médico necessário para consultar_horarios, procedimento e agendar.", + "source": "database", + "workflowId": { "value": "", "mode": "id" } + } + }, + { + "id": "tool-convenio", + "name": "convenio", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [260, -80], + "parameters": { + "description": "Lista os convênios e planos aceitos pela clínica, com nomes para confirmar com o paciente e os códigos necessários para procedimento e agendar. Sem parâmetros de entrada.", + "source": "database", + "workflowId": { "value": "", "mode": "id" } + } + }, + { + "id": "tool-paciente", + "name": "paciente", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [260, 0], + "parameters": { + "description": "Identifica o paciente por CPF, matrícula ou número de celular. Retorna o código do paciente, nome, convênio, plano e telefone — necessários para procedimento e agendar.", + "source": "database", + "workflowId": { "value": "", "mode": "id" } + } + }, + { + "id": "tool-procedimento", + "name": "procedimento", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [260, 80], + "parameters": { + "description": "Lista os procedimentos disponíveis para um convênio, plano e médico específicos, com o valor de cada um. Retorna os códigos de procedimento e tabela de procedimento necessários para agendar.", + "source": "database", + "workflowId": { "value": "", "mode": "id" } + } + }, + { + "id": "tool-consultar-horarios", + "name": "consultar_horarios", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [260, 160], + "parameters": { + "description": "Consulta horários disponíveis para uma especialidade, origem e médico específicos. Retorna data, hora e o número da consulta (numeroConsulta) necessário para agendar.", + "source": "database", + "workflowId": { "value": "", "mode": "id" } + } + }, + { + "id": "tool-agendar", + "name": "agendar", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [260, 240], + "parameters": { + "description": "Cria o agendamento real na clínica. Use somente depois de já ter resolvido, com as outras tools, todos os códigos necessários (paciente, médico, especialidade, origem, convênio, plano, procedimento, tabela de procedimento, e o numeroConsulta/data/hora vindos de consultar_horarios).", + "source": "database", + "workflowId": { "value": "", "mode": "id" } + } + } + ], + "connections": { + "especialidade": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "medico": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "convenio": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "paciente": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "procedimento": { "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 7 `` pelos IDs reais das Tasks 1-7. + +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 cada um dos 6 nodes de tool que recebem parâmetro (`medico`, `paciente`, `procedimento`, `consultar_horarios`, `agendar` — `especialidade` e `convenio` não têm parâmetro) na UI do n8n. Em "Workflow Inputs", mapear cada campo para uma expressão `$fromAI(...)` com descrição clara pro modelo. Exemplo para `agendar`: + +``` +dataAtendimento: ={{ $fromAI('dataAtendimento', 'Data do atendimento no formato que o consultar_horarios retornou, não inventar formato', 'string') }} +horaAtendimento: ={{ $fromAI('horaAtendimento', 'Hora do atendimento no formato que o consultar_horarios retornou', 'string') }} +numeroConsulta: ={{ $fromAI('numeroConsulta', 'Número da consulta (nuConsulta) retornado por consultar_horarios para o horário escolhido', 'string') }} +codigoOrigem: ={{ $fromAI('codigoOrigem', 'Código de origem retornado por especialidade', 'string') }} +codigoPaciente: ={{ $fromAI('codigoPaciente', 'Código do paciente retornado por paciente', 'string') }} +nomePaciente: ={{ $fromAI('nomePaciente', 'Nome completo do paciente, confirmado na conversa', 'string') }} +codigoConvenio: ={{ $fromAI('codigoConvenio', 'Código do convênio retornado por paciente ou convenio', 'string') }} +codigoPlano: ={{ $fromAI('codigoPlano', 'Código do plano retornado por paciente ou convenio', 'string') }} +codigoTabelaProcedimento: ={{ $fromAI('codigoTabelaProcedimento', 'Código da tabela de procedimento retornado por procedimento', 'string') }} +codigoProcedimento: ={{ $fromAI('codigoProcedimento', 'Código do procedimento retornado por procedimento', 'string') }} +codigoMedico: ={{ $fromAI('codigoMedico', 'Código do médico retornado por medico', 'string') }} +codigoEspecialidade: ={{ $fromAI('codigoEspecialidade', 'Código da especialidade retornado por especialidade', 'string') }} +numeroCelular: ={{ $fromAI('numeroCelular', 'Número de celular do paciente, sem o DDD', 'string') }} +numeroCelularDDD: ={{ $fromAI('numeroCelularDDD', 'DDD do celular do paciente', 'string') }} +``` + +Mesmo padrão pros demais 4 nodes com parâmetro, usando os nomes de campo já definidos nas Tasks 2, 4, 5 e 6. + +- [ ] **Step 3: Validar (sem ativar)** + +`n8n_validate_workflow` com `{"id": ""}`. Esperado: sem erros. **Não ativar** nenhum dos 8 workflows nesta etapa — mesma decisão do Klingo (sem workflow de agente cliente ainda). Ativar todos juntos quando um workflow de agente cliente for construído. + +--- + +### Task 9: Revisão final e commit + +**Interfaces:** +- Consumes: workflows das Tasks 1-8. + +- [ ] **Step 1: Validação estrutural final dos 8 workflows** + +Rodar `n8n_validate_workflow` nos oito IDs com `{"options": {"profile": "ai-friendly"}}`. Esperado: sem erros nos oito. + +- [ ] **Step 2: Atualizar a spec com os IDs reais** + +Editar `docs/superpowers/specs/2026-08-11-mcp-server-nativo-n8n-syslife-design.md`, adicionando ao final uma seção curta "Status desta etapa" com os 8 IDs de workflow criados, o path do endpoint MCP (`mcp-server-syslife`), e uma nota reafirmando que nada está ativo/testado com dado real ainda — pendente de uma clínica SysLife 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-syslife-design.md +git commit -m "docs: registra IDs dos workflows do MCP Server nativo (SysLife) + +Co-Authored-By: Claude Sonnet 5 " +``` + +--- + +## Observação sobre execução + +Assim como nos planos do Z-PRO e do Klingo, 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 SysLife real disponível, a validação em todas as tasks é estrutural, não execução real — isso é intencional (ver Global Constraints), não um passo pulado por engano. As Tasks 1, 3 (especialidade, convenio) e 2, 4, 5, 6 (medico, paciente, procedimento, consultar_horarios) têm esqueleto quase idêntico entre si — isso é proposital (mesmo padrão do manual), não duplicação acidental a refatorar.