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.
Files changed (3) hide show
  1. package/README.md +60 -12
  2. package/dist/index.js +264 -64
  3. 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.supersendtx.com/mcp` | **OAuth** (recommended) or `Authorization: Bearer stx_…` or `rnl_…` |
13
+ | **HTTP** (hosted) | `https://mcp.ranla.ai/mcp` | **OAuth** (default) or `Authorization: Bearer stx_…` or `rnl_…` |
14
14
 
15
- Optional: `SUPERSENDTX_API_URL` (e.g. `http://localhost:3003/api` for local API).
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.supersendtx.com/health`
18
+ Hosted health: `GET https://mcp.ranla.ai/health`
19
19
 
20
- Hosted OAuth: add only `"url": "https://mcp.supersendtx.com/mcp"` — the client runs the browser consent flow. Bearer `stx_…` or `rnl_…` still works for advanced setups.
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.supersendtx.com/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, or configure Bearer headers for HTTP.
105
+ Export `SUPERSENDTX_API_KEY` for stdio.
92
106
 
93
107
  ---
94
108
 
95
109
  ## Tools
96
110
 
97
- See https://docs.supersendtx.com/ai/mcp — emails, domains, webhooks, suppressions, templates, deliverability, webhook test.
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 Required for stdio (Bearer stx_\u2026 or rnl_\u2026 for HTTP)
60
- SUPERSENDTX_API_URL Optional API base (default https://api.supersendtx.com)
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 stx_\u2026 or rnl_\u2026 (API key) or MCP OAuth access token
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("Tool input fields (see Ranla tool docs)."),
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 any mutating work. Returns text plus pendingApproval when HITL is required.",
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 HITL card and resume the run.",
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 HITL card and resume the run.",
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
- const def = ARC_MCP_TOOLS.find((tool) => tool.name === name);
304
- if (!def) return null;
347
+ if (!isArcMcpToolName(name)) return null;
305
348
  const origin = resolveArcAppOrigin({ baseUrl, apiKey });
306
- if (def.kind === "growth" && def.growthTool) {
307
- const response = await arcFetch(origin, apiKey, `/api/growth/tools/${encodeURIComponent(def.growthTool)}`, {
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
- var EMAIL_WEBHOOK_EVENTS = /* @__PURE__ */ new Set([
460
- "email.received",
461
- "email.sent",
462
- "email.delivered",
463
- "email.delivery_delayed",
464
- "email.bounced",
465
- "email.complained",
466
- "email.opened",
467
- "email.clicked",
468
- "email.failed",
469
- "email.suppressed",
470
- "email.scheduled"
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
- const arc = ARC_MCP_TOOLS.find((tool) => tool.name === name);
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 = optString2(args, "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: optString2(args, "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
- subject: subjectArg,
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
- return textResult(await client.templates.publish(idOrAlias));
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 || !EMAIL_WEBHOOK_EVENTS.has(event)) {
764
+ if (!event || !TEST_WEBHOOK_EVENTS.has(event)) {
648
765
  return textResult(
649
- `Missing or invalid event. Use one of: ${[...EMAIL_WEBHOOK_EVENTS].join(", ")}`,
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 a transactional email via SuperSend TX. Provide subject + html/text, or a published template_id / template_alias with optional variables.",
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: { type: "string", description: "Sender address (must match a verified domain or sandbox sender)" },
686
- to: { type: "string", description: "Recipient email address" },
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
- reply_to: { type: "string" },
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: { ...paginationProps }
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: "Apply SuperSend TX DNS records for a domain. Cloudflare and GoDaddy can use credentials stored in the dashboard; GoDaddy also accepts one-time credentials on the request.",
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: "Optional event types (defaults apply when omitted)"
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 (wh_\u2026)" }
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 (tpl_\u2026)" },
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", "subject"]
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: [...EMAIL_WEBHOOK_EVENTS],
895
- description: "Webhook event type to simulate"
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 is required");
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("SUPERSENDTX_API_KEY must start with stx_ or rnl_");
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: "0.3.0" },
1134
+ { name: "supersendtx-mcp", version: MCP_SERVER_VERSION },
934
1135
  { capabilities: { tools: {} } }
935
1136
  );
936
1137
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
937
- tools: MCP_TOOLS_WITH_ARC.map((tool) => ({
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 baseUrl = process.env.SUPERSENDTX_API_URL?.trim();
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 = process.env.SUPERSENDTX_API_URL?.trim() || void 0;
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(`SuperSend TX MCP HTTP listening on http://${host}:${port}/mcp`);
1117
- console.error("Authenticate with Bearer stx_\u2026 / rnl_\u2026 or MCP OAuth access token");
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.30",
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.5"
43
+ "supersendtx": "0.15.6"
44
44
  },
45
45
  "devDependencies": {
46
46
  "tsup": "^8.5.0",