vistoazul-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/index.js +211 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Visto Azul
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# vistoazul-mcp
|
|
2
|
+
|
|
3
|
+
> Servidor **MCP** (Model Context Protocol) da **[Visto Azul](https://vistoazul.com.br)**. Dê ao **Claude**, **Claude Code**, **Cursor** e a qualquer cliente MCP a capacidade de operar o **WhatsApp**: enviar texto, mídia e **cobrança PIX**, disparar campanhas, gerenciar contatos e configurar webhooks — tudo pela API REST do Visto Azul, sem taxa por mensagem da Meta e com anti-ban de fábrica.
|
|
4
|
+
|
|
5
|
+
## O que é
|
|
6
|
+
|
|
7
|
+
O MCP é o padrão que conecta agentes de IA a ferramentas externas. Com este servidor, você pede em linguagem natural — *"manda um PIX de R$97 pro cliente"*, *"dispara a campanha pra essa lista"* — e o agente executa pela sua conta do Visto Azul.
|
|
8
|
+
|
|
9
|
+
## Ferramentas
|
|
10
|
+
|
|
11
|
+
| Ferramenta | O que faz |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `list_instances` | Lista os números conectados e o status |
|
|
14
|
+
| `send_text` | Envia mensagem de texto |
|
|
15
|
+
| `send_media` | Envia imagem, vídeo ou documento |
|
|
16
|
+
| `send_pix` | Envia cobrança PIX na conversa |
|
|
17
|
+
| `send_poll` | Envia enquete |
|
|
18
|
+
| `check_numbers` | Verifica quem tem WhatsApp (anti-ban) |
|
|
19
|
+
| `create_campaign` | Cria campanha (disparo em massa) |
|
|
20
|
+
| `campaign_status` | Status de uma campanha |
|
|
21
|
+
| `list_contacts` | Lista contatos (filtra por tag) |
|
|
22
|
+
| `upsert_contact` | Cria ou atualiza um contato |
|
|
23
|
+
| `set_webhook` | Configura webhook para receber mensagens |
|
|
24
|
+
|
|
25
|
+
## Instalação
|
|
26
|
+
|
|
27
|
+
Você precisa de uma **API key** do Visto Azul: crie a conta em [dashboard.vistoazul.com.br](https://dashboard.vistoazul.com.br), conecte seu número e copie a key em *Instâncias*.
|
|
28
|
+
|
|
29
|
+
### Claude Code
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
claude mcp add vistoazul -e VISTOAZUL_API_KEY=SUA_API_KEY -- npx -y vistoazul-mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Claude Desktop / Cursor (config JSON)
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"vistoazul": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["-y", "vistoazul-mcp"],
|
|
43
|
+
"env": { "VISTOAZUL_API_KEY": "SUA_API_KEY" }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuração
|
|
50
|
+
|
|
51
|
+
| Variável | Obrigatória | Padrão |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `VISTOAZUL_API_KEY` | sim | — |
|
|
54
|
+
| `VISTOAZUL_BASE_URL` | não | `https://dashboard.vistoazul.com.br/api/v1` |
|
|
55
|
+
|
|
56
|
+
## Links
|
|
57
|
+
|
|
58
|
+
- 🌐 Site: **https://vistoazul.com.br**
|
|
59
|
+
- 📚 Documentação: **https://vistoazul.com.br/docs**
|
|
60
|
+
- 🧩 Node do n8n: **https://www.npmjs.com/package/n8n-nodes-vistoazul**
|
|
61
|
+
|
|
62
|
+
## Licença
|
|
63
|
+
|
|
64
|
+
[MIT](./LICENSE)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Servidor MCP da Visto Azul.
|
|
4
|
+
* Expoe a API REST de WhatsApp do Visto Azul como ferramentas para agentes de IA
|
|
5
|
+
* (Claude, Claude Code, Cursor e qualquer cliente MCP). Auth por API key (Bearer).
|
|
6
|
+
* Config: variavel de ambiente VISTOAZUL_API_KEY (obrigatoria) e VISTOAZUL_BASE_URL (opcional).
|
|
7
|
+
*/
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
const API_KEY = process.env.VISTOAZUL_API_KEY ?? "";
|
|
12
|
+
const BASE_URL = (process.env.VISTOAZUL_BASE_URL ?? "https://dashboard.vistoazul.com.br/api/v1").replace(/\/+$/, "");
|
|
13
|
+
if (!API_KEY) {
|
|
14
|
+
console.error("[vistoazul-mcp] Defina VISTOAZUL_API_KEY com a sua API key do Visto Azul (painel > Instancias).");
|
|
15
|
+
}
|
|
16
|
+
async function call(method, path, body, instance) {
|
|
17
|
+
const headers = {
|
|
18
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
};
|
|
21
|
+
if (instance)
|
|
22
|
+
headers["X-Instance"] = instance;
|
|
23
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
27
|
+
});
|
|
28
|
+
const text = await res.text();
|
|
29
|
+
const data = text ? (() => { try {
|
|
30
|
+
return JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return text;
|
|
34
|
+
} })() : null;
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const msg = (data && typeof data === "object" && "error" in data) ? data.error : `Erro ${res.status}`;
|
|
37
|
+
throw new Error(`${res.status}: ${msg}`);
|
|
38
|
+
}
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
const ok = (data) => ({
|
|
42
|
+
content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }],
|
|
43
|
+
});
|
|
44
|
+
const fail = (e) => ({
|
|
45
|
+
content: [{ type: "text", text: `Erro: ${e instanceof Error ? e.message : String(e)}` }],
|
|
46
|
+
isError: true,
|
|
47
|
+
});
|
|
48
|
+
const server = new McpServer({ name: "vistoazul", version: "0.1.0" });
|
|
49
|
+
server.tool("list_instances", "Lista as instancias (numeros de WhatsApp) conectadas na conta, com status.", {}, async () => {
|
|
50
|
+
try {
|
|
51
|
+
return ok(await call("GET", "/instances"));
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
return fail(e);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
server.tool("send_text", "Envia uma mensagem de texto no WhatsApp.", {
|
|
58
|
+
instance: z.string().optional().describe("Nome da instancia (omita se so houver um numero)."),
|
|
59
|
+
number: z.string().describe("Numero de destino com DDI e DDD, so digitos. Ex: 5511999999999"),
|
|
60
|
+
text: z.string().describe("Texto da mensagem."),
|
|
61
|
+
}, async ({ instance, number, text }) => {
|
|
62
|
+
try {
|
|
63
|
+
const body = { number, text };
|
|
64
|
+
if (instance)
|
|
65
|
+
body.instance = instance;
|
|
66
|
+
return ok(await call("POST", "/messages/text", body));
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return fail(e);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
server.tool("send_media", "Envia midia (imagem, video ou documento) por uma URL publica, com legenda opcional.", {
|
|
73
|
+
instance: z.string().optional(),
|
|
74
|
+
number: z.string(),
|
|
75
|
+
type: z.enum(["image", "video", "document"]),
|
|
76
|
+
file: z.string().describe("URL publica do arquivo."),
|
|
77
|
+
text: z.string().optional().describe("Legenda (opcional)."),
|
|
78
|
+
}, async ({ instance, number, type, file, text }) => {
|
|
79
|
+
try {
|
|
80
|
+
const body = { number, type, file };
|
|
81
|
+
if (text)
|
|
82
|
+
body.text = text;
|
|
83
|
+
return ok(await call("POST", "/wa/send/media", body, instance));
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
return fail(e);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
server.tool("send_pix", "Envia uma cobranca PIX na conversa do WhatsApp.", {
|
|
90
|
+
instance: z.string().optional(),
|
|
91
|
+
number: z.string(),
|
|
92
|
+
amount: z.number().describe("Valor da cobranca. Ex: 97.00"),
|
|
93
|
+
pixKey: z.string().describe("Sua chave PIX."),
|
|
94
|
+
pixType: z.enum(["email", "cpf", "cnpj", "phone", "random"]),
|
|
95
|
+
title: z.string().optional(),
|
|
96
|
+
text: z.string().optional(),
|
|
97
|
+
itemName: z.string().optional(),
|
|
98
|
+
}, async ({ instance, number, amount, pixKey, pixType, title, text, itemName }) => {
|
|
99
|
+
try {
|
|
100
|
+
const body = { number, amount, pixKey, pixType };
|
|
101
|
+
if (title)
|
|
102
|
+
body.title = title;
|
|
103
|
+
if (text)
|
|
104
|
+
body.text = text;
|
|
105
|
+
if (itemName)
|
|
106
|
+
body.itemName = itemName;
|
|
107
|
+
return ok(await call("POST", "/wa/send/request-payment", body, instance));
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return fail(e);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
server.tool("send_poll", "Envia uma enquete (poll) no WhatsApp.", {
|
|
114
|
+
instance: z.string().optional(),
|
|
115
|
+
number: z.string(),
|
|
116
|
+
text: z.string().describe("Pergunta da enquete."),
|
|
117
|
+
choices: z.array(z.string()).describe("Opcoes da enquete."),
|
|
118
|
+
selectableCount: z.number().optional().describe("Quantas opcoes o contato pode marcar (padrao 1)."),
|
|
119
|
+
}, async ({ instance, number, text, choices, selectableCount }) => {
|
|
120
|
+
try {
|
|
121
|
+
const body = { type: "poll", number, text, choices, selectableCount: selectableCount ?? 1 };
|
|
122
|
+
return ok(await call("POST", "/wa/send/menu", body, instance));
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
return fail(e);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
server.tool("check_numbers", "Verifica quais numeros tem WhatsApp (use antes de disparar, anti-ban).", {
|
|
129
|
+
instance: z.string().optional(),
|
|
130
|
+
numbers: z.array(z.string()).describe("Lista de numeros a verificar."),
|
|
131
|
+
}, async ({ instance, numbers }) => {
|
|
132
|
+
try {
|
|
133
|
+
return ok(await call("POST", "/wa/chat/check", { numbers }, instance));
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
return fail(e);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
server.tool("create_campaign", "Cria uma campanha (disparo em massa com anti-ban). template aceita spintax {a|b} e variaveis {{nome}}.", {
|
|
140
|
+
instances: z.array(z.string()).describe("Nomes das instancias que vao disparar."),
|
|
141
|
+
template: z.string(),
|
|
142
|
+
recipients: z.array(z.object({
|
|
143
|
+
number: z.string(),
|
|
144
|
+
vars: z.record(z.string()).optional(),
|
|
145
|
+
})),
|
|
146
|
+
minDelayMs: z.number().optional(),
|
|
147
|
+
maxDelayMs: z.number().optional(),
|
|
148
|
+
scheduledForMs: z.number().optional().describe("epoch ms; no futuro = agendada."),
|
|
149
|
+
}, async ({ instances, template, recipients, minDelayMs, maxDelayMs, scheduledForMs }) => {
|
|
150
|
+
try {
|
|
151
|
+
const body = { instances, template, recipients };
|
|
152
|
+
if (minDelayMs !== undefined)
|
|
153
|
+
body.minDelayMs = minDelayMs;
|
|
154
|
+
if (maxDelayMs !== undefined)
|
|
155
|
+
body.maxDelayMs = maxDelayMs;
|
|
156
|
+
if (scheduledForMs !== undefined)
|
|
157
|
+
body.scheduledForMs = scheduledForMs;
|
|
158
|
+
return ok(await call("POST", "/campaigns", body));
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
return fail(e);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
server.tool("campaign_status", "Consulta o status de uma campanha.", { id: z.string() }, async ({ id }) => {
|
|
165
|
+
try {
|
|
166
|
+
return ok(await call("GET", `/campaigns/${id}`));
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
return fail(e);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
server.tool("list_contacts", "Lista contatos da conta. Filtre por tag opcional.", { tag: z.string().optional() }, async ({ tag }) => {
|
|
173
|
+
try {
|
|
174
|
+
return ok(await call("GET", `/contacts${tag ? `?tag=${encodeURIComponent(tag)}` : ""}`));
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
return fail(e);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
server.tool("upsert_contact", "Cria ou atualiza um contato.", {
|
|
181
|
+
number: z.string(),
|
|
182
|
+
name: z.string().optional(),
|
|
183
|
+
tags: z.array(z.string()).optional(),
|
|
184
|
+
}, async ({ number, name, tags }) => {
|
|
185
|
+
try {
|
|
186
|
+
const body = { number };
|
|
187
|
+
if (name)
|
|
188
|
+
body.name = name;
|
|
189
|
+
if (tags)
|
|
190
|
+
body.tags = tags;
|
|
191
|
+
return ok(await call("POST", "/contacts", body));
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
return fail(e);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
server.tool("set_webhook", "Configura o webhook de uma instancia para receber mensagens no seu endpoint (para chatbots).", {
|
|
198
|
+
instance: z.string(),
|
|
199
|
+
url: z.string().describe("URL do seu endpoint."),
|
|
200
|
+
enabled: z.boolean().optional(),
|
|
201
|
+
}, async ({ instance, url, enabled }) => {
|
|
202
|
+
try {
|
|
203
|
+
return ok(await call("POST", `/instances/${instance}/webhook`, { url, enabled: enabled ?? true }));
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
return fail(e);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
const transport = new StdioServerTransport();
|
|
210
|
+
await server.connect(transport);
|
|
211
|
+
console.error("[vistoazul-mcp] servidor MCP iniciado (stdio).");
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vistoazul-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Servidor MCP da Visto Azul: use o WhatsApp (enviar texto, midia, PIX, campanhas, contatos, webhook) direto de agentes de IA como Claude, Claude Code e Cursor, pela API REST do Visto Azul.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"whatsapp",
|
|
9
|
+
"vistoazul",
|
|
10
|
+
"whatsapp-api",
|
|
11
|
+
"pix",
|
|
12
|
+
"claude",
|
|
13
|
+
"ai-agent"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://vistoazul.com.br",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Visto Azul",
|
|
19
|
+
"url": "https://vistoazul.com.br"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/useideia/vistoazul-mcp.git"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"vistoazul-mcp": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
34
|
+
"start": "node dist/index.js",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
39
|
+
"zod": "^3.23.8"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.11.0",
|
|
43
|
+
"typescript": "^5.5.4"
|
|
44
|
+
}
|
|
45
|
+
}
|