@codespar/mcp-zenvia 0.1.0 → 0.2.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/README.md +114 -0
- package/dist/index.js +227 -9
- package/package.json +7 -3
- package/server.json +30 -0
- package/src/__tests__/index.test.ts +52 -0
- package/src/index.ts +203 -9
- 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
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* MCP Server for Zenvia — multi-channel messaging (SMS, WhatsApp, RCS).
|
|
3
|
+
* MCP Server for Zenvia — multi-channel messaging (SMS, WhatsApp, RCS, Email, Voice, Facebook).
|
|
4
4
|
*
|
|
5
5
|
* Tools:
|
|
6
6
|
* - send_sms: Send an SMS message
|
|
7
7
|
* - send_whatsapp: Send a WhatsApp message
|
|
8
8
|
* - send_rcs: Send an RCS message
|
|
9
|
+
* - send_email: Send a transactional email
|
|
10
|
+
* - send_voice: Send a voice message (TTS or pre-recorded audio)
|
|
11
|
+
* - send_facebook_message: Send a Facebook Messenger message
|
|
9
12
|
* - get_message_status: Get message delivery status
|
|
10
13
|
* - list_channels: List available messaging channels
|
|
11
14
|
* - create_subscription: Create a webhook subscription for events
|
|
15
|
+
* - list_subscriptions: List all webhook subscriptions
|
|
16
|
+
* - delete_subscription: Delete a webhook subscription
|
|
12
17
|
* - list_contacts: List contacts
|
|
18
|
+
* - create_contact: Create a contact in the contact base
|
|
19
|
+
* - delete_contact: Delete a contact
|
|
13
20
|
* - send_template: Send a WhatsApp template message
|
|
21
|
+
* - list_templates: List approved WhatsApp templates
|
|
22
|
+
* - get_report_entries: Get message report entries by date range
|
|
23
|
+
* - add_opt_out: Add a phone number to the opt-out list
|
|
14
24
|
*
|
|
15
25
|
* Environment:
|
|
16
26
|
* ZENVIA_API_TOKEN — API token from https://app.zenvia.com/
|
|
17
27
|
*/
|
|
18
28
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
29
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
30
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
31
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
20
32
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
21
33
|
const API_TOKEN = process.env.ZENVIA_API_TOKEN || "";
|
|
22
34
|
const BASE_URL = "https://api.zenvia.com/v2";
|
|
@@ -33,9 +45,11 @@ async function zenviaRequest(method, path, body) {
|
|
|
33
45
|
const err = await res.text();
|
|
34
46
|
throw new Error(`Zenvia API ${res.status}: ${err}`);
|
|
35
47
|
}
|
|
36
|
-
|
|
48
|
+
// Some endpoints (DELETE) return empty
|
|
49
|
+
const text = await res.text();
|
|
50
|
+
return text ? JSON.parse(text) : { ok: true };
|
|
37
51
|
}
|
|
38
|
-
const server = new Server({ name: "mcp-zenvia", version: "0.
|
|
52
|
+
const server = new Server({ name: "mcp-zenvia", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
39
53
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
40
54
|
tools: [
|
|
41
55
|
{
|
|
@@ -77,6 +91,48 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
77
91
|
required: ["from", "to", "text"],
|
|
78
92
|
},
|
|
79
93
|
},
|
|
94
|
+
{
|
|
95
|
+
name: "send_email",
|
|
96
|
+
description: "Send a transactional email",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
from: { type: "string", description: "Sender email address (verified domain)" },
|
|
101
|
+
to: { type: "string", description: "Recipient email address" },
|
|
102
|
+
subject: { type: "string", description: "Email subject" },
|
|
103
|
+
html: { type: "string", description: "HTML body of the email" },
|
|
104
|
+
text: { type: "string", description: "Plain text body (fallback)" },
|
|
105
|
+
},
|
|
106
|
+
required: ["from", "to", "subject"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "send_voice",
|
|
111
|
+
description: "Send a voice message via TTS or pre-recorded audio URL",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
from: { type: "string", description: "Sender ID (Voice channel)" },
|
|
116
|
+
to: { type: "string", description: "Recipient phone number with country code" },
|
|
117
|
+
text: { type: "string", description: "Text to be spoken (TTS) — use either text or audioUrl" },
|
|
118
|
+
audioUrl: { type: "string", description: "URL of pre-recorded audio file — use either text or audioUrl" },
|
|
119
|
+
},
|
|
120
|
+
required: ["from", "to"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "send_facebook_message",
|
|
125
|
+
description: "Send a Facebook Messenger message",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
from: { type: "string", description: "Sender ID (Facebook page)" },
|
|
130
|
+
to: { type: "string", description: "Recipient PSID (page-scoped user ID)" },
|
|
131
|
+
text: { type: "string", description: "Message text" },
|
|
132
|
+
},
|
|
133
|
+
required: ["from", "to", "text"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
80
136
|
{
|
|
81
137
|
name: "get_message_status",
|
|
82
138
|
description: "Get message delivery status by ID",
|
|
@@ -100,12 +156,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
100
156
|
type: "object",
|
|
101
157
|
properties: {
|
|
102
158
|
url: { type: "string", description: "Webhook URL to receive events" },
|
|
103
|
-
channel: { type: "string", enum: ["sms", "whatsapp", "rcs"], description: "Channel to subscribe to" },
|
|
159
|
+
channel: { type: "string", enum: ["sms", "whatsapp", "rcs", "email", "voice", "facebook"], description: "Channel to subscribe to" },
|
|
104
160
|
eventType: { type: "string", enum: ["MESSAGE", "MESSAGE_STATUS"], description: "Event type" },
|
|
105
161
|
},
|
|
106
162
|
required: ["url", "channel", "eventType"],
|
|
107
163
|
},
|
|
108
164
|
},
|
|
165
|
+
{
|
|
166
|
+
name: "list_subscriptions",
|
|
167
|
+
description: "List all webhook subscriptions",
|
|
168
|
+
inputSchema: { type: "object", properties: {} },
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "delete_subscription",
|
|
172
|
+
description: "Delete a webhook subscription by ID",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
id: { type: "string", description: "Subscription ID" },
|
|
177
|
+
},
|
|
178
|
+
required: ["id"],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
109
181
|
{
|
|
110
182
|
name: "list_contacts",
|
|
111
183
|
description: "List contacts from the contact base",
|
|
@@ -117,6 +189,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
117
189
|
},
|
|
118
190
|
},
|
|
119
191
|
},
|
|
192
|
+
{
|
|
193
|
+
name: "create_contact",
|
|
194
|
+
description: "Create a contact in the contact base",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
name: { type: "string", description: "Contact full name" },
|
|
199
|
+
phone: { type: "string", description: "Phone number with country code" },
|
|
200
|
+
email: { type: "string", description: "Email address" },
|
|
201
|
+
groupId: { type: "string", description: "Optional group ID to add the contact to" },
|
|
202
|
+
},
|
|
203
|
+
required: ["name"],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "delete_contact",
|
|
208
|
+
description: "Delete a contact by ID",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
id: { type: "string", description: "Contact ID" },
|
|
213
|
+
},
|
|
214
|
+
required: ["id"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
120
217
|
{
|
|
121
218
|
name: "send_template",
|
|
122
219
|
description: "Send a WhatsApp template message (pre-approved)",
|
|
@@ -134,6 +231,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
134
231
|
required: ["from", "to", "templateId"],
|
|
135
232
|
},
|
|
136
233
|
},
|
|
234
|
+
{
|
|
235
|
+
name: "list_templates",
|
|
236
|
+
description: "List approved message templates (WhatsApp/SMS/RCS)",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
channel: { type: "string", enum: ["sms", "whatsapp", "rcs"], description: "Filter templates by channel" },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "get_report_entries",
|
|
246
|
+
description: "Get message report entries within a date range",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
channel: { type: "string", enum: ["sms", "whatsapp", "rcs", "email", "voice", "facebook"], description: "Channel to report on" },
|
|
251
|
+
startDate: { type: "string", description: "ISO 8601 start date (e.g. 2026-04-01)" },
|
|
252
|
+
endDate: { type: "string", description: "ISO 8601 end date (e.g. 2026-04-24)" },
|
|
253
|
+
},
|
|
254
|
+
required: ["channel", "startDate", "endDate"],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "add_opt_out",
|
|
259
|
+
description: "Add a phone number to the opt-out list (suppresses future messages)",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: {
|
|
263
|
+
channel: { type: "string", enum: ["sms", "whatsapp", "rcs", "voice"], description: "Channel for the opt-out" },
|
|
264
|
+
from: { type: "string", description: "Sender ID the opt-out applies to" },
|
|
265
|
+
phone: { type: "string", description: "Phone number to opt out (with country code)" },
|
|
266
|
+
},
|
|
267
|
+
required: ["channel", "from", "phone"],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
137
270
|
],
|
|
138
271
|
}));
|
|
139
272
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -146,12 +279,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
146
279
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/whatsapp/messages", { from: args?.from, to: args?.to, contents: [{ type: "text", text: args?.text }] }), null, 2) }] };
|
|
147
280
|
case "send_rcs":
|
|
148
281
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/rcs/messages", { from: args?.from, to: args?.to, contents: [{ type: "text", text: args?.text }] }), null, 2) }] };
|
|
282
|
+
case "send_email": {
|
|
283
|
+
const contents = [];
|
|
284
|
+
if (args?.html)
|
|
285
|
+
contents.push({ type: "email", html: args.html, subject: args?.subject });
|
|
286
|
+
else if (args?.text)
|
|
287
|
+
contents.push({ type: "email", text: args.text, subject: args?.subject });
|
|
288
|
+
else
|
|
289
|
+
contents.push({ type: "email", subject: args?.subject });
|
|
290
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/email/messages", { from: args?.from, to: args?.to, contents }), null, 2) }] };
|
|
291
|
+
}
|
|
292
|
+
case "send_voice": {
|
|
293
|
+
const contents = [];
|
|
294
|
+
if (args?.audioUrl)
|
|
295
|
+
contents.push({ type: "audio", url: args.audioUrl });
|
|
296
|
+
else if (args?.text)
|
|
297
|
+
contents.push({ type: "text", text: args.text });
|
|
298
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/voice/messages", { from: args?.from, to: args?.to, contents }), null, 2) }] };
|
|
299
|
+
}
|
|
300
|
+
case "send_facebook_message":
|
|
301
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/facebook/messages", { from: args?.from, to: args?.to, contents: [{ type: "text", text: args?.text }] }), null, 2) }] };
|
|
149
302
|
case "get_message_status":
|
|
150
303
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", `/reports/${args?.id}`), null, 2) }] };
|
|
151
304
|
case "list_channels":
|
|
152
305
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", "/channels"), null, 2) }] };
|
|
153
306
|
case "create_subscription":
|
|
154
307
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/subscriptions", { webhook: { url: args?.url }, criteria: { channel: args?.channel }, eventType: args?.eventType }), null, 2) }] };
|
|
308
|
+
case "list_subscriptions":
|
|
309
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", "/subscriptions"), null, 2) }] };
|
|
310
|
+
case "delete_subscription":
|
|
311
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("DELETE", `/subscriptions/${args?.id}`), null, 2) }] };
|
|
155
312
|
case "list_contacts": {
|
|
156
313
|
const params = new URLSearchParams();
|
|
157
314
|
if (args?.page)
|
|
@@ -160,8 +317,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
160
317
|
params.set("size", String(args.size));
|
|
161
318
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", `/contacts?${params}`), null, 2) }] };
|
|
162
319
|
}
|
|
320
|
+
case "create_contact": {
|
|
321
|
+
const body = { name: args?.name };
|
|
322
|
+
if (args?.phone)
|
|
323
|
+
body.phone = args.phone;
|
|
324
|
+
if (args?.email)
|
|
325
|
+
body.email = args.email;
|
|
326
|
+
if (args?.groupId)
|
|
327
|
+
body.groupId = args.groupId;
|
|
328
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/contacts", body), null, 2) }] };
|
|
329
|
+
}
|
|
330
|
+
case "delete_contact":
|
|
331
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("DELETE", `/contacts/${args?.id}`), null, 2) }] };
|
|
163
332
|
case "send_template":
|
|
164
333
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/whatsapp/messages", { from: args?.from, to: args?.to, contents: [{ type: "template", templateId: args?.templateId, fields: args?.fields || {} }] }), null, 2) }] };
|
|
334
|
+
case "list_templates": {
|
|
335
|
+
const params = new URLSearchParams();
|
|
336
|
+
if (args?.channel)
|
|
337
|
+
params.set("channel", String(args.channel));
|
|
338
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", `/templates?${params}`), null, 2) }] };
|
|
339
|
+
}
|
|
340
|
+
case "get_report_entries": {
|
|
341
|
+
const params = new URLSearchParams();
|
|
342
|
+
params.set("startDate", String(args?.startDate));
|
|
343
|
+
params.set("endDate", String(args?.endDate));
|
|
344
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", `/reports/${args?.channel}/entries?${params}`), null, 2) }] };
|
|
345
|
+
}
|
|
346
|
+
case "add_opt_out":
|
|
347
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", `/channels/${args?.channel}/senders/${args?.from}/opt-outs`, { phone: args?.phone }), null, 2) }] };
|
|
165
348
|
default:
|
|
166
349
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
167
350
|
}
|
|
@@ -171,11 +354,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
171
354
|
}
|
|
172
355
|
});
|
|
173
356
|
async function main() {
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
357
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
358
|
+
const { default: express } = await import("express");
|
|
359
|
+
const { randomUUID } = await import("node:crypto");
|
|
360
|
+
const app = express();
|
|
361
|
+
app.use(express.json());
|
|
362
|
+
const transports = new Map();
|
|
363
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
364
|
+
app.post("/mcp", async (req, res) => {
|
|
365
|
+
const sid = req.headers["mcp-session-id"];
|
|
366
|
+
if (sid && transports.has(sid)) {
|
|
367
|
+
await transports.get(sid).handleRequest(req, res, req.body);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
371
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
372
|
+
t.onclose = () => { if (t.sessionId)
|
|
373
|
+
transports.delete(t.sessionId); };
|
|
374
|
+
const s = new Server({ name: "mcp-zenvia", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
375
|
+
server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
|
|
376
|
+
server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
|
|
377
|
+
await s.connect(t);
|
|
378
|
+
await t.handleRequest(req, res, req.body);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
382
|
+
});
|
|
383
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
384
|
+
await transports.get(sid).handleRequest(req, res);
|
|
385
|
+
else
|
|
386
|
+
res.status(400).send("Invalid session"); });
|
|
387
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
388
|
+
await transports.get(sid).handleRequest(req, res);
|
|
389
|
+
else
|
|
390
|
+
res.status(400).send("Invalid session"); });
|
|
391
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
392
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
const transport = new StdioServerTransport();
|
|
396
|
+
await server.connect(transport);
|
|
177
397
|
}
|
|
178
|
-
const transport = new StdioServerTransport();
|
|
179
|
-
await server.connect(transport);
|
|
180
398
|
}
|
|
181
399
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codespar/mcp-zenvia",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Zenvia — SMS, WhatsApp, RCS messaging and
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for Zenvia — SMS, WhatsApp, RCS, Email, Voice, Facebook messaging, templates and reports",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"sms",
|
|
26
26
|
"whatsapp",
|
|
27
27
|
"rcs",
|
|
28
|
+
"email",
|
|
29
|
+
"voice",
|
|
30
|
+
"facebook",
|
|
28
31
|
"brazil"
|
|
29
|
-
]
|
|
32
|
+
],
|
|
33
|
+
"mcpName": "io.github.codespar/mcp-zenvia"
|
|
30
34
|
}
|
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, Email, Voice, Facebook messaging, templates and reports",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/codespar/mcp-dev-brasil",
|
|
7
|
+
"source": "github",
|
|
8
|
+
"subfolder": "packages/communication/zenvia"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.2.0",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@codespar/mcp-zenvia",
|
|
15
|
+
"version": "0.2.0",
|
|
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
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* MCP Server for Zenvia — multi-channel messaging (SMS, WhatsApp, RCS).
|
|
4
|
+
* MCP Server for Zenvia — multi-channel messaging (SMS, WhatsApp, RCS, Email, Voice, Facebook).
|
|
5
5
|
*
|
|
6
6
|
* Tools:
|
|
7
7
|
* - send_sms: Send an SMS message
|
|
8
8
|
* - send_whatsapp: Send a WhatsApp message
|
|
9
9
|
* - send_rcs: Send an RCS message
|
|
10
|
+
* - send_email: Send a transactional email
|
|
11
|
+
* - send_voice: Send a voice message (TTS or pre-recorded audio)
|
|
12
|
+
* - send_facebook_message: Send a Facebook Messenger message
|
|
10
13
|
* - get_message_status: Get message delivery status
|
|
11
14
|
* - list_channels: List available messaging channels
|
|
12
15
|
* - create_subscription: Create a webhook subscription for events
|
|
16
|
+
* - list_subscriptions: List all webhook subscriptions
|
|
17
|
+
* - delete_subscription: Delete a webhook subscription
|
|
13
18
|
* - list_contacts: List contacts
|
|
19
|
+
* - create_contact: Create a contact in the contact base
|
|
20
|
+
* - delete_contact: Delete a contact
|
|
14
21
|
* - send_template: Send a WhatsApp template message
|
|
22
|
+
* - list_templates: List approved WhatsApp templates
|
|
23
|
+
* - get_report_entries: Get message report entries by date range
|
|
24
|
+
* - add_opt_out: Add a phone number to the opt-out list
|
|
15
25
|
*
|
|
16
26
|
* Environment:
|
|
17
27
|
* ZENVIA_API_TOKEN — API token from https://app.zenvia.com/
|
|
@@ -19,6 +29,8 @@
|
|
|
19
29
|
|
|
20
30
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
31
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
32
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
33
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
22
34
|
import {
|
|
23
35
|
CallToolRequestSchema,
|
|
24
36
|
ListToolsRequestSchema,
|
|
@@ -40,11 +52,13 @@ async function zenviaRequest(method: string, path: string, body?: unknown): Prom
|
|
|
40
52
|
const err = await res.text();
|
|
41
53
|
throw new Error(`Zenvia API ${res.status}: ${err}`);
|
|
42
54
|
}
|
|
43
|
-
|
|
55
|
+
// Some endpoints (DELETE) return empty
|
|
56
|
+
const text = await res.text();
|
|
57
|
+
return text ? JSON.parse(text) : { ok: true };
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
const server = new Server(
|
|
47
|
-
{ name: "mcp-zenvia", version: "0.
|
|
61
|
+
{ name: "mcp-zenvia", version: "0.2.0" },
|
|
48
62
|
{ capabilities: { tools: {} } }
|
|
49
63
|
);
|
|
50
64
|
|
|
@@ -89,6 +103,48 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
89
103
|
required: ["from", "to", "text"],
|
|
90
104
|
},
|
|
91
105
|
},
|
|
106
|
+
{
|
|
107
|
+
name: "send_email",
|
|
108
|
+
description: "Send a transactional email",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
from: { type: "string", description: "Sender email address (verified domain)" },
|
|
113
|
+
to: { type: "string", description: "Recipient email address" },
|
|
114
|
+
subject: { type: "string", description: "Email subject" },
|
|
115
|
+
html: { type: "string", description: "HTML body of the email" },
|
|
116
|
+
text: { type: "string", description: "Plain text body (fallback)" },
|
|
117
|
+
},
|
|
118
|
+
required: ["from", "to", "subject"],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "send_voice",
|
|
123
|
+
description: "Send a voice message via TTS or pre-recorded audio URL",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {
|
|
127
|
+
from: { type: "string", description: "Sender ID (Voice channel)" },
|
|
128
|
+
to: { type: "string", description: "Recipient phone number with country code" },
|
|
129
|
+
text: { type: "string", description: "Text to be spoken (TTS) — use either text or audioUrl" },
|
|
130
|
+
audioUrl: { type: "string", description: "URL of pre-recorded audio file — use either text or audioUrl" },
|
|
131
|
+
},
|
|
132
|
+
required: ["from", "to"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "send_facebook_message",
|
|
137
|
+
description: "Send a Facebook Messenger message",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: {
|
|
141
|
+
from: { type: "string", description: "Sender ID (Facebook page)" },
|
|
142
|
+
to: { type: "string", description: "Recipient PSID (page-scoped user ID)" },
|
|
143
|
+
text: { type: "string", description: "Message text" },
|
|
144
|
+
},
|
|
145
|
+
required: ["from", "to", "text"],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
92
148
|
{
|
|
93
149
|
name: "get_message_status",
|
|
94
150
|
description: "Get message delivery status by ID",
|
|
@@ -112,12 +168,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
112
168
|
type: "object",
|
|
113
169
|
properties: {
|
|
114
170
|
url: { type: "string", description: "Webhook URL to receive events" },
|
|
115
|
-
channel: { type: "string", enum: ["sms", "whatsapp", "rcs"], description: "Channel to subscribe to" },
|
|
171
|
+
channel: { type: "string", enum: ["sms", "whatsapp", "rcs", "email", "voice", "facebook"], description: "Channel to subscribe to" },
|
|
116
172
|
eventType: { type: "string", enum: ["MESSAGE", "MESSAGE_STATUS"], description: "Event type" },
|
|
117
173
|
},
|
|
118
174
|
required: ["url", "channel", "eventType"],
|
|
119
175
|
},
|
|
120
176
|
},
|
|
177
|
+
{
|
|
178
|
+
name: "list_subscriptions",
|
|
179
|
+
description: "List all webhook subscriptions",
|
|
180
|
+
inputSchema: { type: "object", properties: {} },
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "delete_subscription",
|
|
184
|
+
description: "Delete a webhook subscription by ID",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
id: { type: "string", description: "Subscription ID" },
|
|
189
|
+
},
|
|
190
|
+
required: ["id"],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
121
193
|
{
|
|
122
194
|
name: "list_contacts",
|
|
123
195
|
description: "List contacts from the contact base",
|
|
@@ -129,6 +201,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
129
201
|
},
|
|
130
202
|
},
|
|
131
203
|
},
|
|
204
|
+
{
|
|
205
|
+
name: "create_contact",
|
|
206
|
+
description: "Create a contact in the contact base",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
name: { type: "string", description: "Contact full name" },
|
|
211
|
+
phone: { type: "string", description: "Phone number with country code" },
|
|
212
|
+
email: { type: "string", description: "Email address" },
|
|
213
|
+
groupId: { type: "string", description: "Optional group ID to add the contact to" },
|
|
214
|
+
},
|
|
215
|
+
required: ["name"],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "delete_contact",
|
|
220
|
+
description: "Delete a contact by ID",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {
|
|
224
|
+
id: { type: "string", description: "Contact ID" },
|
|
225
|
+
},
|
|
226
|
+
required: ["id"],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
132
229
|
{
|
|
133
230
|
name: "send_template",
|
|
134
231
|
description: "Send a WhatsApp template message (pre-approved)",
|
|
@@ -146,6 +243,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
146
243
|
required: ["from", "to", "templateId"],
|
|
147
244
|
},
|
|
148
245
|
},
|
|
246
|
+
{
|
|
247
|
+
name: "list_templates",
|
|
248
|
+
description: "List approved message templates (WhatsApp/SMS/RCS)",
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
channel: { type: "string", enum: ["sms", "whatsapp", "rcs"], description: "Filter templates by channel" },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "get_report_entries",
|
|
258
|
+
description: "Get message report entries within a date range",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: {
|
|
262
|
+
channel: { type: "string", enum: ["sms", "whatsapp", "rcs", "email", "voice", "facebook"], description: "Channel to report on" },
|
|
263
|
+
startDate: { type: "string", description: "ISO 8601 start date (e.g. 2026-04-01)" },
|
|
264
|
+
endDate: { type: "string", description: "ISO 8601 end date (e.g. 2026-04-24)" },
|
|
265
|
+
},
|
|
266
|
+
required: ["channel", "startDate", "endDate"],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "add_opt_out",
|
|
271
|
+
description: "Add a phone number to the opt-out list (suppresses future messages)",
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
channel: { type: "string", enum: ["sms", "whatsapp", "rcs", "voice"], description: "Channel for the opt-out" },
|
|
276
|
+
from: { type: "string", description: "Sender ID the opt-out applies to" },
|
|
277
|
+
phone: { type: "string", description: "Phone number to opt out (with country code)" },
|
|
278
|
+
},
|
|
279
|
+
required: ["channel", "from", "phone"],
|
|
280
|
+
},
|
|
281
|
+
},
|
|
149
282
|
],
|
|
150
283
|
}));
|
|
151
284
|
|
|
@@ -160,20 +293,61 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
160
293
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/whatsapp/messages", { from: args?.from, to: args?.to, contents: [{ type: "text", text: args?.text }] }), null, 2) }] };
|
|
161
294
|
case "send_rcs":
|
|
162
295
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/rcs/messages", { from: args?.from, to: args?.to, contents: [{ type: "text", text: args?.text }] }), null, 2) }] };
|
|
296
|
+
case "send_email": {
|
|
297
|
+
const contents: any[] = [];
|
|
298
|
+
if (args?.html) contents.push({ type: "email", html: args.html, subject: args?.subject });
|
|
299
|
+
else if (args?.text) contents.push({ type: "email", text: args.text, subject: args?.subject });
|
|
300
|
+
else contents.push({ type: "email", subject: args?.subject });
|
|
301
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/email/messages", { from: args?.from, to: args?.to, contents }), null, 2) }] };
|
|
302
|
+
}
|
|
303
|
+
case "send_voice": {
|
|
304
|
+
const contents: any[] = [];
|
|
305
|
+
if (args?.audioUrl) contents.push({ type: "audio", url: args.audioUrl });
|
|
306
|
+
else if (args?.text) contents.push({ type: "text", text: args.text });
|
|
307
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/voice/messages", { from: args?.from, to: args?.to, contents }), null, 2) }] };
|
|
308
|
+
}
|
|
309
|
+
case "send_facebook_message":
|
|
310
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/facebook/messages", { from: args?.from, to: args?.to, contents: [{ type: "text", text: args?.text }] }), null, 2) }] };
|
|
163
311
|
case "get_message_status":
|
|
164
312
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", `/reports/${args?.id}`), null, 2) }] };
|
|
165
313
|
case "list_channels":
|
|
166
314
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", "/channels"), null, 2) }] };
|
|
167
315
|
case "create_subscription":
|
|
168
316
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/subscriptions", { webhook: { url: args?.url }, criteria: { channel: args?.channel }, eventType: args?.eventType }), null, 2) }] };
|
|
317
|
+
case "list_subscriptions":
|
|
318
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", "/subscriptions"), null, 2) }] };
|
|
319
|
+
case "delete_subscription":
|
|
320
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("DELETE", `/subscriptions/${args?.id}`), null, 2) }] };
|
|
169
321
|
case "list_contacts": {
|
|
170
322
|
const params = new URLSearchParams();
|
|
171
323
|
if (args?.page) params.set("page", String(args.page));
|
|
172
324
|
if (args?.size) params.set("size", String(args.size));
|
|
173
325
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", `/contacts?${params}`), null, 2) }] };
|
|
174
326
|
}
|
|
327
|
+
case "create_contact": {
|
|
328
|
+
const body: any = { name: args?.name };
|
|
329
|
+
if (args?.phone) body.phone = args.phone;
|
|
330
|
+
if (args?.email) body.email = args.email;
|
|
331
|
+
if (args?.groupId) body.groupId = args.groupId;
|
|
332
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/contacts", body), null, 2) }] };
|
|
333
|
+
}
|
|
334
|
+
case "delete_contact":
|
|
335
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("DELETE", `/contacts/${args?.id}`), null, 2) }] };
|
|
175
336
|
case "send_template":
|
|
176
337
|
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", "/channels/whatsapp/messages", { from: args?.from, to: args?.to, contents: [{ type: "template", templateId: args?.templateId, fields: args?.fields || {} }] }), null, 2) }] };
|
|
338
|
+
case "list_templates": {
|
|
339
|
+
const params = new URLSearchParams();
|
|
340
|
+
if (args?.channel) params.set("channel", String(args.channel));
|
|
341
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", `/templates?${params}`), null, 2) }] };
|
|
342
|
+
}
|
|
343
|
+
case "get_report_entries": {
|
|
344
|
+
const params = new URLSearchParams();
|
|
345
|
+
params.set("startDate", String(args?.startDate));
|
|
346
|
+
params.set("endDate", String(args?.endDate));
|
|
347
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("GET", `/reports/${args?.channel}/entries?${params}`), null, 2) }] };
|
|
348
|
+
}
|
|
349
|
+
case "add_opt_out":
|
|
350
|
+
return { content: [{ type: "text", text: JSON.stringify(await zenviaRequest("POST", `/channels/${args?.channel}/senders/${args?.from}/opt-outs`, { phone: args?.phone }), null, 2) }] };
|
|
177
351
|
default:
|
|
178
352
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
179
353
|
}
|
|
@@ -183,12 +357,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
183
357
|
});
|
|
184
358
|
|
|
185
359
|
async function main() {
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
360
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
361
|
+
const { default: express } = await import("express");
|
|
362
|
+
const { randomUUID } = await import("node:crypto");
|
|
363
|
+
const app = express();
|
|
364
|
+
app.use(express.json());
|
|
365
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
366
|
+
app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
|
|
367
|
+
app.post("/mcp", async (req: any, res: any) => {
|
|
368
|
+
const sid = req.headers["mcp-session-id"] as string | undefined;
|
|
369
|
+
if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
|
|
370
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
371
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
372
|
+
t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
|
|
373
|
+
const s = new Server({ name: "mcp-zenvia", version: "0.2.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);
|
|
374
|
+
await t.handleRequest(req, res, req.body); return;
|
|
375
|
+
}
|
|
376
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
377
|
+
});
|
|
378
|
+
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"); });
|
|
379
|
+
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"); });
|
|
380
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
381
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
382
|
+
} else {
|
|
383
|
+
const transport = new StdioServerTransport();
|
|
384
|
+
await server.connect(transport);
|
|
189
385
|
}
|
|
190
|
-
const transport = new StdioServerTransport();
|
|
191
|
-
await server.connect(transport);
|
|
192
386
|
}
|
|
193
387
|
|
|
194
388
|
main().catch(console.error);
|