Files
KLINGO-CLINICAL-SYSLIFE-GCHAT/docs/superpowers/plans/2026-08-11-mcp-server-nativo-n8n-syslife.md
T
GustavoandClaude Sonnet 5 1968a6ae28 Plano: MCP Server nativo no n8n para o SysLife
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 <noreply@anthropic.com>
2026-08-11 00:07:55 -03:00

44 KiB

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

{
  "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 <wf-especialidade>.

  • Step 2: Validar

n8n_validate_workflow com {"id": "<wf-especialidade>"}. 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

{
  "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 <wf-medico>.

  • Step 2: Validar

n8n_validate_workflow com {"id": "<wf-medico>"}. 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:

{
  "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 <wf-convenio>.

  • Step 2: Validar

n8n_validate_workflow com {"id": "<wf-convenio>"}. 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

{
  "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 <wf-paciente>.

  • Step 2: Validar

n8n_validate_workflow com {"id": "<wf-paciente>"}. 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

{
  "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 <wf-procedimento>.

  • Step 2: Validar

n8n_validate_workflow com {"id": "<wf-procedimento>"}. 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

{
  "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 <wf-consultar-horarios>.

  • Step 2: Validar

n8n_validate_workflow com {"id": "<wf-consultar-horarios>"}. 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

{
  "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 <wf-agendar>.

  • Step 2: Validar

n8n_validate_workflow com {"id": "<wf-agendar>"}. Esperado: sem erros.


Task 8: Workflow "MCP Server - SysLife" (o servidor)

Interfaces:

  • Consumes: <wf-especialidade> (Task 1), <wf-medico> (Task 2), <wf-convenio> (Task 3), <wf-paciente> (Task 4), <wf-procedimento> (Task 5), <wf-consultar-horarios> (Task 6), <wf-agendar> (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

{
  "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": "<wf-especialidade>", "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": "<wf-medico>", "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": "<wf-convenio>", "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": "<wf-paciente>", "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": "<wf-procedimento>", "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": "<wf-consultar-horarios>", "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": "<wf-agendar>", "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 <wf-...> pelos IDs reais das Tasks 1-7.

Guarde o id deste workflow como <wf-mcp-server-syslife>.

  • 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": "<wf-mcp-server-syslife>"}. 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
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 <noreply@anthropic.com>"

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.