supersendtx-mcp 0.6.30 → 0.6.34

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 +65 -12
  2. package/dist/index.js +391 -75
  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,62 @@ 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; list threads a page at a time |
138
+ | `arc_get_thread` | Read a thread's messages, artifacts and waiting approvals |
139
+ | `arc_message` | Send text to Ranla and wait for the turn — returns `text`, `artifacts`, and `pendingApproval` |
140
+ | `arc_list_approvals` / `arc_approve` / `arc_reject` | Answer approvals |
141
+ | `arc_request_connection` | Connect link for a missing integration |
142
+ | `arc_<tool>` | Read-only Ranla tools, listed from the account with input schemas. Results say in `forCaller` when a step needs `arc_message` |
143
+
144
+ Flow: `arc_ensure_thread` → `arc_message` → if a step needs approval, `arc_list_approvals` → `arc_approve` or `arc_reject`.
145
+
146
+ An SEO page approval also carries `actionId`: pass it to `arc_seo_page_package` to read the draft, for example to build the page in your own repo.
147
+
148
+ Every tool has a `title` and `readOnlyHint` / `destructiveHint` annotations, so clients can run reads without asking and confirm writes.
149
+
150
+ Full reference: https://docs.supersendtx.com/ai/mcp
98
151
 
99
152
  ## License
100
153
 
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,79 @@ 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.34";
88
+
89
+ // src/annotations.ts
90
+ var READ = { readOnlyHint: true };
91
+ var WRITE = { readOnlyHint: false, destructiveHint: false };
92
+ var DELETE = { readOnlyHint: false, destructiveHint: true };
93
+ var FIXED_TOOL_HINTS = {
94
+ send_email: { ...WRITE, openWorldHint: true },
95
+ list_emails: READ,
96
+ get_email: READ,
97
+ cancel_email: DELETE,
98
+ list_received_emails: READ,
99
+ get_received_email: READ,
100
+ list_domains: READ,
101
+ get_domain: READ,
102
+ create_domain: WRITE,
103
+ apply_domain_dns: { ...WRITE, title: "Apply domain DNS records", openWorldHint: true },
104
+ verify_domain: { ...WRITE, idempotentHint: true },
105
+ list_webhooks: READ,
106
+ create_webhook: WRITE,
107
+ delete_webhook: DELETE,
108
+ list_suppressions: READ,
109
+ add_suppression: { ...WRITE, idempotentHint: true },
110
+ remove_suppression: DELETE,
111
+ list_templates: READ,
112
+ get_template: READ,
113
+ create_template: WRITE,
114
+ publish_template: WRITE,
115
+ send_test_webhook_event: { ...WRITE, openWorldHint: true },
116
+ get_deliverability: READ,
117
+ arc_list_threads: { ...READ, title: "List Ranla threads" },
118
+ arc_get_thread: { ...READ, title: "Read a Ranla thread" },
119
+ arc_ensure_thread: { ...WRITE, title: "Open a Ranla thread", idempotentHint: true },
120
+ arc_message: { ...WRITE, title: "Message Ranla" },
121
+ arc_list_approvals: { ...READ, title: "List pending approvals" },
122
+ /** Approving can send email to real people. */
123
+ arc_approve: { ...WRITE, title: "Approve a Ranla step", openWorldHint: true },
124
+ /** Rejecting discards the proposal. */
125
+ arc_reject: { ...DELETE, title: "Reject a Ranla step" }
126
+ };
127
+ var UPPER = /* @__PURE__ */ new Set(["ai", "aeo", "api", "dns", "id", "seo", "url"]);
128
+ function titleFromToolName(name) {
129
+ const words = name.replace(/^arc_/, "").split("_").filter(Boolean);
130
+ return words.map((word, index) => {
131
+ if (UPPER.has(word)) return word.toUpperCase();
132
+ return index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word;
133
+ }).join(" ");
134
+ }
135
+ function mcpToolAnnotations(tool) {
136
+ const fixed = FIXED_TOOL_HINTS[tool.name];
137
+ if (fixed) {
138
+ const { title: title2, ...hints } = fixed;
139
+ return { title: title2 ?? titleFromToolName(tool.name), ...hints };
140
+ }
141
+ const title = titleFromToolName(tool.name);
142
+ return tool.readOnly === true ? { title, ...READ } : { title, ...WRITE };
143
+ }
144
+
85
145
  // src/arc-talk-tools.ts
86
146
  var ARC_MCP_TALK_GROWTH_TOOL_NAMES = [
87
147
  // Cold-start SEO plan: a minute of vendor data and a planning call. Read-only, so MCP carries it.
88
148
  "seo_plan",
89
- "ask_choice",
90
149
  "get_business_brief",
91
150
  "get_brand",
92
151
  "read_url",
152
+ // A canvas document Ranla wrote, as it stands now. Writing to one mutates
153
+ // and goes through `arc_message` like every other write.
154
+ "read_canvas",
155
+ // The ids read_canvas takes; nothing else an external agent can call returns them.
156
+ "list_documents",
157
+ // Every image in the account, with public URLs an external agent can use as they are.
158
+ "list_library_images",
93
159
  "get_setup_status",
94
160
  "get_growth_health",
95
161
  "get_revenue_health",
@@ -101,6 +167,7 @@ var ARC_MCP_TALK_GROWTH_TOOL_NAMES = [
101
167
  "list_goals",
102
168
  "list_lists",
103
169
  "list_segments",
170
+ "get_segment",
104
171
  "get_tracking_snippet",
105
172
  "verify_tracking_install",
106
173
  "list_automations",
@@ -128,7 +195,6 @@ var ARC_MCP_TALK_GROWTH_TOOL_NAMES = [
128
195
  "get_ad_delivery_diagnostics",
129
196
  "get_search_term_evidence",
130
197
  "list_proposed_ad_campaigns",
131
- "recommend_ad_landings",
132
198
  "read_campaign_design",
133
199
  "list_campaign_replies",
134
200
  "get_learnings",
@@ -186,9 +252,10 @@ var RANLA_AGENT_NAME = "Ranla";
186
252
  var TALK_READ_TOOLS = ARC_MCP_TALK_GROWTH_TOOL_NAMES.map((growthTool) => ({
187
253
  name: `arc_${growthTool}`,
188
254
  description: `Read-only Ranla tool: ${growthTool.replace(/_/g, " ")}.`,
189
- inputSchema: openObjectSchema("Tool input fields (see Ranla tool docs)."),
255
+ inputSchema: openObjectSchema("Pass the tool arguments as fields."),
190
256
  kind: "growth",
191
- growthTool
257
+ growthTool,
258
+ readOnly: true
192
259
  }));
193
260
  var SETUP_TOOLS = [
194
261
  {
@@ -203,14 +270,40 @@ var SETUP_TOOLS = [
203
270
  required: ["slot"]
204
271
  },
205
272
  kind: "growth",
206
- growthTool: "request_connection"
273
+ growthTool: "request_connection",
274
+ readOnly: false
207
275
  }
208
276
  ];
209
277
  var META_TOOLS = [
210
278
  {
211
279
  name: "arc_list_threads",
212
- description: "List Ranla chat threads for this account.",
213
- inputSchema: openObjectSchema(),
280
+ description: "List Ranla chat threads for this account, most recent first: id, title, desk, last activity and a one-line preview. Pass nextBefore as before for older threads. Read one with arc_get_thread.",
281
+ inputSchema: {
282
+ type: "object",
283
+ properties: {
284
+ limit: { type: "integer", description: "Threads per page. Default 50, max 100." },
285
+ before: {
286
+ type: "string",
287
+ description: "ISO timestamp from nextBefore: only threads last active before it."
288
+ }
289
+ }
290
+ },
291
+ kind: "meta"
292
+ },
293
+ {
294
+ name: "arc_get_thread",
295
+ description: "Read a Ranla thread: its messages (role, text, tools used, artifacts by id, approvals waiting on arc_approve), newest page first. Pass earlierBefore as before for older messages.",
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: {
299
+ threadId: { type: "string", description: "From arc_list_threads or arc_ensure_thread." },
300
+ before: {
301
+ type: "string",
302
+ description: "ISO timestamp from earlierBefore: the page of messages older than it."
303
+ }
304
+ },
305
+ required: ["threadId"]
306
+ },
214
307
  kind: "meta"
215
308
  },
216
309
  {
@@ -227,7 +320,7 @@ var META_TOOLS = [
227
320
  },
228
321
  {
229
322
  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.",
323
+ 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
324
  inputSchema: {
232
325
  type: "object",
233
326
  properties: {
@@ -240,13 +333,13 @@ var META_TOOLS = [
240
333
  },
241
334
  {
242
335
  name: "arc_list_approvals",
243
- description: "List pending Ranla approval cards across threads.",
336
+ description: "List pending Ranla approvals across threads.",
244
337
  inputSchema: openObjectSchema(),
245
338
  kind: "meta"
246
339
  },
247
340
  {
248
341
  name: "arc_approve",
249
- description: "Approve a pending Ranla HITL card and resume the run.",
342
+ description: "Approve a pending Ranla approval and resume the run.",
250
343
  inputSchema: {
251
344
  type: "object",
252
345
  properties: {
@@ -258,7 +351,7 @@ var META_TOOLS = [
258
351
  },
259
352
  {
260
353
  name: "arc_reject",
261
- description: "Reject a pending Ranla HITL card and resume the run.",
354
+ description: "Reject a pending Ranla approval and resume the run.",
262
355
  inputSchema: {
263
356
  type: "object",
264
357
  properties: {
@@ -275,7 +368,7 @@ function resolveArcAppOrigin(options = {}) {
275
368
  if (fromEnv) return fromEnv.replace(/\/$/, "");
276
369
  const key = options.apiKey ?? process.env.SUPERSENDTX_API_KEY ?? process.env.RANLA_API_KEY ?? "";
277
370
  if (key.startsWith("rnl_")) return DEFAULT_RANLA_APP_ORIGIN;
278
- const apiUrl = (options.baseUrl ?? process.env.SUPERSENDTX_API_URL ?? "").replace(/\/$/, "");
371
+ const apiUrl = (options.baseUrl ?? process.env.SUPERSENDTX_API_URL ?? process.env.RANLA_API_URL ?? "").replace(/\/$/, "");
279
372
  if (apiUrl.includes("api.ranla.ai")) return DEFAULT_RANLA_APP_ORIGIN;
280
373
  if (apiUrl.includes("app.ranla.ai")) return apiUrl;
281
374
  if (apiUrl.includes("app.supersendtx.com")) return apiUrl;
@@ -294,17 +387,57 @@ async function arcFetch(origin, apiKey, path, init = {}) {
294
387
  const body = await response.json().catch(() => ({}));
295
388
  return { ok: response.ok, status: response.status, body };
296
389
  }
390
+ var ARC_TOOL_PREFIX = "arc_";
391
+ var CATALOG_TIMEOUT_MS = 4e3;
392
+ function isArcMcpToolName(name) {
393
+ return name.startsWith(ARC_TOOL_PREFIX);
394
+ }
395
+ function isCatalogEntry(value) {
396
+ if (!value || typeof value !== "object") return false;
397
+ const entry = value;
398
+ const schema = entry.inputSchema;
399
+ return typeof entry.name === "string" && /^[a-z0-9_]+$/.test(entry.name) && typeof entry.description === "string" && !!schema && typeof schema === "object" && schema.type === "object";
400
+ }
401
+ async function fetchGrowthToolCatalog(apiKey, baseUrl) {
402
+ try {
403
+ const origin = resolveArcAppOrigin({ baseUrl, apiKey });
404
+ const response = await arcFetch(origin, apiKey, "/api/growth/tools", {
405
+ signal: AbortSignal.timeout(CATALOG_TIMEOUT_MS)
406
+ });
407
+ if (!response.ok || !Array.isArray(response.body.tools)) return null;
408
+ const entries = response.body.tools.filter(isCatalogEntry);
409
+ return entries.length > 0 ? entries : null;
410
+ } catch {
411
+ return null;
412
+ }
413
+ }
414
+ async function loadArcMcpTools(apiKey, baseUrl) {
415
+ const catalog = await fetchGrowthToolCatalog(apiKey, baseUrl);
416
+ if (!catalog) return ARC_MCP_TOOLS;
417
+ return [
418
+ ...META_TOOLS,
419
+ ...catalog.map((entry) => ({
420
+ name: `${ARC_TOOL_PREFIX}${entry.name}`,
421
+ description: `${entry.mutates ? RANLA_AGENT_NAME : `${RANLA_AGENT_NAME}, read-only`}: ${entry.description}`,
422
+ inputSchema: entry.inputSchema,
423
+ kind: "growth",
424
+ growthTool: entry.name,
425
+ // Missing `mutates` is not a promise of read-only: only an explicit false is.
426
+ readOnly: entry.mutates === false
427
+ }))
428
+ ];
429
+ }
297
430
  function optString(args, key) {
298
431
  if (args[key] == null) return void 0;
299
432
  const value = String(args[key]).trim();
300
433
  return value || void 0;
301
434
  }
302
435
  async function callArcMcpTool(apiKey, name, args, baseUrl) {
303
- const def = ARC_MCP_TOOLS.find((tool) => tool.name === name);
304
- if (!def) return null;
436
+ if (!isArcMcpToolName(name)) return null;
305
437
  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)}`, {
438
+ if (!META_TOOLS.some((tool) => tool.name === name)) {
439
+ const growthTool = name.slice(ARC_TOOL_PREFIX.length);
440
+ const response = await arcFetch(origin, apiKey, `/api/growth/tools/${encodeURIComponent(growthTool)}`, {
308
441
  method: "POST",
309
442
  body: JSON.stringify({ input: args })
310
443
  });
@@ -317,7 +450,13 @@ async function callArcMcpTool(apiKey, name, args, baseUrl) {
317
450
  }
318
451
  switch (name) {
319
452
  case "arc_list_threads": {
320
- const response = await arcFetch(origin, apiKey, "/api/growth/threads");
453
+ const query = new URLSearchParams();
454
+ const limit = optString(args, "limit");
455
+ const before = optString(args, "before");
456
+ if (limit) query.set("limit", limit);
457
+ if (before) query.set("before", before);
458
+ const suffix = query.toString() ? `?${query.toString()}` : "";
459
+ const response = await arcFetch(origin, apiKey, `/api/growth/threads${suffix}`);
321
460
  if (!response.ok) {
322
461
  throw new Error(
323
462
  typeof response.body.error === "string" ? response.body.error : `List threads failed (${response.status})`
@@ -325,6 +464,22 @@ async function callArcMcpTool(apiKey, name, args, baseUrl) {
325
464
  }
326
465
  return response.body;
327
466
  }
467
+ case "arc_get_thread": {
468
+ const threadId = optString(args, "threadId");
469
+ if (!threadId) throw new Error("Missing required argument: threadId");
470
+ const before = optString(args, "before");
471
+ const response = await arcFetch(
472
+ origin,
473
+ apiKey,
474
+ `/api/growth/threads/${encodeURIComponent(threadId)}${before ? `?before=${encodeURIComponent(before)}` : ""}`
475
+ );
476
+ if (!response.ok) {
477
+ throw new Error(
478
+ typeof response.body.error === "string" ? response.body.error : `Get thread failed (${response.status})`
479
+ );
480
+ }
481
+ return response.body;
482
+ }
328
483
  case "arc_ensure_thread": {
329
484
  const existingId = optString(args, "threadId");
330
485
  if (existingId) {
@@ -456,23 +611,46 @@ function optStringArray(args, key) {
456
611
  if (Array.isArray(value)) return value.map((v) => String(v));
457
612
  return void 0;
458
613
  }
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
- ]);
614
+ function optRecipients(args, key) {
615
+ const value = args[key];
616
+ if (value == null) return void 0;
617
+ const parts = (Array.isArray(value) ? value.map((entry) => String(entry)) : String(value).split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)).map((entry) => entry.trim()).filter(Boolean);
618
+ if (parts.length === 0) return void 0;
619
+ return parts.length === 1 ? parts[0] : parts;
620
+ }
621
+ function optStringMap(args, key) {
622
+ const value = args[key];
623
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
624
+ const entries = Object.entries(value).filter(([, entry]) => entry != null);
625
+ return entries.length > 0 ? Object.fromEntries(entries.map(([name, entry]) => [name, String(entry)])) : void 0;
626
+ }
627
+ var TEST_WEBHOOK_EVENT_FLAGS = {
628
+ "email.sent": true,
629
+ "email.delivered": true,
630
+ "email.delivery_delayed": true,
631
+ "email.bounced": true,
632
+ "email.complained": true,
633
+ "email.opened": true,
634
+ "email.clicked": true,
635
+ "email.failed": true,
636
+ "email.suppressed": true,
637
+ "email.scheduled": true
638
+ };
639
+ var TEST_WEBHOOK_EVENTS = new Set(Object.keys(TEST_WEBHOOK_EVENT_FLAGS));
640
+ var WEBHOOK_EVENT_FLAGS = {
641
+ ...TEST_WEBHOOK_EVENT_FLAGS,
642
+ "email.received": true,
643
+ "contact.unsubscribed": true,
644
+ "automation.started": true,
645
+ "automation.step_completed": true,
646
+ "automation.completed": true,
647
+ "automation.failed": true
648
+ };
649
+ var WEBHOOK_EVENTS = Object.keys(WEBHOOK_EVENT_FLAGS);
650
+ var EMAIL_CATEGORIES = ["transactional", "product", "newsletter"];
472
651
  async function callMcpTool(client, name, args, options = {}) {
473
652
  try {
474
- const arc = ARC_MCP_TOOLS.find((tool) => tool.name === name);
475
- if (arc) {
653
+ if (isArcMcpToolName(name)) {
476
654
  const apiKey = options.apiKey ?? resolveApiKey();
477
655
  const result = await callArcMcpTool(apiKey, name, args, options.baseUrl);
478
656
  return textResult(result);
@@ -480,7 +658,7 @@ async function callMcpTool(client, name, args, options = {}) {
480
658
  switch (name) {
481
659
  case "send_email": {
482
660
  const from = optString2(args, "from");
483
- const to = optString2(args, "to");
661
+ const to = optRecipients(args, "to");
484
662
  const subject = optString2(args, "subject");
485
663
  const templateId = optString2(args, "template_id") || optString2(args, "template_alias");
486
664
  if (!from) return textResult("Missing required argument: from", true);
@@ -488,14 +666,33 @@ async function callMcpTool(client, name, args, options = {}) {
488
666
  if (!templateId && !subject) {
489
667
  return textResult("Provide subject, or template_id / template_alias", true);
490
668
  }
669
+ const category = optString2(args, "category");
670
+ if (category && !EMAIL_CATEGORIES.includes(category)) {
671
+ return textResult(`Invalid category. Use one of: ${EMAIL_CATEGORIES.join(", ")}`, true);
672
+ }
491
673
  const variables = args.variables && typeof args.variables === "object" && !Array.isArray(args.variables) ? args.variables : void 0;
674
+ const cc = optRecipients(args, "cc");
675
+ const bcc = optRecipients(args, "bcc");
676
+ const tags = optStringMap(args, "tags");
677
+ const headers = optStringMap(args, "headers");
678
+ const scheduledAt = optString2(args, "scheduled_at");
679
+ const idempotencyKey = optString2(args, "idempotency_key");
680
+ const unsubscribe = optBoolean(args, "unsubscribe");
492
681
  const result = await client.emails.send({
493
682
  from,
494
683
  to,
495
684
  ...subject ? { subject } : {},
496
685
  html: optString2(args, "html"),
497
686
  text: optString2(args, "text"),
498
- reply_to: optString2(args, "reply_to"),
687
+ reply_to: optRecipients(args, "reply_to"),
688
+ ...cc ? { cc } : {},
689
+ ...bcc ? { bcc } : {},
690
+ ...tags ? { tags } : {},
691
+ ...headers ? { headers } : {},
692
+ ...scheduledAt ? { scheduled_at: scheduledAt } : {},
693
+ ...category ? { category } : {},
694
+ ...unsubscribe !== void 0 ? { unsubscribe } : {},
695
+ ...idempotencyKey ? { idempotencyKey } : {},
499
696
  ...templateId ? { template: { id: templateId, ...variables ? { variables } : {} } } : {}
500
697
  });
501
698
  return textResult(result);
@@ -512,6 +709,30 @@ async function callMcpTool(client, name, args, options = {}) {
512
709
  if (!id) return textResult("Missing required argument: id", true);
513
710
  return textResult(await client.emails.get(id));
514
711
  }
712
+ case "cancel_email": {
713
+ const id = optString2(args, "id");
714
+ if (!id) return textResult("Missing required argument: id", true);
715
+ return textResult(await client.emails.cancel(id));
716
+ }
717
+ case "list_received_emails": {
718
+ return textResult(
719
+ await client.receivedEmails.list({
720
+ limit: optNumber(args, "limit"),
721
+ cursor: optString2(args, "cursor"),
722
+ domain: optString2(args, "domain")
723
+ })
724
+ );
725
+ }
726
+ case "get_received_email": {
727
+ const id = optString2(args, "id");
728
+ if (!id) return textResult("Missing required argument: id", true);
729
+ return textResult(await client.receivedEmails.get(id));
730
+ }
731
+ case "get_domain": {
732
+ const domain = optString2(args, "domain");
733
+ if (!domain) return textResult("Missing required argument: domain", true);
734
+ return textResult(await client.domains.get(domain));
735
+ }
515
736
  case "apply_domain_dns": {
516
737
  const domain = String(args.domain ?? "").trim();
517
738
  if (!domain) {
@@ -538,9 +759,11 @@ async function callMcpTool(client, name, args, options = {}) {
538
759
  return textResult(result);
539
760
  }
540
761
  case "list_domains": {
762
+ const inbound = optBoolean(args, "inbound_enabled");
541
763
  const result = await client.domains.list({
542
764
  limit: optNumber(args, "limit"),
543
- cursor: optString2(args, "cursor")
765
+ cursor: optString2(args, "cursor"),
766
+ ...inbound !== void 0 ? { inbound_enabled: inbound } : {}
544
767
  });
545
768
  return textResult(result);
546
769
  }
@@ -567,6 +790,10 @@ async function callMcpTool(client, name, args, options = {}) {
567
790
  const url = optString2(args, "url");
568
791
  if (!url) return textResult("Missing required argument: url", true);
569
792
  const events = optStringArray(args, "events");
793
+ const unknown = events?.filter((event) => !WEBHOOK_EVENTS.includes(event)) ?? [];
794
+ if (unknown.length > 0) {
795
+ return textResult(`Unknown event types: ${unknown.join(", ")}. Use: ${WEBHOOK_EVENTS.join(", ")}`, true);
796
+ }
570
797
  return textResult(
571
798
  await client.webhooks.create({
572
799
  url,
@@ -624,12 +851,12 @@ async function callMcpTool(client, name, args, options = {}) {
624
851
  const name2 = optString2(args, "name");
625
852
  const subjectArg = optString2(args, "subject");
626
853
  if (!name2) return textResult("Missing required argument: name", true);
627
- if (!subjectArg) return textResult("Missing required argument: subject", true);
628
854
  const format = optString2(args, "format");
629
855
  return textResult(
630
856
  await client.templates.create({
631
857
  name: name2,
632
- subject: subjectArg,
858
+ // Optional: a layout-only template has no default subject.
859
+ ...subjectArg ? { subject: subjectArg } : {},
633
860
  ...format === "blocks" || format === "html" ? { format } : {},
634
861
  html: optString2(args, "html"),
635
862
  text: optString2(args, "text"),
@@ -640,13 +867,14 @@ async function callMcpTool(client, name, args, options = {}) {
640
867
  case "publish_template": {
641
868
  const idOrAlias = optString2(args, "id") ?? optString2(args, "alias");
642
869
  if (!idOrAlias) return textResult("Missing required argument: id or alias", true);
643
- return textResult(await client.templates.publish(idOrAlias));
870
+ const label = optString2(args, "label");
871
+ return textResult(await client.templates.publish(idOrAlias, label ? { label } : {}));
644
872
  }
645
873
  case "send_test_webhook_event": {
646
874
  const event = optString2(args, "event");
647
- if (!event || !EMAIL_WEBHOOK_EVENTS.has(event)) {
875
+ if (!event || !TEST_WEBHOOK_EVENTS.has(event)) {
648
876
  return textResult(
649
- `Missing or invalid event. Use one of: ${[...EMAIL_WEBHOOK_EVENTS].join(", ")}`,
877
+ `Missing or invalid event. Use one of: ${[...TEST_WEBHOOK_EVENTS].join(", ")}`,
650
878
  true
651
879
  );
652
880
  }
@@ -654,6 +882,7 @@ async function callMcpTool(client, name, args, options = {}) {
654
882
  await client.emails.testWebhook({
655
883
  event,
656
884
  email_id: optString2(args, "email_id"),
885
+ webhook_id: optString2(args, "webhook_id"),
657
886
  deliver: optBoolean(args, "deliver")
658
887
  })
659
888
  );
@@ -675,26 +904,56 @@ var paginationProps = {
675
904
  limit: { type: "number", description: "Page size (1\u2013100)" },
676
905
  cursor: { type: "string", description: "Opaque cursor for the next page" }
677
906
  };
907
+ var recipientProp = (what) => ({
908
+ description: `${what}: one address, several separated by commas, or an array of addresses`,
909
+ anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }]
910
+ });
678
911
  var MCP_TOOLS = [
679
912
  {
680
913
  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.",
914
+ 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
915
  inputSchema: {
683
916
  type: "object",
684
917
  properties: {
685
- from: { type: "string", description: "Sender address (must match a verified domain or sandbox sender)" },
686
- to: { type: "string", description: "Recipient email address" },
918
+ from: {
919
+ type: "string",
920
+ 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."
921
+ },
922
+ to: recipientProp("Recipients"),
923
+ cc: recipientProp("Cc recipients"),
924
+ bcc: recipientProp("Bcc recipients"),
925
+ reply_to: recipientProp("Reply-To"),
687
926
  subject: { type: "string", description: "Optional when using a published template" },
688
927
  html: { type: "string" },
689
928
  text: { type: "string" },
690
- reply_to: { type: "string" },
691
- template_id: { type: "string", description: "Published template UUID" },
929
+ template_id: { type: "string", description: "Published template id" },
692
930
  template_alias: { type: "string", description: "Published template alias" },
693
931
  variables: {
694
932
  type: "object",
695
933
  description: "Template variables when using template_id or template_alias",
696
934
  additionalProperties: true
697
- }
935
+ },
936
+ scheduled_at: { type: "string", description: "ISO 8601 time to send, e.g. 2026-10-01T09:00:00Z" },
937
+ category: {
938
+ type: "string",
939
+ enum: [...EMAIL_CATEGORIES],
940
+ description: "Default transactional. product and newsletter add an unsubscribe link and skip recipients who opted out of that category."
941
+ },
942
+ tags: {
943
+ type: "object",
944
+ description: "Tags as name \u2192 value, for filtering and webhooks",
945
+ additionalProperties: { type: "string" }
946
+ },
947
+ headers: {
948
+ type: "object",
949
+ description: "Extra email headers as name \u2192 value",
950
+ additionalProperties: { type: "string" }
951
+ },
952
+ idempotency_key: {
953
+ type: "string",
954
+ description: "Retry-safe key: the same key within 24 hours returns the first send instead of sending twice"
955
+ },
956
+ unsubscribe: { type: "boolean", description: "Add a managed unsubscribe link to this send" }
698
957
  },
699
958
  required: ["from", "to"]
700
959
  }
@@ -718,12 +977,59 @@ var MCP_TOOLS = [
718
977
  required: ["id"]
719
978
  }
720
979
  },
980
+ {
981
+ name: "cancel_email",
982
+ description: "Cancel a scheduled email before it sends.",
983
+ inputSchema: {
984
+ type: "object",
985
+ properties: {
986
+ id: { type: "string", description: "Email id (msg_\u2026) of a scheduled send" }
987
+ },
988
+ required: ["id"]
989
+ }
990
+ },
991
+ {
992
+ name: "list_received_emails",
993
+ description: "List inbound emails received on domains with inbound enabled.",
994
+ inputSchema: {
995
+ type: "object",
996
+ properties: {
997
+ ...paginationProps,
998
+ domain: { type: "string", description: "Only messages received on this domain" }
999
+ }
1000
+ }
1001
+ },
1002
+ {
1003
+ name: "get_received_email",
1004
+ description: "Get one inbound email, including its headers and body.",
1005
+ inputSchema: {
1006
+ type: "object",
1007
+ properties: {
1008
+ id: { type: "string", description: "Received email id" }
1009
+ },
1010
+ required: ["id"]
1011
+ }
1012
+ },
721
1013
  {
722
1014
  name: "list_domains",
723
1015
  description: "List sending domains for this account.",
724
1016
  inputSchema: {
725
1017
  type: "object",
726
- properties: { ...paginationProps }
1018
+ properties: {
1019
+ ...paginationProps,
1020
+ inbound_enabled: { type: "boolean", description: "Only domains with inbound receiving on (or off)" }
1021
+ }
1022
+ }
1023
+ },
1024
+ {
1025
+ name: "get_domain",
1026
+ description: "Get one sending domain by id or name, with its DNS records and verification status.",
1027
+ inputSchema: {
1028
+ type: "object",
1029
+ properties: {
1030
+ domain: { type: "string", description: "Domain id or name (example.com)" }
1031
+ },
1032
+ required: ["domain"]
727
1033
  }
728
1034
  },
729
1035
  {
@@ -740,7 +1046,7 @@ var MCP_TOOLS = [
740
1046
  },
741
1047
  {
742
1048
  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.",
1049
+ 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
1050
  inputSchema: {
745
1051
  type: "object",
746
1052
  properties: {
@@ -782,8 +1088,8 @@ var MCP_TOOLS = [
782
1088
  url: { type: "string", description: "HTTPS URL that receives events" },
783
1089
  events: {
784
1090
  type: "array",
785
- items: { type: "string" },
786
- description: "Optional event types (defaults apply when omitted)"
1091
+ items: { type: "string", enum: WEBHOOK_EVENTS },
1092
+ description: "Event types to deliver (all of them when omitted)"
787
1093
  }
788
1094
  },
789
1095
  required: ["url"]
@@ -795,7 +1101,7 @@ var MCP_TOOLS = [
795
1101
  inputSchema: {
796
1102
  type: "object",
797
1103
  properties: {
798
- id: { type: "string", description: "Webhook id (wh_\u2026)" }
1104
+ id: { type: "string", description: "Webhook id" }
799
1105
  },
800
1106
  required: ["id"]
801
1107
  }
@@ -851,14 +1157,14 @@ var MCP_TOOLS = [
851
1157
  inputSchema: {
852
1158
  type: "object",
853
1159
  properties: {
854
- id: { type: "string", description: "Template id (tpl_\u2026)" },
1160
+ id: { type: "string", description: "Template id" },
855
1161
  alias: { type: "string", description: "Published alias (alternative to id)" }
856
1162
  }
857
1163
  }
858
1164
  },
859
1165
  {
860
1166
  name: "create_template",
861
- description: "Create a draft email template (html or blocks format).",
1167
+ description: "Create a draft email template (html or blocks format). Publish it before sending with it.",
862
1168
  inputSchema: {
863
1169
  type: "object",
864
1170
  properties: {
@@ -869,7 +1175,7 @@ var MCP_TOOLS = [
869
1175
  text: { type: "string" },
870
1176
  alias: { type: "string" }
871
1177
  },
872
- required: ["name", "subject"]
1178
+ required: ["name"]
873
1179
  }
874
1180
  },
875
1181
  {
@@ -879,7 +1185,8 @@ var MCP_TOOLS = [
879
1185
  type: "object",
880
1186
  properties: {
881
1187
  id: { type: "string" },
882
- alias: { type: "string" }
1188
+ alias: { type: "string" },
1189
+ label: { type: "string", description: "Optional name for this version in the template history" }
883
1190
  }
884
1191
  }
885
1192
  },
@@ -891,10 +1198,11 @@ var MCP_TOOLS = [
891
1198
  properties: {
892
1199
  event: {
893
1200
  type: "string",
894
- enum: [...EMAIL_WEBHOOK_EVENTS],
895
- description: "Webhook event type to simulate"
1201
+ enum: [...TEST_WEBHOOK_EVENTS],
1202
+ description: "Email event to simulate (email.received cannot be simulated \u2014 send a real inbound message)"
896
1203
  },
897
1204
  email_id: { type: "string", description: "Optional existing email id (msg_\u2026)" },
1205
+ webhook_id: { type: "string", description: "Deliver to this one webhook endpoint only" },
898
1206
  deliver: {
899
1207
  type: "boolean",
900
1208
  description: "When true (default), enqueue delivery to account webhooks"
@@ -915,30 +1223,39 @@ var MCP_TOOLS = [
915
1223
  }
916
1224
  ];
917
1225
  var MCP_TOOLS_WITH_ARC = [...MCP_TOOLS, ...ARC_MCP_TOOLS];
1226
+ function resolveApiUrl(env = process.env) {
1227
+ return env.SUPERSENDTX_API_URL?.trim() || env.RANLA_API_URL?.trim() || void 0;
1228
+ }
918
1229
  function resolveApiKey() {
919
- const key = process.env.SUPERSENDTX_API_KEY?.trim();
1230
+ const key = process.env.SUPERSENDTX_API_KEY?.trim() || process.env.RANLA_API_KEY?.trim();
920
1231
  if (!key) {
921
- throw new Error("SUPERSENDTX_API_KEY is required");
1232
+ throw new Error("Set RANLA_API_KEY (or SUPERSENDTX_API_KEY) to an API key");
922
1233
  }
923
1234
  if (!key.startsWith("stx_") && !key.startsWith("rnl_")) {
924
- throw new Error("SUPERSENDTX_API_KEY must start with stx_ or rnl_");
1235
+ throw new Error("The API key must start with rnl_ or stx_ (RANLA_API_KEY / SUPERSENDTX_API_KEY)");
925
1236
  }
926
1237
  return key;
927
1238
  }
928
1239
 
929
1240
  // src/server.ts
1241
+ var MCP_SERVER_VERSION = version;
930
1242
  function createMcpServer(apiKey, baseUrl) {
931
1243
  const client = new SuperSendTX(apiKey, baseUrl ? { baseUrl } : {});
932
1244
  const server = new Server(
933
- { name: "supersendtx-mcp", version: "0.3.0" },
1245
+ { name: "supersendtx-mcp", version: MCP_SERVER_VERSION },
934
1246
  { capabilities: { tools: {} } }
935
1247
  );
936
1248
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
937
- tools: MCP_TOOLS_WITH_ARC.map((tool) => ({
938
- name: tool.name,
939
- description: tool.description,
940
- inputSchema: tool.inputSchema
941
- }))
1249
+ tools: [...MCP_TOOLS, ...await loadArcMcpTools(apiKey, baseUrl)].map((tool) => {
1250
+ const annotations = mcpToolAnnotations(tool);
1251
+ return {
1252
+ name: tool.name,
1253
+ title: annotations.title,
1254
+ description: tool.description,
1255
+ inputSchema: tool.inputSchema,
1256
+ annotations
1257
+ };
1258
+ })
942
1259
  }));
943
1260
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
944
1261
  const result = await callMcpTool(client, request.params.name, request.params.arguments ?? {}, {
@@ -951,8 +1268,7 @@ function createMcpServer(apiKey, baseUrl) {
951
1268
  }
952
1269
  async function runStdioServer() {
953
1270
  const apiKey = resolveApiKey();
954
- const baseUrl = process.env.SUPERSENDTX_API_URL?.trim();
955
- const server = createMcpServer(apiKey, baseUrl || void 0);
1271
+ const server = createMcpServer(apiKey, resolveApiUrl());
956
1272
  const transport = new StdioServerTransport();
957
1273
  await server.connect(transport);
958
1274
  }
@@ -1097,7 +1413,7 @@ async function handleHttpRequest(req, res, options = {}) {
1097
1413
  async function runHttpServer(options) {
1098
1414
  const host = options.host ?? "127.0.0.1";
1099
1415
  const port = options.port;
1100
- const baseUrl = process.env.SUPERSENDTX_API_URL?.trim() || void 0;
1416
+ const baseUrl = resolveApiUrl();
1101
1417
  const httpServer = createServer((req, res) => {
1102
1418
  void handleHttpRequest(req, res, { baseUrl }).catch((error) => {
1103
1419
  if (!res.headersSent) {
@@ -1113,8 +1429,8 @@ async function runHttpServer(options) {
1113
1429
  httpServer.once("error", reject);
1114
1430
  httpServer.listen(port, host, () => resolve());
1115
1431
  });
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");
1432
+ console.error(`MCP HTTP server listening on http://${host}:${port}/mcp`);
1433
+ console.error("Authenticate with Bearer rnl_\u2026 / stx_\u2026 or an MCP OAuth access token");
1118
1434
  }
1119
1435
 
1120
1436
  // 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.34",
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",