Plano: MCP Server nativo no n8n para Mensageria (Z-PRO)
6 tasks: 4 sub-workflows de envio (enviar_texto, enviar_template, enviar_template_com_parametros, enviar_template_marketing), servidor MCP com as 4 tools, e task final dedicada a resolver a pendência real de autenticação (ApiID de mensageria não confirmado) com gate de confirmação explícita do usuário antes de qualquer disparo de teste real -- diferente de agendamento fictício, mensagem enviada não pode ser desfeita. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6755c47fcb
commit
6f3a509df5
@@ -0,0 +1,614 @@
|
||||
# MCP Server Nativo no n8n para Mensageria (Z-PRO) 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 um MCP Server nativo do n8n, standalone, com 4 tools de envio de mensagem WhatsApp via API do Z-PRO — desacoplado de qualquer provider de agendamento, reutilizável por qualquer workflow futuro.
|
||||
|
||||
**Architecture:** 5 workflows n8n — o servidor (`MCP Server Trigger` + 4 tools) e quatro sub-workflows (`enviar_texto`, `enviar_template`, `enviar_template_com_parametros`, `enviar_template_marketing`), mesmo padrão já usado nos MCP Servers de agendamento deste projeto.
|
||||
|
||||
**Tech Stack:** n8n (`devn8n.gtigrupo.com.br`), nodes oficiais `@n8n/n8n-nodes-langchain` (`mcpTrigger`, `toolWorkflow`), `n8n-nodes-base` (`executeWorkflowTrigger`, `httpRequest`, `code`, `set`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Nenhuma tool inventa campo fora do que foi documentado pelo usuário (ver spec) — parâmetros de template cobrem só os exemplos reais mostrados.
|
||||
- Classificação de erro genérica por status HTTP (mesma taxonomia dos outros MCP Servers) — a API de mensageria não teve nenhum código de erro específico documentado.
|
||||
- **Pendência real de autenticação**: a credencial `ZPRO API Token - Canal QR - MC AGENDAMENTO` (`8XHGuA4cg9z1jUq8`) é uma hipótese razoável pra esta integração (não confirmada). O `apiId` de mensageria também não é conhecido ainda — fica como placeholder até confirmação.
|
||||
- **Nenhum teste real de envio acontece sem confirmação explícita do usuário antes de cada disparo** — diferente de criar um agendamento fictício (que pode ser apagado), uma mensagem de WhatsApp enviada não pode ser desfeita. Testar só com um número de celular de teste controlado, nunca um número de paciente real.
|
||||
|
||||
Spec de referência: `docs/superpowers/specs/2026-08-11-mcp-server-mensageria-zpro-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Sub-workflow "MCP Server - Mensageria ZPRO - enviar_texto"
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: workflow `MCP Server - Mensageria ZPRO - enviar_texto`, recebe `number`, `body`, `externalKey` (obrigatórios), `isClosed` (opcional), retorna o corpo cru de `POST /v2/api/external/{apiId}/` ou erro classificado.
|
||||
|
||||
- [ ] **Step 1: Criar o workflow**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "MCP Server - Mensageria ZPRO - enviar_texto",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "trigger",
|
||||
"name": "Execute Workflow Trigger",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [0, 0],
|
||||
"parameters": {
|
||||
"inputSource": "workflowInputs",
|
||||
"workflowInputs": {
|
||||
"values": [
|
||||
{ "name": "number", "type": "string" },
|
||||
{ "name": "body", "type": "string" },
|
||||
{ "name": "externalKey", "type": "string" },
|
||||
{ "name": "isClosed", "type": "boolean" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"name": "Config Mensageria",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.5,
|
||||
"position": [260, 0],
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"includeOtherFields": true,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{ "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "https://apigchat.gtigrupo.com.br" },
|
||||
{ "id": "cfg-apiid", "name": "apiId", "type": "string", "value": "SUBSTITUIR_PELO_APIID_DE_MENSAGERIA" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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 body: input.body,\n number: input.number,\n externalKey: input.externalKey,\n};\nif (input.isClosed !== undefined && input.isClosed !== null && input.isClosed !== \"\") {\n requestBody.isClosed = input.isClosed;\n}\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n apiId: input.apiId,\n requestBody,\n },\n};"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "post-enviar-texto",
|
||||
"name": "POST enviar texto",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [780, 0],
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $json.baseUrl }}/v2/api/external/{{ $json.apiId }}/",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify($json.requestBody) }}",
|
||||
"options": {
|
||||
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
|
||||
"timeout": 15000
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"httpHeaderAuth": { "id": "8XHGuA4cg9z1jUq8", "name": "ZPRO API Token - Canal QR - MC AGENDAMENTO" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"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 de mensageria do Z-PRO ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API de mensageria do Z-PRO ao ${context} (status ${status}). Verifique o ApiID.` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API de mensageria do Z-PRO rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"ZPRO_MENSAGERIA_API_ERROR\", message: `A API de mensageria do Z-PRO retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"enviar mensagem de texto\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Execute Workflow Trigger": { "main": [[{ "node": "Config Mensageria", "type": "main", "index": 0 }]] },
|
||||
"Config Mensageria": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] },
|
||||
"Montar corpo": { "main": [[{ "node": "POST enviar texto", "type": "main", "index": 0 }]] },
|
||||
"POST enviar texto": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Guarde o `id` retornado como `<wf-enviar-texto>`.
|
||||
|
||||
- [ ] **Step 2: Validar**
|
||||
|
||||
`n8n_validate_workflow` com `{"id": "<wf-enviar-texto>"}`. Esperado: sem erros.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Sub-workflow "MCP Server - Mensageria ZPRO - enviar_template"
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: workflow `MCP Server - Mensageria ZPRO - enviar_template`, recebe `number`, `templateName`, `templateLanguageCode` (obrigatórios), `isClosed` (opcional), retorna o corpo cru de `POST /v2/api/external/{apiId}/template` ou erro classificado.
|
||||
|
||||
- [ ] **Step 1: Criar o workflow**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "MCP Server - Mensageria ZPRO - enviar_template",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "trigger",
|
||||
"name": "Execute Workflow Trigger",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [0, 0],
|
||||
"parameters": {
|
||||
"inputSource": "workflowInputs",
|
||||
"workflowInputs": {
|
||||
"values": [
|
||||
{ "name": "number", "type": "string" },
|
||||
{ "name": "templateName", "type": "string" },
|
||||
{ "name": "templateLanguageCode", "type": "string" },
|
||||
{ "name": "isClosed", "type": "boolean" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"name": "Config Mensageria",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.5,
|
||||
"position": [260, 0],
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"includeOtherFields": true,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{ "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "https://apigchat.gtigrupo.com.br" },
|
||||
{ "id": "cfg-apiid", "name": "apiId", "type": "string", "value": "SUBSTITUIR_PELO_APIID_DE_MENSAGERIA" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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 number: input.number,\n templateData: {\n messaging_product: \"whatsapp\",\n to: input.number,\n type: \"template\",\n template: {\n name: input.templateName,\n language: { code: input.templateLanguageCode },\n },\n },\n};\nif (input.isClosed !== undefined && input.isClosed !== null && input.isClosed !== \"\") {\n requestBody.isClosed = input.isClosed;\n}\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n apiId: input.apiId,\n requestBody,\n },\n};"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "post-enviar-template",
|
||||
"name": "POST enviar template",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [780, 0],
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $json.baseUrl }}/v2/api/external/{{ $json.apiId }}/template",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify($json.requestBody) }}",
|
||||
"options": {
|
||||
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
|
||||
"timeout": 15000
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"httpHeaderAuth": { "id": "8XHGuA4cg9z1jUq8", "name": "ZPRO API Token - Canal QR - MC AGENDAMENTO" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"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 de mensageria do Z-PRO ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API de mensageria do Z-PRO ao ${context} (status ${status}). Verifique o ApiID.` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API de mensageria do Z-PRO rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"ZPRO_MENSAGERIA_API_ERROR\", message: `A API de mensageria do Z-PRO retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"enviar template\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Execute Workflow Trigger": { "main": [[{ "node": "Config Mensageria", "type": "main", "index": 0 }]] },
|
||||
"Config Mensageria": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] },
|
||||
"Montar corpo": { "main": [[{ "node": "POST enviar template", "type": "main", "index": 0 }]] },
|
||||
"POST enviar template": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Guarde o `id` retornado como `<wf-enviar-template>`.
|
||||
|
||||
- [ ] **Step 2: Validar**
|
||||
|
||||
`n8n_validate_workflow` com `{"id": "<wf-enviar-template>"}`. Esperado: sem erros.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Sub-workflow "MCP Server - Mensageria ZPRO - enviar_template_com_parametros"
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: workflow `MCP Server - Mensageria ZPRO - enviar_template_com_parametros`, recebe `number`, `templateName`, `templateLanguageCode`, `parametrosBody` (array de strings, obrigatórios), `isClosed` (opcional), retorna o corpo cru de `POST /v2/api/external/{apiId}/templateBody` ou erro classificado.
|
||||
|
||||
- [ ] **Step 1: Criar o workflow**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "MCP Server - Mensageria ZPRO - enviar_template_com_parametros",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "trigger",
|
||||
"name": "Execute Workflow Trigger",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [0, 0],
|
||||
"parameters": {
|
||||
"inputSource": "workflowInputs",
|
||||
"workflowInputs": {
|
||||
"values": [
|
||||
{ "name": "number", "type": "string" },
|
||||
{ "name": "templateName", "type": "string" },
|
||||
{ "name": "templateLanguageCode", "type": "string" },
|
||||
{ "name": "parametrosBody", "type": "array" },
|
||||
{ "name": "isClosed", "type": "boolean" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"name": "Config Mensageria",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.5,
|
||||
"position": [260, 0],
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"includeOtherFields": true,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{ "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "https://apigchat.gtigrupo.com.br" },
|
||||
{ "id": "cfg-apiid", "name": "apiId", "type": "string", "value": "SUBSTITUIR_PELO_APIID_DE_MENSAGERIA" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "montar-corpo",
|
||||
"name": "Montar corpo",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [520, 0],
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "const input = $json;\nconst params = Array.isArray(input.parametrosBody) ? input.parametrosBody : [];\nconst requestBody = {\n number: input.number,\n templateData: {\n messaging_product: \"whatsapp\",\n to: input.number,\n type: \"template\",\n template: {\n name: input.templateName,\n language: { code: input.templateLanguageCode },\n components: [\n {\n type: \"body\",\n parameters: params.map((valor) => ({ type: \"text\", text: String(valor) })),\n },\n ],\n },\n },\n};\nif (input.isClosed !== undefined && input.isClosed !== null && input.isClosed !== \"\") {\n requestBody.isClosed = input.isClosed;\n}\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n apiId: input.apiId,\n requestBody,\n },\n};"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "post-enviar-template-body",
|
||||
"name": "POST enviar template com parâmetros",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [780, 0],
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $json.baseUrl }}/v2/api/external/{{ $json.apiId }}/templateBody",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify($json.requestBody) }}",
|
||||
"options": {
|
||||
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
|
||||
"timeout": 15000
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"httpHeaderAuth": { "id": "8XHGuA4cg9z1jUq8", "name": "ZPRO API Token - Canal QR - MC AGENDAMENTO" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"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 de mensageria do Z-PRO ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API de mensageria do Z-PRO ao ${context} (status ${status}). Verifique o ApiID.` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API de mensageria do Z-PRO rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"ZPRO_MENSAGERIA_API_ERROR\", message: `A API de mensageria do Z-PRO retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"enviar template com parâmetros\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Execute Workflow Trigger": { "main": [[{ "node": "Config Mensageria", "type": "main", "index": 0 }]] },
|
||||
"Config Mensageria": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] },
|
||||
"Montar corpo": { "main": [[{ "node": "POST enviar template com parâmetros", "type": "main", "index": 0 }]] },
|
||||
"POST enviar template com parâmetros": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Guarde o `id` retornado como `<wf-enviar-template-parametros>`.
|
||||
|
||||
- [ ] **Step 2: Validar**
|
||||
|
||||
`n8n_validate_workflow` com `{"id": "<wf-enviar-template-parametros>"}`. Esperado: sem erros.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Sub-workflow "MCP Server - Mensageria ZPRO - enviar_template_marketing"
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: workflow `MCP Server - Mensageria ZPRO - enviar_template_marketing`, recebe `number`, `templateName`, `templateLanguageCode`, `parametrosBody` (obrigatórios), `headerImageUrl` (opcional), `isClosed` (opcional), retorna o corpo cru de `POST /v2/api/external/{apiId}/templateMarketingBody` ou erro classificado.
|
||||
|
||||
- [ ] **Step 1: Criar o workflow**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "MCP Server - Mensageria ZPRO - enviar_template_marketing",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "trigger",
|
||||
"name": "Execute Workflow Trigger",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [0, 0],
|
||||
"parameters": {
|
||||
"inputSource": "workflowInputs",
|
||||
"workflowInputs": {
|
||||
"values": [
|
||||
{ "name": "number", "type": "string" },
|
||||
{ "name": "templateName", "type": "string" },
|
||||
{ "name": "templateLanguageCode", "type": "string" },
|
||||
{ "name": "parametrosBody", "type": "array" },
|
||||
{ "name": "headerImageUrl", "type": "string" },
|
||||
{ "name": "isClosed", "type": "boolean" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"name": "Config Mensageria",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.5,
|
||||
"position": [260, 0],
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"includeOtherFields": true,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{ "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "https://apigchat.gtigrupo.com.br" },
|
||||
{ "id": "cfg-apiid", "name": "apiId", "type": "string", "value": "SUBSTITUIR_PELO_APIID_DE_MENSAGERIA" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "montar-corpo",
|
||||
"name": "Montar corpo",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [520, 0],
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "const input = $json;\nconst params = Array.isArray(input.parametrosBody) ? input.parametrosBody : [];\nconst components = [];\nif (input.headerImageUrl) {\n components.push({ type: \"header\", parameters: [{ type: \"image\", image: { link: input.headerImageUrl } }] });\n}\ncomponents.push({ type: \"body\", parameters: params.map((valor) => ({ type: \"text\", text: String(valor) })) });\n\nconst requestBody = {\n number: input.number,\n templateData: {\n messaging_product: \"whatsapp\",\n to: input.number,\n type: \"template\",\n template: {\n name: input.templateName,\n language: { code: input.templateLanguageCode },\n components,\n },\n },\n};\nif (input.isClosed !== undefined && input.isClosed !== null && input.isClosed !== \"\") {\n requestBody.isClosed = input.isClosed;\n}\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n apiId: input.apiId,\n requestBody,\n },\n};"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "post-enviar-template-marketing",
|
||||
"name": "POST enviar template marketing",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [780, 0],
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $json.baseUrl }}/v2/api/external/{{ $json.apiId }}/templateMarketingBody",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify($json.requestBody) }}",
|
||||
"options": {
|
||||
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
|
||||
"timeout": 15000
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"httpHeaderAuth": { "id": "8XHGuA4cg9z1jUq8", "name": "ZPRO API Token - Canal QR - MC AGENDAMENTO" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"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 de mensageria do Z-PRO ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API de mensageria do Z-PRO ao ${context} (status ${status}). Verifique o ApiID.` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API de mensageria do Z-PRO rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"ZPRO_MENSAGERIA_API_ERROR\", message: `A API de mensageria do Z-PRO retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"enviar template de marketing\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Execute Workflow Trigger": { "main": [[{ "node": "Config Mensageria", "type": "main", "index": 0 }]] },
|
||||
"Config Mensageria": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] },
|
||||
"Montar corpo": { "main": [[{ "node": "POST enviar template marketing", "type": "main", "index": 0 }]] },
|
||||
"POST enviar template marketing": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Guarde o `id` retornado como `<wf-enviar-template-marketing>`.
|
||||
|
||||
- [ ] **Step 2: Validar**
|
||||
|
||||
`n8n_validate_workflow` com `{"id": "<wf-enviar-template-marketing>"}`. Esperado: sem erros.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Workflow "MCP Server - Mensageria ZPRO" (o servidor)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `<wf-enviar-texto>` (Task 1), `<wf-enviar-template>` (Task 2), `<wf-enviar-template-parametros>` (Task 3), `<wf-enviar-template-marketing>` (Task 4).
|
||||
- Produces: endpoint MCP em `https://devn8n.gtigrupo.com.br/mcp/mcp-server-mensageria-zpro`.
|
||||
|
||||
- [ ] **Step 1: Criar o workflow com o trigger e os quatro nodes de tool**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "MCP Server - Mensageria ZPRO",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "mcp-trigger",
|
||||
"name": "MCP Server Trigger",
|
||||
"type": "@n8n/n8n-nodes-langchain.mcpTrigger",
|
||||
"typeVersion": 2,
|
||||
"position": [0, 0],
|
||||
"parameters": { "path": "mcp-server-mensageria-zpro", "authentication": "none" }
|
||||
},
|
||||
{
|
||||
"id": "tool-enviar-texto",
|
||||
"name": "enviar_texto",
|
||||
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
|
||||
"typeVersion": 2.2,
|
||||
"position": [260, -160],
|
||||
"parameters": {
|
||||
"description": "Envia uma mensagem de texto simples via WhatsApp pro número informado. Funciona tanto em canal não oficial quanto dentro da janela de 24h de conversa ativa no WABA oficial.",
|
||||
"source": "database",
|
||||
"workflowId": { "value": "<wf-enviar-texto>", "mode": "id" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tool-enviar-template",
|
||||
"name": "enviar_template",
|
||||
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
|
||||
"typeVersion": 2.2,
|
||||
"position": [260, -80],
|
||||
"parameters": {
|
||||
"description": "Envia um template WABA aprovado, sem parâmetros dinâmicos (ex.: template genérico de saudação). Use fora da janela de 24h de conversa, quando o canal for WABA oficial.",
|
||||
"source": "database",
|
||||
"workflowId": { "value": "<wf-enviar-template>", "mode": "id" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tool-enviar-template-parametros",
|
||||
"name": "enviar_template_com_parametros",
|
||||
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
|
||||
"typeVersion": 2.2,
|
||||
"position": [260, 0],
|
||||
"parameters": {
|
||||
"description": "Envia um template WABA aprovado com parâmetros dinâmicos de texto no corpo (ex.: nome do paciente, data da consulta). Use quando o template exigir preencher variáveis.",
|
||||
"source": "database",
|
||||
"workflowId": { "value": "<wf-enviar-template-parametros>", "mode": "id" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tool-enviar-template-marketing",
|
||||
"name": "enviar_template_marketing",
|
||||
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
|
||||
"typeVersion": 2.2,
|
||||
"position": [260, 80],
|
||||
"parameters": {
|
||||
"description": "Envia um template WABA de marketing, com componente de header (ex.: imagem) além do corpo com parâmetros.",
|
||||
"source": "database",
|
||||
"workflowId": { "value": "<wf-enviar-template-marketing>", "mode": "id" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"enviar_texto": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
|
||||
"enviar_template": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
|
||||
"enviar_template_com_parametros": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
|
||||
"enviar_template_marketing": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Substitua os 4 `<wf-...>` pelos IDs reais das Tasks 1-4.
|
||||
|
||||
Guarde o `id` deste workflow como `<wf-mcp-server-mensageria-zpro>`.
|
||||
|
||||
- [ ] **Step 2: Mapear os parâmetros de cada tool na UI (passo manual — resourceMapper não é confiável via API)**
|
||||
|
||||
Abrir cada um dos 4 nodes de tool na UI do n8n. Em "Workflow Inputs",
|
||||
mapear cada campo pra uma expressão `$fromAI(...)` com descrição clara.
|
||||
Exemplo pra `enviar_texto`:
|
||||
|
||||
```
|
||||
number: ={{ $fromAI('number', 'Número de WhatsApp do destinatário, com DDI e DDD, sem símbolos (ex.: 5511999999999)', 'string') }}
|
||||
body: ={{ $fromAI('body', 'Texto da mensagem a enviar', 'string') }}
|
||||
externalKey: ={{ $fromAI('externalKey', 'Chave única de controle para rastrear este envio (ex.: identificador do contexto que originou o envio)', 'string') }}
|
||||
isClosed: ={{ $fromAI('isClosed', 'Se true, fecha o ticket de atendimento após o envio (opcional)', 'boolean') }}
|
||||
```
|
||||
|
||||
Mesmo padrão pros demais 3 nodes, usando os nomes de campo já definidos
|
||||
nas Tasks 2-4 (`templateName`, `templateLanguageCode`, `parametrosBody`,
|
||||
`headerImageUrl`).
|
||||
|
||||
- [ ] **Step 3: Validar (sem ativar)**
|
||||
|
||||
`n8n_validate_workflow` com `{"id": "<wf-mcp-server-mensageria-zpro>"}`.
|
||||
Esperado: sem erros. **Não ativar** — falta confirmar a pendência de
|
||||
autenticação (Task 6) antes de qualquer uso real.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Resolver a pendência de autenticação (ApiID/token de mensageria)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: workflows das Tasks 1-5.
|
||||
|
||||
- [ ] **Step 1: Confirmar com o usuário o ApiID correto da integração de mensageria**
|
||||
|
||||
Perguntar ao usuário (ou verificar na tela de integrações do Z-PRO) qual
|
||||
é o `ApiID` associado à credencial `ZPRO API Token - Canal QR - MC
|
||||
AGENDAMENTO` — mesma disciplina já usada no agendamento (nunca assumir
|
||||
que ApiID e token são da mesma integração sem confirmar). Se for uma
|
||||
integração diferente, criar uma credencial nova e obter o ApiID correto,
|
||||
em vez de reaproveitar por suposição.
|
||||
|
||||
- [ ] **Step 2: Preencher o `apiId` real nos 4 sub-workflows**
|
||||
|
||||
`n8n_update_partial_workflow` em cada um dos 4 sub-workflows (Tasks 1-4),
|
||||
`patchNodeField` no node `"Config Mensageria"`, substituindo
|
||||
`"SUBSTITUIR_PELO_APIID_DE_MENSAGERIA"` pelo valor real confirmado no
|
||||
Step 1.
|
||||
|
||||
- [ ] **Step 3: Teste real, só com número de teste — PEDIR CONFIRMAÇÃO EXPLÍCITA ANTES DE DISPARAR**
|
||||
|
||||
Antes de rodar qualquer teste que envie uma mensagem de verdade, **pedir
|
||||
confirmação explícita ao usuário** (número de destino, qual tool testar)
|
||||
— mensagem de WhatsApp enviada não pode ser desfeita, diferente de um
|
||||
agendamento fictício. Só depois de confirmado, testar via `tools/call`
|
||||
raw MCP (mesmo processo usado nos outros MCP Servers — handshake
|
||||
`initialize` + `tools/call` com `curl`) contra `enviar_texto` primeiro
|
||||
(o mais simples), com um número de celular de teste controlado pela
|
||||
equipe. Confirmar visualmente no celular de teste que a mensagem chegou.
|
||||
|
||||
---
|
||||
|
||||
## Observação sobre execução
|
||||
|
||||
Mesmo padrão dos planos anteriores: tasks são chamadas de API ao n8n,
|
||||
não commits de código. **Diferente da maioria dos planos anteriores,
|
||||
este tem uma ação com efeito colateral real e irreversível** (enviar
|
||||
mensagem) — por isso a Task 6 exige confirmação explícita do usuário
|
||||
antes de qualquer disparo de teste, mesmo depois de toda a estrutura
|
||||
validada. Não pular essa confirmação mesmo que pareça óbvio que o teste
|
||||
vai funcionar.
|
||||
Reference in New Issue
Block a user