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.
Files changed (3) hide show
  1. package/README.md +60 -12
  2. package/dist/index.js +266 -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,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
- "ask_choice",
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("Tool input fields (see Ranla tool docs)."),
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 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.",
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 HITL card and resume the run.",
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 HITL card and resume the run.",
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
- const def = ARC_MCP_TOOLS.find((tool) => tool.name === name);
302
- if (!def) return null;
347
+ if (!isArcMcpToolName(name)) return null;
303
348
  const origin = resolveArcAppOrigin({ baseUrl, apiKey });
304
- if (def.kind === "growth" && def.growthTool) {
305
- 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)}`, {
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
- var EMAIL_WEBHOOK_EVENTS = /* @__PURE__ */ new Set([
458
- "email.received",
459
- "email.sent",
460
- "email.delivered",
461
- "email.delivery_delayed",
462
- "email.bounced",
463
- "email.complained",
464
- "email.opened",
465
- "email.clicked",
466
- "email.failed",
467
- "email.suppressed",
468
- "email.scheduled"
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
- const arc = ARC_MCP_TOOLS.find((tool) => tool.name === name);
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 = optString2(args, "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: 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 } : {},
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
- subject: subjectArg,
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
- return textResult(await client.templates.publish(idOrAlias));
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 || !EMAIL_WEBHOOK_EVENTS.has(event)) {
764
+ if (!event || !TEST_WEBHOOK_EVENTS.has(event)) {
646
765
  return textResult(
647
- `Missing or invalid event. Use one of: ${[...EMAIL_WEBHOOK_EVENTS].join(", ")}`,
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 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).",
680
804
  inputSchema: {
681
805
  type: "object",
682
806
  properties: {
683
- from: { type: "string", description: "Sender address (must match a verified domain or sandbox sender)" },
684
- 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"),
685
815
  subject: { type: "string", description: "Optional when using a published template" },
686
816
  html: { type: "string" },
687
817
  text: { type: "string" },
688
- reply_to: { type: "string" },
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: { ...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"]
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: "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.",
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: "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)"
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 (wh_\u2026)" }
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 (tpl_\u2026)" },
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", "subject"]
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: [...EMAIL_WEBHOOK_EVENTS],
893
- 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)"
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 is required");
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("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)");
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: "0.3.0" },
1134
+ { name: "supersendtx-mcp", version: MCP_SERVER_VERSION },
932
1135
  { capabilities: { tools: {} } }
933
1136
  );
934
1137
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
935
- tools: MCP_TOOLS_WITH_ARC.map((tool) => ({
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 baseUrl = process.env.SUPERSENDTX_API_URL?.trim();
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 = process.env.SUPERSENDTX_API_URL?.trim() || void 0;
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(`SuperSend TX MCP HTTP listening on http://${host}:${port}/mcp`);
1115
- 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");
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.29",
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",