@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 ADDED
@@ -0,0 +1,114 @@
1
+ # @codespar/mcp-zenvia
2
+
3
+ > MCP server for **Zenvia** — multichannel messaging (SMS, WhatsApp, RCS)
4
+
5
+ [![npm](https://img.shields.io/npm/v/@codespar/mcp-zenvia)](https://www.npmjs.com/package/@codespar/mcp-zenvia)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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 (!API_TOKEN) {
175
- console.error("ZENVIA_API_TOKEN environment variable is required");
176
- process.exit(1);
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.0",
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 (!API_TOKEN) {
187
- console.error("ZENVIA_API_TOKEN environment variable is required");
188
- process.exit(1);
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);
package/tsconfig.json CHANGED
@@ -6,6 +6,7 @@
6
6
  "outDir": "./dist",
7
7
  "rootDir": "./src",
8
8
  "strict": true,
9
+ "skipLibCheck": true,
9
10
  "esModuleInterop": true,
10
11
  "declaration": true
11
12
  },