@codespar/mcp-zenvia 0.1.0 → 0.1.2
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/README.md +114 -0
- package/dist/index.js +42 -5
- package/package.json +3 -2
- package/server.json +30 -0
- package/src/__tests__/index.test.ts +52 -0
- package/src/index.ts +27 -5
- package/tsconfig.json +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @codespar/mcp-zenvia
|
|
2
|
+
|
|
3
|
+
> MCP server for **Zenvia** — multichannel messaging (SMS, WhatsApp, RCS)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@codespar/mcp-zenvia)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
### Claude Desktop
|
|
11
|
+
|
|
12
|
+
Add to `~/.config/claude/claude_desktop_config.json`:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"zenvia": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@codespar/mcp-zenvia"],
|
|
20
|
+
"env": {
|
|
21
|
+
"ZENVIA_API_TOKEN": "your-token"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Claude Code
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
claude mcp add zenvia -- npx @codespar/mcp-zenvia
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Cursor / VS Code
|
|
35
|
+
|
|
36
|
+
Add to `.cursor/mcp.json` or `.vscode/mcp.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"servers": {
|
|
41
|
+
"zenvia": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "@codespar/mcp-zenvia"],
|
|
44
|
+
"env": {
|
|
45
|
+
"ZENVIA_API_TOKEN": "your-token"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
| Tool | Description |
|
|
55
|
+
|------|-------------|
|
|
56
|
+
| `send_sms` | Send an SMS message |
|
|
57
|
+
| `send_whatsapp` | Send a WhatsApp message |
|
|
58
|
+
| `send_rcs` | Send an RCS (Rich Communication Services) message |
|
|
59
|
+
| `get_message_status` | Get message delivery status by ID |
|
|
60
|
+
| `list_channels` | List available messaging channels |
|
|
61
|
+
| `create_subscription` | Create a webhook subscription for message events |
|
|
62
|
+
| `list_contacts` | List contacts from the contact base |
|
|
63
|
+
| `send_template` | Send a WhatsApp template message (pre-approved) |
|
|
64
|
+
|
|
65
|
+
## Authentication
|
|
66
|
+
|
|
67
|
+
Zenvia uses an API token passed via the `X-API-TOKEN` header.
|
|
68
|
+
|
|
69
|
+
## Sandbox / Testing
|
|
70
|
+
|
|
71
|
+
Zenvia provides a sandbox via the dashboard for testing messages.
|
|
72
|
+
|
|
73
|
+
### Get your credentials
|
|
74
|
+
|
|
75
|
+
1. Go to [Zenvia](https://app.zenvia.com)
|
|
76
|
+
2. Create an account
|
|
77
|
+
3. Get your API token from the dashboard
|
|
78
|
+
4. Set the `ZENVIA_API_TOKEN` environment variable
|
|
79
|
+
|
|
80
|
+
## Environment Variables
|
|
81
|
+
|
|
82
|
+
| Variable | Required | Description |
|
|
83
|
+
|----------|----------|-------------|
|
|
84
|
+
| `ZENVIA_API_TOKEN` | Yes | API token from Zenvia dashboard |
|
|
85
|
+
|
|
86
|
+
## Roadmap
|
|
87
|
+
|
|
88
|
+
### v0.2 (planned)
|
|
89
|
+
- `create_contact_list` — Create a contact list for campaigns
|
|
90
|
+
- `send_batch` — Send batch messages to a contact list
|
|
91
|
+
- `get_report` — Get message delivery reports
|
|
92
|
+
- `create_flow` — Create a conversational flow
|
|
93
|
+
- `list_templates` — List approved message templates
|
|
94
|
+
|
|
95
|
+
### v0.3 (planned)
|
|
96
|
+
- `chatbot_integration` — Integrate with Zenvia chatbot builder
|
|
97
|
+
- `analytics_dashboard` — Get channel analytics and metrics
|
|
98
|
+
|
|
99
|
+
Want to contribute? [Open a PR](https://github.com/codespar/mcp-dev-brasil) or [request a tool](https://github.com/codespar/mcp-dev-brasil/issues).
|
|
100
|
+
|
|
101
|
+
## Links
|
|
102
|
+
|
|
103
|
+
- [Zenvia Website](https://zenvia.com)
|
|
104
|
+
- [Zenvia API Documentation](https://zenvia.github.io/zenvia-openapi-spec)
|
|
105
|
+
- [MCP Dev Brasil](https://github.com/codespar/mcp-dev-brasil)
|
|
106
|
+
- [Landing Page](https://codespar.dev/mcp)
|
|
107
|
+
|
|
108
|
+
## Enterprise
|
|
109
|
+
|
|
110
|
+
Need governance, budget limits, and audit trails for agent payments? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
19
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
21
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
20
22
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
21
23
|
const API_TOKEN = process.env.ZENVIA_API_TOKEN || "";
|
|
22
24
|
const BASE_URL = "https://api.zenvia.com/v2";
|
|
@@ -171,11 +173,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
171
173
|
}
|
|
172
174
|
});
|
|
173
175
|
async function main() {
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
177
|
+
const { default: express } = await import("express");
|
|
178
|
+
const { randomUUID } = await import("node:crypto");
|
|
179
|
+
const app = express();
|
|
180
|
+
app.use(express.json());
|
|
181
|
+
const transports = new Map();
|
|
182
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
183
|
+
app.post("/mcp", async (req, res) => {
|
|
184
|
+
const sid = req.headers["mcp-session-id"];
|
|
185
|
+
if (sid && transports.has(sid)) {
|
|
186
|
+
await transports.get(sid).handleRequest(req, res, req.body);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
190
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
191
|
+
t.onclose = () => { if (t.sessionId)
|
|
192
|
+
transports.delete(t.sessionId); };
|
|
193
|
+
const s = new Server({ name: "mcp-zenvia", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
194
|
+
server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
|
|
195
|
+
server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
|
|
196
|
+
await s.connect(t);
|
|
197
|
+
await t.handleRequest(req, res, req.body);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
201
|
+
});
|
|
202
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
203
|
+
await transports.get(sid).handleRequest(req, res);
|
|
204
|
+
else
|
|
205
|
+
res.status(400).send("Invalid session"); });
|
|
206
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
207
|
+
await transports.get(sid).handleRequest(req, res);
|
|
208
|
+
else
|
|
209
|
+
res.status(400).send("Invalid session"); });
|
|
210
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
211
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const transport = new StdioServerTransport();
|
|
215
|
+
await server.connect(transport);
|
|
177
216
|
}
|
|
178
|
-
const transport = new StdioServerTransport();
|
|
179
|
-
await server.connect(transport);
|
|
180
217
|
}
|
|
181
218
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codespar/mcp-zenvia",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "MCP server for Zenvia — SMS, WhatsApp, RCS messaging and templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,5 +26,6 @@
|
|
|
26
26
|
"whatsapp",
|
|
27
27
|
"rcs",
|
|
28
28
|
"brazil"
|
|
29
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"mcpName": "io.github.codespar/mcp-zenvia"
|
|
30
31
|
}
|
package/server.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.codespar/mcp-zenvia",
|
|
4
|
+
"description": "MCP server for Zenvia — SMS, WhatsApp, RCS messaging and templates",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/codespar/mcp-dev-brasil",
|
|
7
|
+
"source": "github",
|
|
8
|
+
"subfolder": "packages/communication/zenvia"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.1.2",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@codespar/mcp-zenvia",
|
|
15
|
+
"version": "0.1.2",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
},
|
|
19
|
+
"environmentVariables": [
|
|
20
|
+
{
|
|
21
|
+
"name": "ZENVIA_API_TOKEN",
|
|
22
|
+
"description": "API key for zenvia",
|
|
23
|
+
"isRequired": true,
|
|
24
|
+
"format": "string",
|
|
25
|
+
"isSecret": true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
let listToolsHandler: Function;
|
|
4
|
+
let callToolHandler: Function;
|
|
5
|
+
|
|
6
|
+
vi.mock("@modelcontextprotocol/sdk/server/index.js", () => {
|
|
7
|
+
class FakeServer {
|
|
8
|
+
constructor() {}
|
|
9
|
+
setRequestHandler(schema: any, handler: Function) {
|
|
10
|
+
if (JSON.stringify(schema).includes("tools/list")) listToolsHandler = handler;
|
|
11
|
+
if (JSON.stringify(schema).includes("tools/call")) callToolHandler = handler;
|
|
12
|
+
}
|
|
13
|
+
connect() { return Promise.resolve(); }
|
|
14
|
+
}
|
|
15
|
+
return { Server: FakeServer };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({ StdioServerTransport: class {} }));
|
|
19
|
+
|
|
20
|
+
process.env.ZENVIA_API_TOKEN = "test-token";
|
|
21
|
+
|
|
22
|
+
const mockFetch = vi.fn();
|
|
23
|
+
global.fetch = mockFetch as any;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
vi.resetModules();
|
|
27
|
+
listToolsHandler = undefined as any;
|
|
28
|
+
callToolHandler = undefined as any;
|
|
29
|
+
mockFetch.mockReset();
|
|
30
|
+
global.fetch = mockFetch as any;
|
|
31
|
+
await import("../index.js");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("mcp-zenvia", () => {
|
|
35
|
+
it("should register 8 tools", async () => {
|
|
36
|
+
const result = await listToolsHandler();
|
|
37
|
+
expect(result.tools).toHaveLength(8);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should call correct API endpoint for send_sms", async () => {
|
|
41
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ id: "msg1" }) });
|
|
42
|
+
|
|
43
|
+
await callToolHandler({
|
|
44
|
+
params: { name: "send_sms", arguments: { from: "sender", to: "5511999999999", text: "Hello" } },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const [url, opts] = mockFetch.mock.calls[0];
|
|
48
|
+
expect(url).toContain("api.zenvia.com/v2/channels/sms/messages");
|
|
49
|
+
expect(opts.method).toBe("POST");
|
|
50
|
+
expect(opts.headers["X-API-TOKEN"]).toBe("test-token");
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
22
24
|
import {
|
|
23
25
|
CallToolRequestSchema,
|
|
24
26
|
ListToolsRequestSchema,
|
|
@@ -183,12 +185,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
183
185
|
});
|
|
184
186
|
|
|
185
187
|
async function main() {
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
189
|
+
const { default: express } = await import("express");
|
|
190
|
+
const { randomUUID } = await import("node:crypto");
|
|
191
|
+
const app = express();
|
|
192
|
+
app.use(express.json());
|
|
193
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
194
|
+
app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
|
|
195
|
+
app.post("/mcp", async (req: any, res: any) => {
|
|
196
|
+
const sid = req.headers["mcp-session-id"] as string | undefined;
|
|
197
|
+
if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
|
|
198
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
199
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
200
|
+
t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
|
|
201
|
+
const s = new Server({ name: "mcp-zenvia", version: "0.1.0" }, { capabilities: { tools: {} } }); (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); await s.connect(t);
|
|
202
|
+
await t.handleRequest(req, res, req.body); return;
|
|
203
|
+
}
|
|
204
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
205
|
+
});
|
|
206
|
+
app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
207
|
+
app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
208
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
209
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
210
|
+
} else {
|
|
211
|
+
const transport = new StdioServerTransport();
|
|
212
|
+
await server.connect(transport);
|
|
189
213
|
}
|
|
190
|
-
const transport = new StdioServerTransport();
|
|
191
|
-
await server.connect(transport);
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
main().catch(console.error);
|