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.
- package/README.md +65 -12
- package/dist/index.js +391 -75
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# supersendtx-mcp
|
|
2
2
|
|
|
3
|
-
MCP server for [SuperSend TX](https://supersendtx.com) — send email and manage domains, webhooks, suppressions, and templates from Cursor, Claude Code, and other MCP clients.
|
|
3
|
+
MCP server for [SuperSend TX](https://supersendtx.com) — send email and manage domains, webhooks, suppressions, and templates from Cursor, Claude Code, and other MCP clients, and talk to Ranla, the agent in the same account.
|
|
4
4
|
|
|
5
5
|
**Install:** `npx -y supersendtx-mcp` · **Docs:** https://docs.supersendtx.com/ai/mcp
|
|
6
6
|
|
|
@@ -10,14 +10,14 @@ MCP server for [SuperSend TX](https://supersendtx.com) — send email and manage
|
|
|
10
10
|
|------|---------------|------|
|
|
11
11
|
| **stdio** (default) | `npx -y supersendtx-mcp` | `SUPERSENDTX_API_KEY=stx_…` or `rnl_…` |
|
|
12
12
|
| **HTTP** (local) | `npx -y supersendtx-mcp --http --port 3000` | `Authorization: Bearer stx_…` or `rnl_…` on each request |
|
|
13
|
-
| **HTTP** (hosted) | `https://mcp.
|
|
13
|
+
| **HTTP** (hosted) | `https://mcp.ranla.ai/mcp` | **OAuth** (default) or `Authorization: Bearer stx_…` or `rnl_…` |
|
|
14
14
|
|
|
15
|
-
Optional: `SUPERSENDTX_API_URL` (
|
|
15
|
+
Create API keys on the dashboard **API keys** page. `RANLA_API_KEY` works in place of `SUPERSENDTX_API_KEY`. Optional: `SUPERSENDTX_API_URL` sets the mail API base URL (default `https://api.supersendtx.com`). Over HTTP the server reads only the `Authorization` header.
|
|
16
16
|
|
|
17
17
|
Local HTTP endpoint: `http://127.0.0.1:3000/mcp` · health: `GET /health`
|
|
18
|
-
Hosted health: `GET https://mcp.
|
|
18
|
+
Hosted health: `GET https://mcp.ranla.ai/health`
|
|
19
19
|
|
|
20
|
-
Hosted OAuth: add only `"url": "https://mcp.supersendtx.com/mcp
|
|
20
|
+
Hosted OAuth: add only `"url": "https://mcp.ranla.ai/mcp"`. The client opens a browser to sign in on `app.ranla.ai`; click **Allow access**. `https://mcp.supersendtx.com/mcp` is the same server, and existing configs keep working. A Bearer key still works for advanced setups.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -60,11 +60,25 @@ npx -y supersendtx-mcp --http --port 3000
|
|
|
60
60
|
|
|
61
61
|
### Cursor (hosted)
|
|
62
62
|
|
|
63
|
+
OAuth (default) — no key in config:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"supersendtx": {
|
|
69
|
+
"url": "https://mcp.ranla.ai/mcp"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Bearer (advanced):
|
|
76
|
+
|
|
63
77
|
```json
|
|
64
78
|
{
|
|
65
79
|
"mcpServers": {
|
|
66
80
|
"supersendtx": {
|
|
67
|
-
"url": "https://mcp.
|
|
81
|
+
"url": "https://mcp.ranla.ai/mcp",
|
|
68
82
|
"headers": {
|
|
69
83
|
"Authorization": "Bearer stx_your_key_here"
|
|
70
84
|
}
|
|
@@ -78,23 +92,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
|
|
105
|
+
Export `SUPERSENDTX_API_KEY` for stdio.
|
|
92
106
|
|
|
93
107
|
---
|
|
94
108
|
|
|
95
109
|
## Tools
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
### Mail tools
|
|
112
|
+
|
|
113
|
+
These call the public API through the `supersendtx` SDK.
|
|
114
|
+
|
|
115
|
+
| Tool | Purpose |
|
|
116
|
+
|------|---------|
|
|
117
|
+
| `send_email` | Send now, or later with `scheduled_at` |
|
|
118
|
+
| `list_emails` / `get_email` | List or fetch sends |
|
|
119
|
+
| `cancel_email` | Cancel a scheduled send |
|
|
120
|
+
| `list_received_emails` / `get_received_email` | Inbound mail |
|
|
121
|
+
| `list_domains` / `get_domain` / `create_domain` | Domains; `get_domain` returns DNS records |
|
|
122
|
+
| `apply_domain_dns` / `verify_domain` | Apply DNS (`cloudflare`, `godaddy`, `vercel`) and verify |
|
|
123
|
+
| `list_webhooks` / `create_webhook` / `delete_webhook` | Webhook endpoints |
|
|
124
|
+
| `list_suppressions` / `add_suppression` / `remove_suppression` | Suppression list |
|
|
125
|
+
| `list_templates` / `get_template` / `create_template` / `publish_template` | Templates |
|
|
126
|
+
| `send_test_webhook_event` | Test webhook event (outbound email events) |
|
|
127
|
+
| `get_deliverability` | Best-effort metrics (`7d` / `30d`) |
|
|
128
|
+
|
|
129
|
+
`send_email` takes `from`, `to`, and either `subject` with `html` / `text` or `template_id` / `template_alias` with `variables`. Optional: `cc`, `bcc`, `reply_to`, `scheduled_at`, `category` (`transactional`, `product`, `newsletter`), `tags`, `headers`, `idempotency_key`, `unsubscribe`. Recipient fields take one address, several separated by commas, or an array.
|
|
130
|
+
|
|
131
|
+
### Ranla agent tools
|
|
132
|
+
|
|
133
|
+
These call the app host and need a full-access key (OAuth sign-in and dashboard keys have one; a `sending`-scope key gets a 403).
|
|
134
|
+
|
|
135
|
+
| Tool | Purpose |
|
|
136
|
+
|------|---------|
|
|
137
|
+
| `arc_ensure_thread` / `arc_list_threads` | Durable thread for this client; 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
|
|
60
|
-
SUPERSENDTX_API_URL Optional API base (
|
|
58
|
+
Environment (RANLA_* or SUPERSENDTX_*):
|
|
59
|
+
RANLA_API_KEY / SUPERSENDTX_API_KEY API key (rnl_\u2026 or stx_\u2026), required for stdio
|
|
60
|
+
RANLA_API_URL / SUPERSENDTX_API_URL Optional mail API base (ranla-mcp: https://api.ranla.ai)
|
|
61
|
+
RANLA_APP_URL / SUPERSENDTX_APP_URL Optional app host for the Ranla agent tools
|
|
61
62
|
|
|
62
63
|
HTTP auth:
|
|
63
|
-
Authorization: Bearer
|
|
64
|
+
Authorization: Bearer rnl_\u2026 or stx_\u2026 (API key) or MCP OAuth access token
|
|
64
65
|
`);
|
|
65
66
|
}
|
|
66
67
|
function isSuperSendTxApiKey(token) {
|
|
@@ -82,14 +83,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("
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
304
|
-
if (!def) return null;
|
|
436
|
+
if (!isArcMcpToolName(name)) return null;
|
|
305
437
|
const origin = resolveArcAppOrigin({ baseUrl, apiKey });
|
|
306
|
-
if (
|
|
307
|
-
const
|
|
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
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
"
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
"
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 || !
|
|
875
|
+
if (!event || !TEST_WEBHOOK_EVENTS.has(event)) {
|
|
648
876
|
return textResult(
|
|
649
|
-
`Missing or invalid event. Use one of: ${[...
|
|
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
|
|
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: {
|
|
686
|
-
|
|
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
|
-
|
|
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: {
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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"
|
|
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: [...
|
|
895
|
-
description: "
|
|
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
|
|
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("
|
|
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:
|
|
1245
|
+
{ name: "supersendtx-mcp", version: MCP_SERVER_VERSION },
|
|
934
1246
|
{ capabilities: { tools: {} } }
|
|
935
1247
|
);
|
|
936
1248
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
937
|
-
tools:
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
|
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 =
|
|
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(`
|
|
1117
|
-
console.error("Authenticate with Bearer
|
|
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.
|
|
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.
|
|
43
|
+
"supersendtx": "0.15.6"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"tsup": "^8.5.0",
|