supersendtx-mcp 0.6.29 → 0.6.32
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 +60 -12
- package/dist/index.js +266 -64
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# supersendtx-mcp
|
|
2
2
|
|
|
3
|
-
MCP server for [SuperSend TX](https://supersendtx.com) — send email and manage domains, webhooks, suppressions, and templates from Cursor, Claude Code, and other MCP clients.
|
|
3
|
+
MCP server for [SuperSend TX](https://supersendtx.com) — send email and manage domains, webhooks, suppressions, and templates from Cursor, Claude Code, and other MCP clients, and talk to Ranla, the agent in the same account.
|
|
4
4
|
|
|
5
5
|
**Install:** `npx -y supersendtx-mcp` · **Docs:** https://docs.supersendtx.com/ai/mcp
|
|
6
6
|
|
|
@@ -10,14 +10,14 @@ MCP server for [SuperSend TX](https://supersendtx.com) — send email and manage
|
|
|
10
10
|
|------|---------------|------|
|
|
11
11
|
| **stdio** (default) | `npx -y supersendtx-mcp` | `SUPERSENDTX_API_KEY=stx_…` or `rnl_…` |
|
|
12
12
|
| **HTTP** (local) | `npx -y supersendtx-mcp --http --port 3000` | `Authorization: Bearer stx_…` or `rnl_…` on each request |
|
|
13
|
-
| **HTTP** (hosted) | `https://mcp.
|
|
13
|
+
| **HTTP** (hosted) | `https://mcp.ranla.ai/mcp` | **OAuth** (default) or `Authorization: Bearer stx_…` or `rnl_…` |
|
|
14
14
|
|
|
15
|
-
Optional: `SUPERSENDTX_API_URL` (
|
|
15
|
+
Create API keys on the dashboard **API keys** page. `RANLA_API_KEY` works in place of `SUPERSENDTX_API_KEY`. Optional: `SUPERSENDTX_API_URL` sets the mail API base URL (default `https://api.supersendtx.com`). Over HTTP the server reads only the `Authorization` header.
|
|
16
16
|
|
|
17
17
|
Local HTTP endpoint: `http://127.0.0.1:3000/mcp` · health: `GET /health`
|
|
18
|
-
Hosted health: `GET https://mcp.
|
|
18
|
+
Hosted health: `GET https://mcp.ranla.ai/health`
|
|
19
19
|
|
|
20
|
-
Hosted OAuth: add only `"url": "https://mcp.supersendtx.com/mcp
|
|
20
|
+
Hosted OAuth: add only `"url": "https://mcp.ranla.ai/mcp"`. The client opens a browser to sign in on `app.ranla.ai`; click **Allow access**. `https://mcp.supersendtx.com/mcp` is the same server, and existing configs keep working. A Bearer key still works for advanced setups.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -60,11 +60,25 @@ npx -y supersendtx-mcp --http --port 3000
|
|
|
60
60
|
|
|
61
61
|
### Cursor (hosted)
|
|
62
62
|
|
|
63
|
+
OAuth (default) — no key in config:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"supersendtx": {
|
|
69
|
+
"url": "https://mcp.ranla.ai/mcp"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Bearer (advanced):
|
|
76
|
+
|
|
63
77
|
```json
|
|
64
78
|
{
|
|
65
79
|
"mcpServers": {
|
|
66
80
|
"supersendtx": {
|
|
67
|
-
"url": "https://mcp.
|
|
81
|
+
"url": "https://mcp.ranla.ai/mcp",
|
|
68
82
|
"headers": {
|
|
69
83
|
"Authorization": "Bearer stx_your_key_here"
|
|
70
84
|
}
|
|
@@ -78,23 +92,57 @@ npx -y supersendtx-mcp --http --port 3000
|
|
|
78
92
|
## Claude Code
|
|
79
93
|
|
|
80
94
|
```bash
|
|
95
|
+
# HTTP (hosted, OAuth — run /mcp in Claude Code to sign in)
|
|
96
|
+
claude mcp add --transport http supersendtx https://mcp.ranla.ai/mcp
|
|
97
|
+
|
|
81
98
|
# stdio
|
|
82
99
|
claude mcp add --transport stdio supersendtx -- npx -y supersendtx-mcp
|
|
83
100
|
|
|
84
101
|
# HTTP (local — after starting --http server)
|
|
85
|
-
claude mcp add --transport http supersendtx http://127.0.0.1:3000/mcp
|
|
86
|
-
|
|
87
|
-
# HTTP (hosted)
|
|
88
|
-
claude mcp add --transport http supersendtx https://mcp.supersendtx.com/mcp
|
|
102
|
+
claude mcp add --transport http supersendtx http://127.0.0.1:3000/mcp --header "Authorization: Bearer stx_your_key_here"
|
|
89
103
|
```
|
|
90
104
|
|
|
91
|
-
Export `SUPERSENDTX_API_KEY` for stdio
|
|
105
|
+
Export `SUPERSENDTX_API_KEY` for stdio.
|
|
92
106
|
|
|
93
107
|
---
|
|
94
108
|
|
|
95
109
|
## Tools
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
### Mail tools
|
|
112
|
+
|
|
113
|
+
These call the public API through the `supersendtx` SDK.
|
|
114
|
+
|
|
115
|
+
| Tool | Purpose |
|
|
116
|
+
|------|---------|
|
|
117
|
+
| `send_email` | Send now, or later with `scheduled_at` |
|
|
118
|
+
| `list_emails` / `get_email` | List or fetch sends |
|
|
119
|
+
| `cancel_email` | Cancel a scheduled send |
|
|
120
|
+
| `list_received_emails` / `get_received_email` | Inbound mail |
|
|
121
|
+
| `list_domains` / `get_domain` / `create_domain` | Domains; `get_domain` returns DNS records |
|
|
122
|
+
| `apply_domain_dns` / `verify_domain` | Apply DNS (`cloudflare`, `godaddy`, `vercel`) and verify |
|
|
123
|
+
| `list_webhooks` / `create_webhook` / `delete_webhook` | Webhook endpoints |
|
|
124
|
+
| `list_suppressions` / `add_suppression` / `remove_suppression` | Suppression list |
|
|
125
|
+
| `list_templates` / `get_template` / `create_template` / `publish_template` | Templates |
|
|
126
|
+
| `send_test_webhook_event` | Test webhook event (outbound email events) |
|
|
127
|
+
| `get_deliverability` | Best-effort metrics (`7d` / `30d`) |
|
|
128
|
+
|
|
129
|
+
`send_email` takes `from`, `to`, and either `subject` with `html` / `text` or `template_id` / `template_alias` with `variables`. Optional: `cc`, `bcc`, `reply_to`, `scheduled_at`, `category` (`transactional`, `product`, `newsletter`), `tags`, `headers`, `idempotency_key`, `unsubscribe`. Recipient fields take one address, several separated by commas, or an array.
|
|
130
|
+
|
|
131
|
+
### Ranla agent tools
|
|
132
|
+
|
|
133
|
+
These call the app host and need a full-access key (OAuth sign-in and dashboard keys have one; a `sending`-scope key gets a 403).
|
|
134
|
+
|
|
135
|
+
| Tool | Purpose |
|
|
136
|
+
|------|---------|
|
|
137
|
+
| `arc_ensure_thread` / `arc_list_threads` | Durable thread for this client |
|
|
138
|
+
| `arc_message` | Send text to Ranla and wait for the turn — returns `text`, `artifacts`, and `pendingApproval` |
|
|
139
|
+
| `arc_list_approvals` / `arc_approve` / `arc_reject` | Answer approvals |
|
|
140
|
+
| `arc_request_connection` | Connect link for a missing integration |
|
|
141
|
+
| `arc_<tool>` | Read-only Ranla tools, listed from the account with input schemas |
|
|
142
|
+
|
|
143
|
+
Flow: `arc_ensure_thread` → `arc_message` → if a step needs approval, `arc_list_approvals` → `arc_approve` or `arc_reject`.
|
|
144
|
+
|
|
145
|
+
Full reference: https://docs.supersendtx.com/ai/mcp
|
|
98
146
|
|
|
99
147
|
## License
|
|
100
148
|
|
package/dist/index.js
CHANGED
|
@@ -55,12 +55,13 @@ function printHelp() {
|
|
|
55
55
|
supersendtx-mcp # stdio (default)
|
|
56
56
|
supersendtx-mcp --http [--port 3000] [--host 127.0.0.1]
|
|
57
57
|
|
|
58
|
-
Environment:
|
|
59
|
-
SUPERSENDTX_API_KEY
|
|
60
|
-
SUPERSENDTX_API_URL Optional API base (
|
|
58
|
+
Environment (RANLA_* or SUPERSENDTX_*):
|
|
59
|
+
RANLA_API_KEY / SUPERSENDTX_API_KEY API key (rnl_\u2026 or stx_\u2026), required for stdio
|
|
60
|
+
RANLA_API_URL / SUPERSENDTX_API_URL Optional mail API base (ranla-mcp: https://api.ranla.ai)
|
|
61
|
+
RANLA_APP_URL / SUPERSENDTX_APP_URL Optional app host for the Ranla agent tools
|
|
61
62
|
|
|
62
63
|
HTTP auth:
|
|
63
|
-
Authorization: Bearer
|
|
64
|
+
Authorization: Bearer rnl_\u2026 or stx_\u2026 (API key) or MCP OAuth access token
|
|
64
65
|
`);
|
|
65
66
|
}
|
|
66
67
|
function isSuperSendTxApiKey(token) {
|
|
@@ -82,12 +83,19 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
82
83
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
83
84
|
import { SuperSendTX } from "supersendtx";
|
|
84
85
|
|
|
86
|
+
// package.json
|
|
87
|
+
var version = "0.6.32";
|
|
88
|
+
|
|
85
89
|
// src/arc-talk-tools.ts
|
|
86
90
|
var ARC_MCP_TALK_GROWTH_TOOL_NAMES = [
|
|
87
|
-
|
|
91
|
+
// Cold-start SEO plan: a minute of vendor data and a planning call. Read-only, so MCP carries it.
|
|
92
|
+
"seo_plan",
|
|
88
93
|
"get_business_brief",
|
|
89
94
|
"get_brand",
|
|
90
95
|
"read_url",
|
|
96
|
+
// A canvas document Ranla wrote, as it stands now. Writing to one mutates
|
|
97
|
+
// and goes through `arc_message` like every other write.
|
|
98
|
+
"read_canvas",
|
|
91
99
|
"get_setup_status",
|
|
92
100
|
"get_growth_health",
|
|
93
101
|
"get_revenue_health",
|
|
@@ -184,7 +192,7 @@ var RANLA_AGENT_NAME = "Ranla";
|
|
|
184
192
|
var TALK_READ_TOOLS = ARC_MCP_TALK_GROWTH_TOOL_NAMES.map((growthTool) => ({
|
|
185
193
|
name: `arc_${growthTool}`,
|
|
186
194
|
description: `Read-only Ranla tool: ${growthTool.replace(/_/g, " ")}.`,
|
|
187
|
-
inputSchema: openObjectSchema("
|
|
195
|
+
inputSchema: openObjectSchema("Pass the tool arguments as fields."),
|
|
188
196
|
kind: "growth",
|
|
189
197
|
growthTool
|
|
190
198
|
}));
|
|
@@ -225,7 +233,7 @@ var META_TOOLS = [
|
|
|
225
233
|
},
|
|
226
234
|
{
|
|
227
235
|
name: "arc_message",
|
|
228
|
-
description: "Send a message to Ranla and wait for the turn. Use for strategy and
|
|
236
|
+
description: "Send a message to Ranla and wait for the turn. Use for strategy and anything that changes the account. Returns text, artifacts (drafts, plans, and documents it made or opened, by id), and pendingApproval when a step needs approval.",
|
|
229
237
|
inputSchema: {
|
|
230
238
|
type: "object",
|
|
231
239
|
properties: {
|
|
@@ -244,7 +252,7 @@ var META_TOOLS = [
|
|
|
244
252
|
},
|
|
245
253
|
{
|
|
246
254
|
name: "arc_approve",
|
|
247
|
-
description: "Approve a pending Ranla
|
|
255
|
+
description: "Approve a pending Ranla approval and resume the run.",
|
|
248
256
|
inputSchema: {
|
|
249
257
|
type: "object",
|
|
250
258
|
properties: {
|
|
@@ -256,7 +264,7 @@ var META_TOOLS = [
|
|
|
256
264
|
},
|
|
257
265
|
{
|
|
258
266
|
name: "arc_reject",
|
|
259
|
-
description: "Reject a pending Ranla
|
|
267
|
+
description: "Reject a pending Ranla approval and resume the run.",
|
|
260
268
|
inputSchema: {
|
|
261
269
|
type: "object",
|
|
262
270
|
properties: {
|
|
@@ -273,7 +281,7 @@ function resolveArcAppOrigin(options = {}) {
|
|
|
273
281
|
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
|
274
282
|
const key = options.apiKey ?? process.env.SUPERSENDTX_API_KEY ?? process.env.RANLA_API_KEY ?? "";
|
|
275
283
|
if (key.startsWith("rnl_")) return DEFAULT_RANLA_APP_ORIGIN;
|
|
276
|
-
const apiUrl = (options.baseUrl ?? process.env.SUPERSENDTX_API_URL ?? "").replace(/\/$/, "");
|
|
284
|
+
const apiUrl = (options.baseUrl ?? process.env.SUPERSENDTX_API_URL ?? process.env.RANLA_API_URL ?? "").replace(/\/$/, "");
|
|
277
285
|
if (apiUrl.includes("api.ranla.ai")) return DEFAULT_RANLA_APP_ORIGIN;
|
|
278
286
|
if (apiUrl.includes("app.ranla.ai")) return apiUrl;
|
|
279
287
|
if (apiUrl.includes("app.supersendtx.com")) return apiUrl;
|
|
@@ -292,17 +300,55 @@ async function arcFetch(origin, apiKey, path, init = {}) {
|
|
|
292
300
|
const body = await response.json().catch(() => ({}));
|
|
293
301
|
return { ok: response.ok, status: response.status, body };
|
|
294
302
|
}
|
|
303
|
+
var ARC_TOOL_PREFIX = "arc_";
|
|
304
|
+
var CATALOG_TIMEOUT_MS = 4e3;
|
|
305
|
+
function isArcMcpToolName(name) {
|
|
306
|
+
return name.startsWith(ARC_TOOL_PREFIX);
|
|
307
|
+
}
|
|
308
|
+
function isCatalogEntry(value) {
|
|
309
|
+
if (!value || typeof value !== "object") return false;
|
|
310
|
+
const entry = value;
|
|
311
|
+
const schema = entry.inputSchema;
|
|
312
|
+
return typeof entry.name === "string" && /^[a-z0-9_]+$/.test(entry.name) && typeof entry.description === "string" && !!schema && typeof schema === "object" && schema.type === "object";
|
|
313
|
+
}
|
|
314
|
+
async function fetchGrowthToolCatalog(apiKey, baseUrl) {
|
|
315
|
+
try {
|
|
316
|
+
const origin = resolveArcAppOrigin({ baseUrl, apiKey });
|
|
317
|
+
const response = await arcFetch(origin, apiKey, "/api/growth/tools", {
|
|
318
|
+
signal: AbortSignal.timeout(CATALOG_TIMEOUT_MS)
|
|
319
|
+
});
|
|
320
|
+
if (!response.ok || !Array.isArray(response.body.tools)) return null;
|
|
321
|
+
const entries = response.body.tools.filter(isCatalogEntry);
|
|
322
|
+
return entries.length > 0 ? entries : null;
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function loadArcMcpTools(apiKey, baseUrl) {
|
|
328
|
+
const catalog = await fetchGrowthToolCatalog(apiKey, baseUrl);
|
|
329
|
+
if (!catalog) return ARC_MCP_TOOLS;
|
|
330
|
+
return [
|
|
331
|
+
...META_TOOLS,
|
|
332
|
+
...catalog.map((entry) => ({
|
|
333
|
+
name: `${ARC_TOOL_PREFIX}${entry.name}`,
|
|
334
|
+
description: `${entry.mutates ? RANLA_AGENT_NAME : `${RANLA_AGENT_NAME}, read-only`}: ${entry.description}`,
|
|
335
|
+
inputSchema: entry.inputSchema,
|
|
336
|
+
kind: "growth",
|
|
337
|
+
growthTool: entry.name
|
|
338
|
+
}))
|
|
339
|
+
];
|
|
340
|
+
}
|
|
295
341
|
function optString(args, key) {
|
|
296
342
|
if (args[key] == null) return void 0;
|
|
297
343
|
const value = String(args[key]).trim();
|
|
298
344
|
return value || void 0;
|
|
299
345
|
}
|
|
300
346
|
async function callArcMcpTool(apiKey, name, args, baseUrl) {
|
|
301
|
-
|
|
302
|
-
if (!def) return null;
|
|
347
|
+
if (!isArcMcpToolName(name)) return null;
|
|
303
348
|
const origin = resolveArcAppOrigin({ baseUrl, apiKey });
|
|
304
|
-
if (
|
|
305
|
-
const
|
|
349
|
+
if (!META_TOOLS.some((tool) => tool.name === name)) {
|
|
350
|
+
const growthTool = name.slice(ARC_TOOL_PREFIX.length);
|
|
351
|
+
const response = await arcFetch(origin, apiKey, `/api/growth/tools/${encodeURIComponent(growthTool)}`, {
|
|
306
352
|
method: "POST",
|
|
307
353
|
body: JSON.stringify({ input: args })
|
|
308
354
|
});
|
|
@@ -454,23 +500,46 @@ function optStringArray(args, key) {
|
|
|
454
500
|
if (Array.isArray(value)) return value.map((v) => String(v));
|
|
455
501
|
return void 0;
|
|
456
502
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
"
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
503
|
+
function optRecipients(args, key) {
|
|
504
|
+
const value = args[key];
|
|
505
|
+
if (value == null) return void 0;
|
|
506
|
+
const parts = (Array.isArray(value) ? value.map((entry) => String(entry)) : String(value).split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)).map((entry) => entry.trim()).filter(Boolean);
|
|
507
|
+
if (parts.length === 0) return void 0;
|
|
508
|
+
return parts.length === 1 ? parts[0] : parts;
|
|
509
|
+
}
|
|
510
|
+
function optStringMap(args, key) {
|
|
511
|
+
const value = args[key];
|
|
512
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
513
|
+
const entries = Object.entries(value).filter(([, entry]) => entry != null);
|
|
514
|
+
return entries.length > 0 ? Object.fromEntries(entries.map(([name, entry]) => [name, String(entry)])) : void 0;
|
|
515
|
+
}
|
|
516
|
+
var TEST_WEBHOOK_EVENT_FLAGS = {
|
|
517
|
+
"email.sent": true,
|
|
518
|
+
"email.delivered": true,
|
|
519
|
+
"email.delivery_delayed": true,
|
|
520
|
+
"email.bounced": true,
|
|
521
|
+
"email.complained": true,
|
|
522
|
+
"email.opened": true,
|
|
523
|
+
"email.clicked": true,
|
|
524
|
+
"email.failed": true,
|
|
525
|
+
"email.suppressed": true,
|
|
526
|
+
"email.scheduled": true
|
|
527
|
+
};
|
|
528
|
+
var TEST_WEBHOOK_EVENTS = new Set(Object.keys(TEST_WEBHOOK_EVENT_FLAGS));
|
|
529
|
+
var WEBHOOK_EVENT_FLAGS = {
|
|
530
|
+
...TEST_WEBHOOK_EVENT_FLAGS,
|
|
531
|
+
"email.received": true,
|
|
532
|
+
"contact.unsubscribed": true,
|
|
533
|
+
"automation.started": true,
|
|
534
|
+
"automation.step_completed": true,
|
|
535
|
+
"automation.completed": true,
|
|
536
|
+
"automation.failed": true
|
|
537
|
+
};
|
|
538
|
+
var WEBHOOK_EVENTS = Object.keys(WEBHOOK_EVENT_FLAGS);
|
|
539
|
+
var EMAIL_CATEGORIES = ["transactional", "product", "newsletter"];
|
|
470
540
|
async function callMcpTool(client, name, args, options = {}) {
|
|
471
541
|
try {
|
|
472
|
-
|
|
473
|
-
if (arc) {
|
|
542
|
+
if (isArcMcpToolName(name)) {
|
|
474
543
|
const apiKey = options.apiKey ?? resolveApiKey();
|
|
475
544
|
const result = await callArcMcpTool(apiKey, name, args, options.baseUrl);
|
|
476
545
|
return textResult(result);
|
|
@@ -478,7 +547,7 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
478
547
|
switch (name) {
|
|
479
548
|
case "send_email": {
|
|
480
549
|
const from = optString2(args, "from");
|
|
481
|
-
const to =
|
|
550
|
+
const to = optRecipients(args, "to");
|
|
482
551
|
const subject = optString2(args, "subject");
|
|
483
552
|
const templateId = optString2(args, "template_id") || optString2(args, "template_alias");
|
|
484
553
|
if (!from) return textResult("Missing required argument: from", true);
|
|
@@ -486,14 +555,33 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
486
555
|
if (!templateId && !subject) {
|
|
487
556
|
return textResult("Provide subject, or template_id / template_alias", true);
|
|
488
557
|
}
|
|
558
|
+
const category = optString2(args, "category");
|
|
559
|
+
if (category && !EMAIL_CATEGORIES.includes(category)) {
|
|
560
|
+
return textResult(`Invalid category. Use one of: ${EMAIL_CATEGORIES.join(", ")}`, true);
|
|
561
|
+
}
|
|
489
562
|
const variables = args.variables && typeof args.variables === "object" && !Array.isArray(args.variables) ? args.variables : void 0;
|
|
563
|
+
const cc = optRecipients(args, "cc");
|
|
564
|
+
const bcc = optRecipients(args, "bcc");
|
|
565
|
+
const tags = optStringMap(args, "tags");
|
|
566
|
+
const headers = optStringMap(args, "headers");
|
|
567
|
+
const scheduledAt = optString2(args, "scheduled_at");
|
|
568
|
+
const idempotencyKey = optString2(args, "idempotency_key");
|
|
569
|
+
const unsubscribe = optBoolean(args, "unsubscribe");
|
|
490
570
|
const result = await client.emails.send({
|
|
491
571
|
from,
|
|
492
572
|
to,
|
|
493
573
|
...subject ? { subject } : {},
|
|
494
574
|
html: optString2(args, "html"),
|
|
495
575
|
text: optString2(args, "text"),
|
|
496
|
-
reply_to:
|
|
576
|
+
reply_to: optRecipients(args, "reply_to"),
|
|
577
|
+
...cc ? { cc } : {},
|
|
578
|
+
...bcc ? { bcc } : {},
|
|
579
|
+
...tags ? { tags } : {},
|
|
580
|
+
...headers ? { headers } : {},
|
|
581
|
+
...scheduledAt ? { scheduled_at: scheduledAt } : {},
|
|
582
|
+
...category ? { category } : {},
|
|
583
|
+
...unsubscribe !== void 0 ? { unsubscribe } : {},
|
|
584
|
+
...idempotencyKey ? { idempotencyKey } : {},
|
|
497
585
|
...templateId ? { template: { id: templateId, ...variables ? { variables } : {} } } : {}
|
|
498
586
|
});
|
|
499
587
|
return textResult(result);
|
|
@@ -510,6 +598,30 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
510
598
|
if (!id) return textResult("Missing required argument: id", true);
|
|
511
599
|
return textResult(await client.emails.get(id));
|
|
512
600
|
}
|
|
601
|
+
case "cancel_email": {
|
|
602
|
+
const id = optString2(args, "id");
|
|
603
|
+
if (!id) return textResult("Missing required argument: id", true);
|
|
604
|
+
return textResult(await client.emails.cancel(id));
|
|
605
|
+
}
|
|
606
|
+
case "list_received_emails": {
|
|
607
|
+
return textResult(
|
|
608
|
+
await client.receivedEmails.list({
|
|
609
|
+
limit: optNumber(args, "limit"),
|
|
610
|
+
cursor: optString2(args, "cursor"),
|
|
611
|
+
domain: optString2(args, "domain")
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
case "get_received_email": {
|
|
616
|
+
const id = optString2(args, "id");
|
|
617
|
+
if (!id) return textResult("Missing required argument: id", true);
|
|
618
|
+
return textResult(await client.receivedEmails.get(id));
|
|
619
|
+
}
|
|
620
|
+
case "get_domain": {
|
|
621
|
+
const domain = optString2(args, "domain");
|
|
622
|
+
if (!domain) return textResult("Missing required argument: domain", true);
|
|
623
|
+
return textResult(await client.domains.get(domain));
|
|
624
|
+
}
|
|
513
625
|
case "apply_domain_dns": {
|
|
514
626
|
const domain = String(args.domain ?? "").trim();
|
|
515
627
|
if (!domain) {
|
|
@@ -536,9 +648,11 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
536
648
|
return textResult(result);
|
|
537
649
|
}
|
|
538
650
|
case "list_domains": {
|
|
651
|
+
const inbound = optBoolean(args, "inbound_enabled");
|
|
539
652
|
const result = await client.domains.list({
|
|
540
653
|
limit: optNumber(args, "limit"),
|
|
541
|
-
cursor: optString2(args, "cursor")
|
|
654
|
+
cursor: optString2(args, "cursor"),
|
|
655
|
+
...inbound !== void 0 ? { inbound_enabled: inbound } : {}
|
|
542
656
|
});
|
|
543
657
|
return textResult(result);
|
|
544
658
|
}
|
|
@@ -565,6 +679,10 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
565
679
|
const url = optString2(args, "url");
|
|
566
680
|
if (!url) return textResult("Missing required argument: url", true);
|
|
567
681
|
const events = optStringArray(args, "events");
|
|
682
|
+
const unknown = events?.filter((event) => !WEBHOOK_EVENTS.includes(event)) ?? [];
|
|
683
|
+
if (unknown.length > 0) {
|
|
684
|
+
return textResult(`Unknown event types: ${unknown.join(", ")}. Use: ${WEBHOOK_EVENTS.join(", ")}`, true);
|
|
685
|
+
}
|
|
568
686
|
return textResult(
|
|
569
687
|
await client.webhooks.create({
|
|
570
688
|
url,
|
|
@@ -622,12 +740,12 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
622
740
|
const name2 = optString2(args, "name");
|
|
623
741
|
const subjectArg = optString2(args, "subject");
|
|
624
742
|
if (!name2) return textResult("Missing required argument: name", true);
|
|
625
|
-
if (!subjectArg) return textResult("Missing required argument: subject", true);
|
|
626
743
|
const format = optString2(args, "format");
|
|
627
744
|
return textResult(
|
|
628
745
|
await client.templates.create({
|
|
629
746
|
name: name2,
|
|
630
|
-
|
|
747
|
+
// Optional: a layout-only template has no default subject.
|
|
748
|
+
...subjectArg ? { subject: subjectArg } : {},
|
|
631
749
|
...format === "blocks" || format === "html" ? { format } : {},
|
|
632
750
|
html: optString2(args, "html"),
|
|
633
751
|
text: optString2(args, "text"),
|
|
@@ -638,13 +756,14 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
638
756
|
case "publish_template": {
|
|
639
757
|
const idOrAlias = optString2(args, "id") ?? optString2(args, "alias");
|
|
640
758
|
if (!idOrAlias) return textResult("Missing required argument: id or alias", true);
|
|
641
|
-
|
|
759
|
+
const label = optString2(args, "label");
|
|
760
|
+
return textResult(await client.templates.publish(idOrAlias, label ? { label } : {}));
|
|
642
761
|
}
|
|
643
762
|
case "send_test_webhook_event": {
|
|
644
763
|
const event = optString2(args, "event");
|
|
645
|
-
if (!event || !
|
|
764
|
+
if (!event || !TEST_WEBHOOK_EVENTS.has(event)) {
|
|
646
765
|
return textResult(
|
|
647
|
-
`Missing or invalid event. Use one of: ${[...
|
|
766
|
+
`Missing or invalid event. Use one of: ${[...TEST_WEBHOOK_EVENTS].join(", ")}`,
|
|
648
767
|
true
|
|
649
768
|
);
|
|
650
769
|
}
|
|
@@ -652,6 +771,7 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
652
771
|
await client.emails.testWebhook({
|
|
653
772
|
event,
|
|
654
773
|
email_id: optString2(args, "email_id"),
|
|
774
|
+
webhook_id: optString2(args, "webhook_id"),
|
|
655
775
|
deliver: optBoolean(args, "deliver")
|
|
656
776
|
})
|
|
657
777
|
);
|
|
@@ -673,26 +793,56 @@ var paginationProps = {
|
|
|
673
793
|
limit: { type: "number", description: "Page size (1\u2013100)" },
|
|
674
794
|
cursor: { type: "string", description: "Opaque cursor for the next page" }
|
|
675
795
|
};
|
|
796
|
+
var recipientProp = (what) => ({
|
|
797
|
+
description: `${what}: one address, several separated by commas, or an array of addresses`,
|
|
798
|
+
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }]
|
|
799
|
+
});
|
|
676
800
|
var MCP_TOOLS = [
|
|
677
801
|
{
|
|
678
802
|
name: "send_email",
|
|
679
|
-
description: "Send
|
|
803
|
+
description: "Send an email. Provide subject + html/text, or a published template_id / template_alias with optional variables. Set scheduled_at to send later (cancel with cancel_email).",
|
|
680
804
|
inputSchema: {
|
|
681
805
|
type: "object",
|
|
682
806
|
properties: {
|
|
683
|
-
from: {
|
|
684
|
-
|
|
807
|
+
from: {
|
|
808
|
+
type: "string",
|
|
809
|
+
description: "Sender address on a verified domain (or `Name <address>`). Before a domain is verified, use the sandbox sender noreply@mail.ranla.ai and send only to the account email."
|
|
810
|
+
},
|
|
811
|
+
to: recipientProp("Recipients"),
|
|
812
|
+
cc: recipientProp("Cc recipients"),
|
|
813
|
+
bcc: recipientProp("Bcc recipients"),
|
|
814
|
+
reply_to: recipientProp("Reply-To"),
|
|
685
815
|
subject: { type: "string", description: "Optional when using a published template" },
|
|
686
816
|
html: { type: "string" },
|
|
687
817
|
text: { type: "string" },
|
|
688
|
-
|
|
689
|
-
template_id: { type: "string", description: "Published template UUID" },
|
|
818
|
+
template_id: { type: "string", description: "Published template id" },
|
|
690
819
|
template_alias: { type: "string", description: "Published template alias" },
|
|
691
820
|
variables: {
|
|
692
821
|
type: "object",
|
|
693
822
|
description: "Template variables when using template_id or template_alias",
|
|
694
823
|
additionalProperties: true
|
|
695
|
-
}
|
|
824
|
+
},
|
|
825
|
+
scheduled_at: { type: "string", description: "ISO 8601 time to send, e.g. 2026-10-01T09:00:00Z" },
|
|
826
|
+
category: {
|
|
827
|
+
type: "string",
|
|
828
|
+
enum: [...EMAIL_CATEGORIES],
|
|
829
|
+
description: "Default transactional. product and newsletter add an unsubscribe link and skip recipients who opted out of that category."
|
|
830
|
+
},
|
|
831
|
+
tags: {
|
|
832
|
+
type: "object",
|
|
833
|
+
description: "Tags as name \u2192 value, for filtering and webhooks",
|
|
834
|
+
additionalProperties: { type: "string" }
|
|
835
|
+
},
|
|
836
|
+
headers: {
|
|
837
|
+
type: "object",
|
|
838
|
+
description: "Extra email headers as name \u2192 value",
|
|
839
|
+
additionalProperties: { type: "string" }
|
|
840
|
+
},
|
|
841
|
+
idempotency_key: {
|
|
842
|
+
type: "string",
|
|
843
|
+
description: "Retry-safe key: the same key within 24 hours returns the first send instead of sending twice"
|
|
844
|
+
},
|
|
845
|
+
unsubscribe: { type: "boolean", description: "Add a managed unsubscribe link to this send" }
|
|
696
846
|
},
|
|
697
847
|
required: ["from", "to"]
|
|
698
848
|
}
|
|
@@ -716,12 +866,59 @@ var MCP_TOOLS = [
|
|
|
716
866
|
required: ["id"]
|
|
717
867
|
}
|
|
718
868
|
},
|
|
869
|
+
{
|
|
870
|
+
name: "cancel_email",
|
|
871
|
+
description: "Cancel a scheduled email before it sends.",
|
|
872
|
+
inputSchema: {
|
|
873
|
+
type: "object",
|
|
874
|
+
properties: {
|
|
875
|
+
id: { type: "string", description: "Email id (msg_\u2026) of a scheduled send" }
|
|
876
|
+
},
|
|
877
|
+
required: ["id"]
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
name: "list_received_emails",
|
|
882
|
+
description: "List inbound emails received on domains with inbound enabled.",
|
|
883
|
+
inputSchema: {
|
|
884
|
+
type: "object",
|
|
885
|
+
properties: {
|
|
886
|
+
...paginationProps,
|
|
887
|
+
domain: { type: "string", description: "Only messages received on this domain" }
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
name: "get_received_email",
|
|
893
|
+
description: "Get one inbound email, including its headers and body.",
|
|
894
|
+
inputSchema: {
|
|
895
|
+
type: "object",
|
|
896
|
+
properties: {
|
|
897
|
+
id: { type: "string", description: "Received email id" }
|
|
898
|
+
},
|
|
899
|
+
required: ["id"]
|
|
900
|
+
}
|
|
901
|
+
},
|
|
719
902
|
{
|
|
720
903
|
name: "list_domains",
|
|
721
904
|
description: "List sending domains for this account.",
|
|
722
905
|
inputSchema: {
|
|
723
906
|
type: "object",
|
|
724
|
-
properties: {
|
|
907
|
+
properties: {
|
|
908
|
+
...paginationProps,
|
|
909
|
+
inbound_enabled: { type: "boolean", description: "Only domains with inbound receiving on (or off)" }
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
name: "get_domain",
|
|
915
|
+
description: "Get one sending domain by id or name, with its DNS records and verification status.",
|
|
916
|
+
inputSchema: {
|
|
917
|
+
type: "object",
|
|
918
|
+
properties: {
|
|
919
|
+
domain: { type: "string", description: "Domain id or name (example.com)" }
|
|
920
|
+
},
|
|
921
|
+
required: ["domain"]
|
|
725
922
|
}
|
|
726
923
|
},
|
|
727
924
|
{
|
|
@@ -738,7 +935,7 @@ var MCP_TOOLS = [
|
|
|
738
935
|
},
|
|
739
936
|
{
|
|
740
937
|
name: "apply_domain_dns",
|
|
741
|
-
description: "
|
|
938
|
+
description: "Create the sending domain's DNS records at your DNS provider (Cloudflare, GoDaddy, or Vercel), using the connection stored in the dashboard. GoDaddy also accepts one-time credentials on the request.",
|
|
742
939
|
inputSchema: {
|
|
743
940
|
type: "object",
|
|
744
941
|
properties: {
|
|
@@ -780,8 +977,8 @@ var MCP_TOOLS = [
|
|
|
780
977
|
url: { type: "string", description: "HTTPS URL that receives events" },
|
|
781
978
|
events: {
|
|
782
979
|
type: "array",
|
|
783
|
-
items: { type: "string" },
|
|
784
|
-
description: "
|
|
980
|
+
items: { type: "string", enum: WEBHOOK_EVENTS },
|
|
981
|
+
description: "Event types to deliver (all of them when omitted)"
|
|
785
982
|
}
|
|
786
983
|
},
|
|
787
984
|
required: ["url"]
|
|
@@ -793,7 +990,7 @@ var MCP_TOOLS = [
|
|
|
793
990
|
inputSchema: {
|
|
794
991
|
type: "object",
|
|
795
992
|
properties: {
|
|
796
|
-
id: { type: "string", description: "Webhook id
|
|
993
|
+
id: { type: "string", description: "Webhook id" }
|
|
797
994
|
},
|
|
798
995
|
required: ["id"]
|
|
799
996
|
}
|
|
@@ -849,14 +1046,14 @@ var MCP_TOOLS = [
|
|
|
849
1046
|
inputSchema: {
|
|
850
1047
|
type: "object",
|
|
851
1048
|
properties: {
|
|
852
|
-
id: { type: "string", description: "Template id
|
|
1049
|
+
id: { type: "string", description: "Template id" },
|
|
853
1050
|
alias: { type: "string", description: "Published alias (alternative to id)" }
|
|
854
1051
|
}
|
|
855
1052
|
}
|
|
856
1053
|
},
|
|
857
1054
|
{
|
|
858
1055
|
name: "create_template",
|
|
859
|
-
description: "Create a draft email template (html or blocks format).",
|
|
1056
|
+
description: "Create a draft email template (html or blocks format). Publish it before sending with it.",
|
|
860
1057
|
inputSchema: {
|
|
861
1058
|
type: "object",
|
|
862
1059
|
properties: {
|
|
@@ -867,7 +1064,7 @@ var MCP_TOOLS = [
|
|
|
867
1064
|
text: { type: "string" },
|
|
868
1065
|
alias: { type: "string" }
|
|
869
1066
|
},
|
|
870
|
-
required: ["name"
|
|
1067
|
+
required: ["name"]
|
|
871
1068
|
}
|
|
872
1069
|
},
|
|
873
1070
|
{
|
|
@@ -877,7 +1074,8 @@ var MCP_TOOLS = [
|
|
|
877
1074
|
type: "object",
|
|
878
1075
|
properties: {
|
|
879
1076
|
id: { type: "string" },
|
|
880
|
-
alias: { type: "string" }
|
|
1077
|
+
alias: { type: "string" },
|
|
1078
|
+
label: { type: "string", description: "Optional name for this version in the template history" }
|
|
881
1079
|
}
|
|
882
1080
|
}
|
|
883
1081
|
},
|
|
@@ -889,10 +1087,11 @@ var MCP_TOOLS = [
|
|
|
889
1087
|
properties: {
|
|
890
1088
|
event: {
|
|
891
1089
|
type: "string",
|
|
892
|
-
enum: [...
|
|
893
|
-
description: "
|
|
1090
|
+
enum: [...TEST_WEBHOOK_EVENTS],
|
|
1091
|
+
description: "Email event to simulate (email.received cannot be simulated \u2014 send a real inbound message)"
|
|
894
1092
|
},
|
|
895
1093
|
email_id: { type: "string", description: "Optional existing email id (msg_\u2026)" },
|
|
1094
|
+
webhook_id: { type: "string", description: "Deliver to this one webhook endpoint only" },
|
|
896
1095
|
deliver: {
|
|
897
1096
|
type: "boolean",
|
|
898
1097
|
description: "When true (default), enqueue delivery to account webhooks"
|
|
@@ -913,26 +1112,30 @@ var MCP_TOOLS = [
|
|
|
913
1112
|
}
|
|
914
1113
|
];
|
|
915
1114
|
var MCP_TOOLS_WITH_ARC = [...MCP_TOOLS, ...ARC_MCP_TOOLS];
|
|
1115
|
+
function resolveApiUrl(env = process.env) {
|
|
1116
|
+
return env.SUPERSENDTX_API_URL?.trim() || env.RANLA_API_URL?.trim() || void 0;
|
|
1117
|
+
}
|
|
916
1118
|
function resolveApiKey() {
|
|
917
|
-
const key = process.env.SUPERSENDTX_API_KEY?.trim();
|
|
1119
|
+
const key = process.env.SUPERSENDTX_API_KEY?.trim() || process.env.RANLA_API_KEY?.trim();
|
|
918
1120
|
if (!key) {
|
|
919
|
-
throw new Error("SUPERSENDTX_API_KEY
|
|
1121
|
+
throw new Error("Set RANLA_API_KEY (or SUPERSENDTX_API_KEY) to an API key");
|
|
920
1122
|
}
|
|
921
1123
|
if (!key.startsWith("stx_") && !key.startsWith("rnl_")) {
|
|
922
|
-
throw new Error("
|
|
1124
|
+
throw new Error("The API key must start with rnl_ or stx_ (RANLA_API_KEY / SUPERSENDTX_API_KEY)");
|
|
923
1125
|
}
|
|
924
1126
|
return key;
|
|
925
1127
|
}
|
|
926
1128
|
|
|
927
1129
|
// src/server.ts
|
|
1130
|
+
var MCP_SERVER_VERSION = version;
|
|
928
1131
|
function createMcpServer(apiKey, baseUrl) {
|
|
929
1132
|
const client = new SuperSendTX(apiKey, baseUrl ? { baseUrl } : {});
|
|
930
1133
|
const server = new Server(
|
|
931
|
-
{ name: "supersendtx-mcp", version:
|
|
1134
|
+
{ name: "supersendtx-mcp", version: MCP_SERVER_VERSION },
|
|
932
1135
|
{ capabilities: { tools: {} } }
|
|
933
1136
|
);
|
|
934
1137
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
935
|
-
tools:
|
|
1138
|
+
tools: [...MCP_TOOLS, ...await loadArcMcpTools(apiKey, baseUrl)].map((tool) => ({
|
|
936
1139
|
name: tool.name,
|
|
937
1140
|
description: tool.description,
|
|
938
1141
|
inputSchema: tool.inputSchema
|
|
@@ -949,8 +1152,7 @@ function createMcpServer(apiKey, baseUrl) {
|
|
|
949
1152
|
}
|
|
950
1153
|
async function runStdioServer() {
|
|
951
1154
|
const apiKey = resolveApiKey();
|
|
952
|
-
const
|
|
953
|
-
const server = createMcpServer(apiKey, baseUrl || void 0);
|
|
1155
|
+
const server = createMcpServer(apiKey, resolveApiUrl());
|
|
954
1156
|
const transport = new StdioServerTransport();
|
|
955
1157
|
await server.connect(transport);
|
|
956
1158
|
}
|
|
@@ -1095,7 +1297,7 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
1095
1297
|
async function runHttpServer(options) {
|
|
1096
1298
|
const host = options.host ?? "127.0.0.1";
|
|
1097
1299
|
const port = options.port;
|
|
1098
|
-
const baseUrl =
|
|
1300
|
+
const baseUrl = resolveApiUrl();
|
|
1099
1301
|
const httpServer = createServer((req, res) => {
|
|
1100
1302
|
void handleHttpRequest(req, res, { baseUrl }).catch((error) => {
|
|
1101
1303
|
if (!res.headersSent) {
|
|
@@ -1111,8 +1313,8 @@ async function runHttpServer(options) {
|
|
|
1111
1313
|
httpServer.once("error", reject);
|
|
1112
1314
|
httpServer.listen(port, host, () => resolve());
|
|
1113
1315
|
});
|
|
1114
|
-
console.error(`
|
|
1115
|
-
console.error("Authenticate with Bearer
|
|
1316
|
+
console.error(`MCP HTTP server listening on http://${host}:${port}/mcp`);
|
|
1317
|
+
console.error("Authenticate with Bearer rnl_\u2026 / stx_\u2026 or an MCP OAuth access token");
|
|
1116
1318
|
}
|
|
1117
1319
|
|
|
1118
1320
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supersendtx-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.32",
|
|
4
4
|
"description": "SuperSend TX MCP server — mail tools plus Ranla agent-first tools for Cursor and Claude",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
43
|
-
"supersendtx": "0.15.
|
|
43
|
+
"supersendtx": "0.15.6"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"tsup": "^8.5.0",
|