supersendtx-mcp 0.6.30 → 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 +264 -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,14 +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.
|
|
88
92
|
"seo_plan",
|
|
89
|
-
"ask_choice",
|
|
90
93
|
"get_business_brief",
|
|
91
94
|
"get_brand",
|
|
92
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",
|
|
93
99
|
"get_setup_status",
|
|
94
100
|
"get_growth_health",
|
|
95
101
|
"get_revenue_health",
|
|
@@ -186,7 +192,7 @@ var RANLA_AGENT_NAME = "Ranla";
|
|
|
186
192
|
var TALK_READ_TOOLS = ARC_MCP_TALK_GROWTH_TOOL_NAMES.map((growthTool) => ({
|
|
187
193
|
name: `arc_${growthTool}`,
|
|
188
194
|
description: `Read-only Ranla tool: ${growthTool.replace(/_/g, " ")}.`,
|
|
189
|
-
inputSchema: openObjectSchema("
|
|
195
|
+
inputSchema: openObjectSchema("Pass the tool arguments as fields."),
|
|
190
196
|
kind: "growth",
|
|
191
197
|
growthTool
|
|
192
198
|
}));
|
|
@@ -227,7 +233,7 @@ var META_TOOLS = [
|
|
|
227
233
|
},
|
|
228
234
|
{
|
|
229
235
|
name: "arc_message",
|
|
230
|
-
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.",
|
|
231
237
|
inputSchema: {
|
|
232
238
|
type: "object",
|
|
233
239
|
properties: {
|
|
@@ -246,7 +252,7 @@ var META_TOOLS = [
|
|
|
246
252
|
},
|
|
247
253
|
{
|
|
248
254
|
name: "arc_approve",
|
|
249
|
-
description: "Approve a pending Ranla
|
|
255
|
+
description: "Approve a pending Ranla approval and resume the run.",
|
|
250
256
|
inputSchema: {
|
|
251
257
|
type: "object",
|
|
252
258
|
properties: {
|
|
@@ -258,7 +264,7 @@ var META_TOOLS = [
|
|
|
258
264
|
},
|
|
259
265
|
{
|
|
260
266
|
name: "arc_reject",
|
|
261
|
-
description: "Reject a pending Ranla
|
|
267
|
+
description: "Reject a pending Ranla approval and resume the run.",
|
|
262
268
|
inputSchema: {
|
|
263
269
|
type: "object",
|
|
264
270
|
properties: {
|
|
@@ -275,7 +281,7 @@ function resolveArcAppOrigin(options = {}) {
|
|
|
275
281
|
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
|
276
282
|
const key = options.apiKey ?? process.env.SUPERSENDTX_API_KEY ?? process.env.RANLA_API_KEY ?? "";
|
|
277
283
|
if (key.startsWith("rnl_")) return DEFAULT_RANLA_APP_ORIGIN;
|
|
278
|
-
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(/\/$/, "");
|
|
279
285
|
if (apiUrl.includes("api.ranla.ai")) return DEFAULT_RANLA_APP_ORIGIN;
|
|
280
286
|
if (apiUrl.includes("app.ranla.ai")) return apiUrl;
|
|
281
287
|
if (apiUrl.includes("app.supersendtx.com")) return apiUrl;
|
|
@@ -294,17 +300,55 @@ async function arcFetch(origin, apiKey, path, init = {}) {
|
|
|
294
300
|
const body = await response.json().catch(() => ({}));
|
|
295
301
|
return { ok: response.ok, status: response.status, body };
|
|
296
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
|
+
}
|
|
297
341
|
function optString(args, key) {
|
|
298
342
|
if (args[key] == null) return void 0;
|
|
299
343
|
const value = String(args[key]).trim();
|
|
300
344
|
return value || void 0;
|
|
301
345
|
}
|
|
302
346
|
async function callArcMcpTool(apiKey, name, args, baseUrl) {
|
|
303
|
-
|
|
304
|
-
if (!def) return null;
|
|
347
|
+
if (!isArcMcpToolName(name)) return null;
|
|
305
348
|
const origin = resolveArcAppOrigin({ baseUrl, apiKey });
|
|
306
|
-
if (
|
|
307
|
-
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)}`, {
|
|
308
352
|
method: "POST",
|
|
309
353
|
body: JSON.stringify({ input: args })
|
|
310
354
|
});
|
|
@@ -456,23 +500,46 @@ function optStringArray(args, key) {
|
|
|
456
500
|
if (Array.isArray(value)) return value.map((v) => String(v));
|
|
457
501
|
return void 0;
|
|
458
502
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
"
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
"
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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"];
|
|
472
540
|
async function callMcpTool(client, name, args, options = {}) {
|
|
473
541
|
try {
|
|
474
|
-
|
|
475
|
-
if (arc) {
|
|
542
|
+
if (isArcMcpToolName(name)) {
|
|
476
543
|
const apiKey = options.apiKey ?? resolveApiKey();
|
|
477
544
|
const result = await callArcMcpTool(apiKey, name, args, options.baseUrl);
|
|
478
545
|
return textResult(result);
|
|
@@ -480,7 +547,7 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
480
547
|
switch (name) {
|
|
481
548
|
case "send_email": {
|
|
482
549
|
const from = optString2(args, "from");
|
|
483
|
-
const to =
|
|
550
|
+
const to = optRecipients(args, "to");
|
|
484
551
|
const subject = optString2(args, "subject");
|
|
485
552
|
const templateId = optString2(args, "template_id") || optString2(args, "template_alias");
|
|
486
553
|
if (!from) return textResult("Missing required argument: from", true);
|
|
@@ -488,14 +555,33 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
488
555
|
if (!templateId && !subject) {
|
|
489
556
|
return textResult("Provide subject, or template_id / template_alias", true);
|
|
490
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
|
+
}
|
|
491
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");
|
|
492
570
|
const result = await client.emails.send({
|
|
493
571
|
from,
|
|
494
572
|
to,
|
|
495
573
|
...subject ? { subject } : {},
|
|
496
574
|
html: optString2(args, "html"),
|
|
497
575
|
text: optString2(args, "text"),
|
|
498
|
-
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 } : {},
|
|
499
585
|
...templateId ? { template: { id: templateId, ...variables ? { variables } : {} } } : {}
|
|
500
586
|
});
|
|
501
587
|
return textResult(result);
|
|
@@ -512,6 +598,30 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
512
598
|
if (!id) return textResult("Missing required argument: id", true);
|
|
513
599
|
return textResult(await client.emails.get(id));
|
|
514
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
|
+
}
|
|
515
625
|
case "apply_domain_dns": {
|
|
516
626
|
const domain = String(args.domain ?? "").trim();
|
|
517
627
|
if (!domain) {
|
|
@@ -538,9 +648,11 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
538
648
|
return textResult(result);
|
|
539
649
|
}
|
|
540
650
|
case "list_domains": {
|
|
651
|
+
const inbound = optBoolean(args, "inbound_enabled");
|
|
541
652
|
const result = await client.domains.list({
|
|
542
653
|
limit: optNumber(args, "limit"),
|
|
543
|
-
cursor: optString2(args, "cursor")
|
|
654
|
+
cursor: optString2(args, "cursor"),
|
|
655
|
+
...inbound !== void 0 ? { inbound_enabled: inbound } : {}
|
|
544
656
|
});
|
|
545
657
|
return textResult(result);
|
|
546
658
|
}
|
|
@@ -567,6 +679,10 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
567
679
|
const url = optString2(args, "url");
|
|
568
680
|
if (!url) return textResult("Missing required argument: url", true);
|
|
569
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
|
+
}
|
|
570
686
|
return textResult(
|
|
571
687
|
await client.webhooks.create({
|
|
572
688
|
url,
|
|
@@ -624,12 +740,12 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
624
740
|
const name2 = optString2(args, "name");
|
|
625
741
|
const subjectArg = optString2(args, "subject");
|
|
626
742
|
if (!name2) return textResult("Missing required argument: name", true);
|
|
627
|
-
if (!subjectArg) return textResult("Missing required argument: subject", true);
|
|
628
743
|
const format = optString2(args, "format");
|
|
629
744
|
return textResult(
|
|
630
745
|
await client.templates.create({
|
|
631
746
|
name: name2,
|
|
632
|
-
|
|
747
|
+
// Optional: a layout-only template has no default subject.
|
|
748
|
+
...subjectArg ? { subject: subjectArg } : {},
|
|
633
749
|
...format === "blocks" || format === "html" ? { format } : {},
|
|
634
750
|
html: optString2(args, "html"),
|
|
635
751
|
text: optString2(args, "text"),
|
|
@@ -640,13 +756,14 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
640
756
|
case "publish_template": {
|
|
641
757
|
const idOrAlias = optString2(args, "id") ?? optString2(args, "alias");
|
|
642
758
|
if (!idOrAlias) return textResult("Missing required argument: id or alias", true);
|
|
643
|
-
|
|
759
|
+
const label = optString2(args, "label");
|
|
760
|
+
return textResult(await client.templates.publish(idOrAlias, label ? { label } : {}));
|
|
644
761
|
}
|
|
645
762
|
case "send_test_webhook_event": {
|
|
646
763
|
const event = optString2(args, "event");
|
|
647
|
-
if (!event || !
|
|
764
|
+
if (!event || !TEST_WEBHOOK_EVENTS.has(event)) {
|
|
648
765
|
return textResult(
|
|
649
|
-
`Missing or invalid event. Use one of: ${[...
|
|
766
|
+
`Missing or invalid event. Use one of: ${[...TEST_WEBHOOK_EVENTS].join(", ")}`,
|
|
650
767
|
true
|
|
651
768
|
);
|
|
652
769
|
}
|
|
@@ -654,6 +771,7 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
654
771
|
await client.emails.testWebhook({
|
|
655
772
|
event,
|
|
656
773
|
email_id: optString2(args, "email_id"),
|
|
774
|
+
webhook_id: optString2(args, "webhook_id"),
|
|
657
775
|
deliver: optBoolean(args, "deliver")
|
|
658
776
|
})
|
|
659
777
|
);
|
|
@@ -675,26 +793,56 @@ var paginationProps = {
|
|
|
675
793
|
limit: { type: "number", description: "Page size (1\u2013100)" },
|
|
676
794
|
cursor: { type: "string", description: "Opaque cursor for the next page" }
|
|
677
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
|
+
});
|
|
678
800
|
var MCP_TOOLS = [
|
|
679
801
|
{
|
|
680
802
|
name: "send_email",
|
|
681
|
-
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).",
|
|
682
804
|
inputSchema: {
|
|
683
805
|
type: "object",
|
|
684
806
|
properties: {
|
|
685
|
-
from: {
|
|
686
|
-
|
|
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"),
|
|
687
815
|
subject: { type: "string", description: "Optional when using a published template" },
|
|
688
816
|
html: { type: "string" },
|
|
689
817
|
text: { type: "string" },
|
|
690
|
-
|
|
691
|
-
template_id: { type: "string", description: "Published template UUID" },
|
|
818
|
+
template_id: { type: "string", description: "Published template id" },
|
|
692
819
|
template_alias: { type: "string", description: "Published template alias" },
|
|
693
820
|
variables: {
|
|
694
821
|
type: "object",
|
|
695
822
|
description: "Template variables when using template_id or template_alias",
|
|
696
823
|
additionalProperties: true
|
|
697
|
-
}
|
|
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" }
|
|
698
846
|
},
|
|
699
847
|
required: ["from", "to"]
|
|
700
848
|
}
|
|
@@ -718,12 +866,59 @@ var MCP_TOOLS = [
|
|
|
718
866
|
required: ["id"]
|
|
719
867
|
}
|
|
720
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
|
+
},
|
|
721
902
|
{
|
|
722
903
|
name: "list_domains",
|
|
723
904
|
description: "List sending domains for this account.",
|
|
724
905
|
inputSchema: {
|
|
725
906
|
type: "object",
|
|
726
|
-
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"]
|
|
727
922
|
}
|
|
728
923
|
},
|
|
729
924
|
{
|
|
@@ -740,7 +935,7 @@ var MCP_TOOLS = [
|
|
|
740
935
|
},
|
|
741
936
|
{
|
|
742
937
|
name: "apply_domain_dns",
|
|
743
|
-
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.",
|
|
744
939
|
inputSchema: {
|
|
745
940
|
type: "object",
|
|
746
941
|
properties: {
|
|
@@ -782,8 +977,8 @@ var MCP_TOOLS = [
|
|
|
782
977
|
url: { type: "string", description: "HTTPS URL that receives events" },
|
|
783
978
|
events: {
|
|
784
979
|
type: "array",
|
|
785
|
-
items: { type: "string" },
|
|
786
|
-
description: "
|
|
980
|
+
items: { type: "string", enum: WEBHOOK_EVENTS },
|
|
981
|
+
description: "Event types to deliver (all of them when omitted)"
|
|
787
982
|
}
|
|
788
983
|
},
|
|
789
984
|
required: ["url"]
|
|
@@ -795,7 +990,7 @@ var MCP_TOOLS = [
|
|
|
795
990
|
inputSchema: {
|
|
796
991
|
type: "object",
|
|
797
992
|
properties: {
|
|
798
|
-
id: { type: "string", description: "Webhook id
|
|
993
|
+
id: { type: "string", description: "Webhook id" }
|
|
799
994
|
},
|
|
800
995
|
required: ["id"]
|
|
801
996
|
}
|
|
@@ -851,14 +1046,14 @@ var MCP_TOOLS = [
|
|
|
851
1046
|
inputSchema: {
|
|
852
1047
|
type: "object",
|
|
853
1048
|
properties: {
|
|
854
|
-
id: { type: "string", description: "Template id
|
|
1049
|
+
id: { type: "string", description: "Template id" },
|
|
855
1050
|
alias: { type: "string", description: "Published alias (alternative to id)" }
|
|
856
1051
|
}
|
|
857
1052
|
}
|
|
858
1053
|
},
|
|
859
1054
|
{
|
|
860
1055
|
name: "create_template",
|
|
861
|
-
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.",
|
|
862
1057
|
inputSchema: {
|
|
863
1058
|
type: "object",
|
|
864
1059
|
properties: {
|
|
@@ -869,7 +1064,7 @@ var MCP_TOOLS = [
|
|
|
869
1064
|
text: { type: "string" },
|
|
870
1065
|
alias: { type: "string" }
|
|
871
1066
|
},
|
|
872
|
-
required: ["name"
|
|
1067
|
+
required: ["name"]
|
|
873
1068
|
}
|
|
874
1069
|
},
|
|
875
1070
|
{
|
|
@@ -879,7 +1074,8 @@ var MCP_TOOLS = [
|
|
|
879
1074
|
type: "object",
|
|
880
1075
|
properties: {
|
|
881
1076
|
id: { type: "string" },
|
|
882
|
-
alias: { type: "string" }
|
|
1077
|
+
alias: { type: "string" },
|
|
1078
|
+
label: { type: "string", description: "Optional name for this version in the template history" }
|
|
883
1079
|
}
|
|
884
1080
|
}
|
|
885
1081
|
},
|
|
@@ -891,10 +1087,11 @@ var MCP_TOOLS = [
|
|
|
891
1087
|
properties: {
|
|
892
1088
|
event: {
|
|
893
1089
|
type: "string",
|
|
894
|
-
enum: [...
|
|
895
|
-
description: "
|
|
1090
|
+
enum: [...TEST_WEBHOOK_EVENTS],
|
|
1091
|
+
description: "Email event to simulate (email.received cannot be simulated \u2014 send a real inbound message)"
|
|
896
1092
|
},
|
|
897
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" },
|
|
898
1095
|
deliver: {
|
|
899
1096
|
type: "boolean",
|
|
900
1097
|
description: "When true (default), enqueue delivery to account webhooks"
|
|
@@ -915,26 +1112,30 @@ var MCP_TOOLS = [
|
|
|
915
1112
|
}
|
|
916
1113
|
];
|
|
917
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
|
+
}
|
|
918
1118
|
function resolveApiKey() {
|
|
919
|
-
const key = process.env.SUPERSENDTX_API_KEY?.trim();
|
|
1119
|
+
const key = process.env.SUPERSENDTX_API_KEY?.trim() || process.env.RANLA_API_KEY?.trim();
|
|
920
1120
|
if (!key) {
|
|
921
|
-
throw new Error("SUPERSENDTX_API_KEY
|
|
1121
|
+
throw new Error("Set RANLA_API_KEY (or SUPERSENDTX_API_KEY) to an API key");
|
|
922
1122
|
}
|
|
923
1123
|
if (!key.startsWith("stx_") && !key.startsWith("rnl_")) {
|
|
924
|
-
throw new Error("
|
|
1124
|
+
throw new Error("The API key must start with rnl_ or stx_ (RANLA_API_KEY / SUPERSENDTX_API_KEY)");
|
|
925
1125
|
}
|
|
926
1126
|
return key;
|
|
927
1127
|
}
|
|
928
1128
|
|
|
929
1129
|
// src/server.ts
|
|
1130
|
+
var MCP_SERVER_VERSION = version;
|
|
930
1131
|
function createMcpServer(apiKey, baseUrl) {
|
|
931
1132
|
const client = new SuperSendTX(apiKey, baseUrl ? { baseUrl } : {});
|
|
932
1133
|
const server = new Server(
|
|
933
|
-
{ name: "supersendtx-mcp", version:
|
|
1134
|
+
{ name: "supersendtx-mcp", version: MCP_SERVER_VERSION },
|
|
934
1135
|
{ capabilities: { tools: {} } }
|
|
935
1136
|
);
|
|
936
1137
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
937
|
-
tools:
|
|
1138
|
+
tools: [...MCP_TOOLS, ...await loadArcMcpTools(apiKey, baseUrl)].map((tool) => ({
|
|
938
1139
|
name: tool.name,
|
|
939
1140
|
description: tool.description,
|
|
940
1141
|
inputSchema: tool.inputSchema
|
|
@@ -951,8 +1152,7 @@ function createMcpServer(apiKey, baseUrl) {
|
|
|
951
1152
|
}
|
|
952
1153
|
async function runStdioServer() {
|
|
953
1154
|
const apiKey = resolveApiKey();
|
|
954
|
-
const
|
|
955
|
-
const server = createMcpServer(apiKey, baseUrl || void 0);
|
|
1155
|
+
const server = createMcpServer(apiKey, resolveApiUrl());
|
|
956
1156
|
const transport = new StdioServerTransport();
|
|
957
1157
|
await server.connect(transport);
|
|
958
1158
|
}
|
|
@@ -1097,7 +1297,7 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
1097
1297
|
async function runHttpServer(options) {
|
|
1098
1298
|
const host = options.host ?? "127.0.0.1";
|
|
1099
1299
|
const port = options.port;
|
|
1100
|
-
const baseUrl =
|
|
1300
|
+
const baseUrl = resolveApiUrl();
|
|
1101
1301
|
const httpServer = createServer((req, res) => {
|
|
1102
1302
|
void handleHttpRequest(req, res, { baseUrl }).catch((error) => {
|
|
1103
1303
|
if (!res.headersSent) {
|
|
@@ -1113,8 +1313,8 @@ async function runHttpServer(options) {
|
|
|
1113
1313
|
httpServer.once("error", reject);
|
|
1114
1314
|
httpServer.listen(port, host, () => resolve());
|
|
1115
1315
|
});
|
|
1116
|
-
console.error(`
|
|
1117
|
-
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");
|
|
1118
1318
|
}
|
|
1119
1319
|
|
|
1120
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",
|