supersendtx-mcp 0.1.0 → 0.4.0
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 +101 -0
- package/dist/index.js +540 -9
- package/package.json +6 -4
- package/smithery.yaml +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# supersendtx-mcp
|
|
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.
|
|
4
|
+
|
|
5
|
+
**Install:** `npx -y supersendtx-mcp` · **Docs:** https://docs.supersendtx.com/ai/mcp
|
|
6
|
+
|
|
7
|
+
## Transports
|
|
8
|
+
|
|
9
|
+
| Mode | Command / URL | Auth |
|
|
10
|
+
|------|---------------|------|
|
|
11
|
+
| **stdio** (default) | `npx -y supersendtx-mcp` | `SUPERSENDTX_API_KEY=stx_…` |
|
|
12
|
+
| **HTTP** (local) | `npx -y supersendtx-mcp --http --port 3000` | `Authorization: Bearer stx_…` on each request |
|
|
13
|
+
| **HTTP** (hosted) | `https://mcp.supersendtx.com/mcp` | **OAuth** (recommended) or `Authorization: Bearer stx_…` |
|
|
14
|
+
|
|
15
|
+
Optional: `SUPERSENDTX_API_URL` (e.g. `http://localhost:3003/api` for local API).
|
|
16
|
+
|
|
17
|
+
Local HTTP endpoint: `http://127.0.0.1:3000/mcp` · health: `GET /health`
|
|
18
|
+
Hosted health: `GET https://mcp.supersendtx.com/health`
|
|
19
|
+
|
|
20
|
+
Hosted OAuth: add only `"url": "https://mcp.supersendtx.com/mcp"` — the client runs the browser consent flow. Bearer `stx_…` still works for advanced setups.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Cursor (stdio)
|
|
25
|
+
|
|
26
|
+
`.cursor/mcp.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"supersendtx": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "supersendtx-mcp"],
|
|
34
|
+
"env": {
|
|
35
|
+
"SUPERSENDTX_API_KEY": "stx_your_key_here"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Cursor (local HTTP)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx -y supersendtx-mcp --http --port 3000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"supersendtx": {
|
|
52
|
+
"url": "http://127.0.0.1:3000/mcp",
|
|
53
|
+
"headers": {
|
|
54
|
+
"Authorization": "Bearer stx_your_key_here"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Cursor (hosted)
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"supersendtx": {
|
|
67
|
+
"url": "https://mcp.supersendtx.com/mcp",
|
|
68
|
+
"headers": {
|
|
69
|
+
"Authorization": "Bearer stx_your_key_here"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Claude Code
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# stdio
|
|
82
|
+
claude mcp add --transport stdio supersendtx -- npx -y supersendtx-mcp
|
|
83
|
+
|
|
84
|
+
# 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
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Export `SUPERSENDTX_API_KEY` for stdio, or configure Bearer headers for HTTP.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Tools
|
|
96
|
+
|
|
97
|
+
See https://docs.supersendtx.com/ai/mcp — emails, domains, webhooks, suppressions, templates, deliverability, webhook test.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
var DEFAULT_HTTP_PORT = 3e3;
|
|
5
|
+
var DEFAULT_HTTP_HOST = "127.0.0.1";
|
|
6
|
+
function parseCliArgs(argv) {
|
|
7
|
+
const args = argv.slice(2);
|
|
8
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
9
|
+
return { mode: "help" };
|
|
10
|
+
}
|
|
11
|
+
const http = args.includes("--http");
|
|
12
|
+
if (!http) {
|
|
13
|
+
return { mode: "stdio" };
|
|
14
|
+
}
|
|
15
|
+
let port = DEFAULT_HTTP_PORT;
|
|
16
|
+
let host = DEFAULT_HTTP_HOST;
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
const arg = args[i];
|
|
19
|
+
if (arg === "--port" || arg === "-p") {
|
|
20
|
+
const raw = args[i + 1];
|
|
21
|
+
const parsed = Number(raw);
|
|
22
|
+
if (!raw || !Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
23
|
+
throw new Error(`Invalid --port value: ${raw ?? "(missing)"}`);
|
|
24
|
+
}
|
|
25
|
+
port = parsed;
|
|
26
|
+
i++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg?.startsWith("--port=")) {
|
|
30
|
+
const raw = arg.slice("--port=".length);
|
|
31
|
+
const parsed = Number(raw);
|
|
32
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
33
|
+
throw new Error(`Invalid --port value: ${raw}`);
|
|
34
|
+
}
|
|
35
|
+
port = parsed;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === "--host") {
|
|
39
|
+
const raw = args[i + 1]?.trim();
|
|
40
|
+
if (!raw) throw new Error("Missing --host value");
|
|
41
|
+
host = raw;
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (arg?.startsWith("--host=")) {
|
|
46
|
+
const raw = arg.slice("--host=".length).trim();
|
|
47
|
+
if (!raw) throw new Error("Missing --host value");
|
|
48
|
+
host = raw;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { mode: "http", port, host };
|
|
52
|
+
}
|
|
53
|
+
function printHelp() {
|
|
54
|
+
console.error(`Usage:
|
|
55
|
+
supersendtx-mcp # stdio (default)
|
|
56
|
+
supersendtx-mcp --http [--port 3000] [--host 127.0.0.1]
|
|
57
|
+
|
|
58
|
+
Environment:
|
|
59
|
+
SUPERSENDTX_API_KEY Required for stdio (Bearer stx_\u2026 for HTTP)
|
|
60
|
+
SUPERSENDTX_API_URL Optional API base (default https://api.supersendtx.com)
|
|
61
|
+
|
|
62
|
+
HTTP auth:
|
|
63
|
+
Authorization: Bearer stx_\u2026 (API key) or MCP OAuth access token
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
function isSuperSendTxApiKey(token) {
|
|
67
|
+
return /^stx_[a-f0-9]{48}$/i.test(token.trim());
|
|
68
|
+
}
|
|
69
|
+
function parseBearerToken(authorization) {
|
|
70
|
+
if (!authorization) return null;
|
|
71
|
+
const match = /^Bearer\s+(\S+)/i.exec(authorization.trim());
|
|
72
|
+
return match?.[1] || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/http.ts
|
|
76
|
+
import { createServer } from "http";
|
|
77
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
78
|
+
|
|
3
79
|
// src/server.ts
|
|
4
80
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
81
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -13,20 +89,75 @@ function textResult(value, isError = false) {
|
|
|
13
89
|
...isError ? { isError: true } : {}
|
|
14
90
|
};
|
|
15
91
|
}
|
|
92
|
+
function optString(args, key) {
|
|
93
|
+
if (args[key] == null) return void 0;
|
|
94
|
+
const value = String(args[key]).trim();
|
|
95
|
+
return value || void 0;
|
|
96
|
+
}
|
|
97
|
+
function optNumber(args, key) {
|
|
98
|
+
if (args[key] == null || args[key] === "") return void 0;
|
|
99
|
+
const n = Number(args[key]);
|
|
100
|
+
return Number.isFinite(n) ? n : void 0;
|
|
101
|
+
}
|
|
102
|
+
function optBoolean(args, key) {
|
|
103
|
+
if (args[key] == null) return void 0;
|
|
104
|
+
if (typeof args[key] === "boolean") return args[key];
|
|
105
|
+
const raw = String(args[key]).trim().toLowerCase();
|
|
106
|
+
if (raw === "true" || raw === "1") return true;
|
|
107
|
+
if (raw === "false" || raw === "0") return false;
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
function optStringArray(args, key) {
|
|
111
|
+
const value = args[key];
|
|
112
|
+
if (value == null) return void 0;
|
|
113
|
+
if (Array.isArray(value)) return value.map((v) => String(v));
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
var EMAIL_WEBHOOK_EVENTS = /* @__PURE__ */ new Set([
|
|
117
|
+
"email.received",
|
|
118
|
+
"email.sent",
|
|
119
|
+
"email.delivered",
|
|
120
|
+
"email.delivery_delayed",
|
|
121
|
+
"email.bounced",
|
|
122
|
+
"email.complained",
|
|
123
|
+
"email.opened",
|
|
124
|
+
"email.clicked",
|
|
125
|
+
"email.failed",
|
|
126
|
+
"email.suppressed",
|
|
127
|
+
"email.scheduled"
|
|
128
|
+
]);
|
|
16
129
|
async function callMcpTool(client, name, args) {
|
|
17
130
|
try {
|
|
18
131
|
switch (name) {
|
|
19
132
|
case "send_email": {
|
|
133
|
+
const from = optString(args, "from");
|
|
134
|
+
const to = optString(args, "to");
|
|
135
|
+
const subject = optString(args, "subject");
|
|
136
|
+
if (!from) return textResult("Missing required argument: from", true);
|
|
137
|
+
if (!to) return textResult("Missing required argument: to", true);
|
|
138
|
+
if (!subject) return textResult("Missing required argument: subject", true);
|
|
20
139
|
const result = await client.emails.send({
|
|
21
|
-
from
|
|
22
|
-
to
|
|
23
|
-
subject
|
|
24
|
-
html: args
|
|
25
|
-
text: args
|
|
26
|
-
reply_to: args
|
|
140
|
+
from,
|
|
141
|
+
to,
|
|
142
|
+
subject,
|
|
143
|
+
html: optString(args, "html"),
|
|
144
|
+
text: optString(args, "text"),
|
|
145
|
+
reply_to: optString(args, "reply_to")
|
|
27
146
|
});
|
|
28
147
|
return textResult(result);
|
|
29
148
|
}
|
|
149
|
+
case "list_emails": {
|
|
150
|
+
const result = await client.emails.list({
|
|
151
|
+
limit: optNumber(args, "limit"),
|
|
152
|
+
cursor: optString(args, "cursor")
|
|
153
|
+
});
|
|
154
|
+
return textResult(result);
|
|
155
|
+
}
|
|
156
|
+
case "get_email": {
|
|
157
|
+
const id = optString(args, "id");
|
|
158
|
+
if (!id) return textResult("Missing required argument: id", true);
|
|
159
|
+
return textResult(await client.emails.get(id));
|
|
160
|
+
}
|
|
30
161
|
case "apply_domain_dns": {
|
|
31
162
|
const domain = String(args.domain ?? "").trim();
|
|
32
163
|
if (!domain) {
|
|
@@ -47,9 +178,110 @@ async function callMcpTool(client, name, args) {
|
|
|
47
178
|
return textResult(result);
|
|
48
179
|
}
|
|
49
180
|
case "verify_domain": {
|
|
50
|
-
const
|
|
181
|
+
const domain = optString(args, "domain");
|
|
182
|
+
if (!domain) return textResult("Missing required argument: domain", true);
|
|
183
|
+
const result = await client.domains.verify(domain);
|
|
184
|
+
return textResult(result);
|
|
185
|
+
}
|
|
186
|
+
case "list_domains": {
|
|
187
|
+
const result = await client.domains.list({
|
|
188
|
+
limit: optNumber(args, "limit"),
|
|
189
|
+
cursor: optString(args, "cursor")
|
|
190
|
+
});
|
|
51
191
|
return textResult(result);
|
|
52
192
|
}
|
|
193
|
+
case "create_domain": {
|
|
194
|
+
const domainName = optString(args, "name");
|
|
195
|
+
if (!domainName) return textResult("Missing required argument: name", true);
|
|
196
|
+
const inbound = optBoolean(args, "inbound_enabled");
|
|
197
|
+
return textResult(
|
|
198
|
+
await client.domains.create({
|
|
199
|
+
name: domainName,
|
|
200
|
+
...inbound !== void 0 ? { inbound_enabled: inbound } : {}
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
case "list_webhooks": {
|
|
205
|
+
return textResult(
|
|
206
|
+
await client.webhooks.list({
|
|
207
|
+
limit: optNumber(args, "limit"),
|
|
208
|
+
cursor: optString(args, "cursor")
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
case "create_webhook": {
|
|
213
|
+
const url = optString(args, "url");
|
|
214
|
+
if (!url) return textResult("Missing required argument: url", true);
|
|
215
|
+
const events = optStringArray(args, "events");
|
|
216
|
+
return textResult(
|
|
217
|
+
await client.webhooks.create({
|
|
218
|
+
url,
|
|
219
|
+
...events ? { events } : {}
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
case "delete_webhook": {
|
|
224
|
+
const id = optString(args, "id");
|
|
225
|
+
if (!id) return textResult("Missing required argument: id", true);
|
|
226
|
+
return textResult(await client.webhooks.delete(id));
|
|
227
|
+
}
|
|
228
|
+
case "list_suppressions": {
|
|
229
|
+
return textResult(
|
|
230
|
+
await client.suppressions.list({
|
|
231
|
+
limit: optNumber(args, "limit"),
|
|
232
|
+
cursor: optString(args, "cursor"),
|
|
233
|
+
email: optString(args, "email")
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
case "add_suppression": {
|
|
238
|
+
const email = optString(args, "email");
|
|
239
|
+
if (!email) return textResult("Missing required argument: email", true);
|
|
240
|
+
return textResult(
|
|
241
|
+
await client.suppressions.create({
|
|
242
|
+
email,
|
|
243
|
+
reason: optString(args, "reason")
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
case "remove_suppression": {
|
|
248
|
+
const idOrEmail = optString(args, "id") ?? optString(args, "email");
|
|
249
|
+
if (!idOrEmail) {
|
|
250
|
+
return textResult("Missing required argument: id or email", true);
|
|
251
|
+
}
|
|
252
|
+
return textResult(await client.suppressions.remove(idOrEmail));
|
|
253
|
+
}
|
|
254
|
+
case "list_templates": {
|
|
255
|
+
const status = optString(args, "status");
|
|
256
|
+
return textResult(
|
|
257
|
+
await client.templates.list({
|
|
258
|
+
limit: optNumber(args, "limit"),
|
|
259
|
+
cursor: optString(args, "cursor"),
|
|
260
|
+
...status === "draft" || status === "published" ? { status } : {}
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
case "get_template": {
|
|
265
|
+
const idOrAlias = optString(args, "id") ?? optString(args, "alias");
|
|
266
|
+
if (!idOrAlias) return textResult("Missing required argument: id or alias", true);
|
|
267
|
+
return textResult(await client.templates.get(idOrAlias));
|
|
268
|
+
}
|
|
269
|
+
case "send_test_webhook_event": {
|
|
270
|
+
const event = optString(args, "event");
|
|
271
|
+
if (!event || !EMAIL_WEBHOOK_EVENTS.has(event)) {
|
|
272
|
+
return textResult(
|
|
273
|
+
`Missing or invalid event. Use one of: ${[...EMAIL_WEBHOOK_EVENTS].join(", ")}`,
|
|
274
|
+
true
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return textResult(
|
|
278
|
+
await client.emails.testWebhook({
|
|
279
|
+
event,
|
|
280
|
+
email_id: optString(args, "email_id"),
|
|
281
|
+
deliver: optBoolean(args, "deliver")
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
}
|
|
53
285
|
case "get_deliverability": {
|
|
54
286
|
const window = args.window === "7d" ? "7d" : "30d";
|
|
55
287
|
const result = await client.emails.insights(window);
|
|
@@ -63,6 +295,10 @@ async function callMcpTool(client, name, args) {
|
|
|
63
295
|
return textResult(message, true);
|
|
64
296
|
}
|
|
65
297
|
}
|
|
298
|
+
var paginationProps = {
|
|
299
|
+
limit: { type: "number", description: "Page size (1\u2013100)" },
|
|
300
|
+
cursor: { type: "string", description: "Opaque cursor for the next page" }
|
|
301
|
+
};
|
|
66
302
|
var MCP_TOOLS = [
|
|
67
303
|
{
|
|
68
304
|
name: "send_email",
|
|
@@ -80,6 +316,45 @@ var MCP_TOOLS = [
|
|
|
80
316
|
required: ["from", "to", "subject"]
|
|
81
317
|
}
|
|
82
318
|
},
|
|
319
|
+
{
|
|
320
|
+
name: "list_emails",
|
|
321
|
+
description: "List recent emails sent from this account.",
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: { ...paginationProps }
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: "get_email",
|
|
329
|
+
description: "Get a single email by public id (msg_\u2026).",
|
|
330
|
+
inputSchema: {
|
|
331
|
+
type: "object",
|
|
332
|
+
properties: {
|
|
333
|
+
id: { type: "string", description: "Email id (msg_\u2026)" }
|
|
334
|
+
},
|
|
335
|
+
required: ["id"]
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "list_domains",
|
|
340
|
+
description: "List sending domains for this account.",
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: "object",
|
|
343
|
+
properties: { ...paginationProps }
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "create_domain",
|
|
348
|
+
description: "Add a sending domain. DNS must still be applied and verified before production sends.",
|
|
349
|
+
inputSchema: {
|
|
350
|
+
type: "object",
|
|
351
|
+
properties: {
|
|
352
|
+
name: { type: "string", description: "Domain name (example.com)" },
|
|
353
|
+
inbound_enabled: { type: "boolean", description: "Enable inbound receiving on this domain" }
|
|
354
|
+
},
|
|
355
|
+
required: ["name"]
|
|
356
|
+
}
|
|
357
|
+
},
|
|
83
358
|
{
|
|
84
359
|
name: "apply_domain_dns",
|
|
85
360
|
description: "Apply SuperSend TX DNS records for a domain. Cloudflare can use a token stored in the dashboard; GoDaddy accepts one-time credentials on the request.",
|
|
@@ -107,6 +382,117 @@ var MCP_TOOLS = [
|
|
|
107
382
|
required: ["domain"]
|
|
108
383
|
}
|
|
109
384
|
},
|
|
385
|
+
{
|
|
386
|
+
name: "list_webhooks",
|
|
387
|
+
description: "List webhook endpoints for this account.",
|
|
388
|
+
inputSchema: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: { ...paginationProps }
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "create_webhook",
|
|
395
|
+
description: "Create a webhook endpoint. Returns the endpoint plus a signing secret (shown once).",
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: "object",
|
|
398
|
+
properties: {
|
|
399
|
+
url: { type: "string", description: "HTTPS URL that receives events" },
|
|
400
|
+
events: {
|
|
401
|
+
type: "array",
|
|
402
|
+
items: { type: "string" },
|
|
403
|
+
description: "Optional event types (defaults apply when omitted)"
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
required: ["url"]
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: "delete_webhook",
|
|
411
|
+
description: "Delete a webhook endpoint by id.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: "object",
|
|
414
|
+
properties: {
|
|
415
|
+
id: { type: "string", description: "Webhook id (wh_\u2026)" }
|
|
416
|
+
},
|
|
417
|
+
required: ["id"]
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: "list_suppressions",
|
|
422
|
+
description: "List suppressed recipient addresses.",
|
|
423
|
+
inputSchema: {
|
|
424
|
+
type: "object",
|
|
425
|
+
properties: {
|
|
426
|
+
...paginationProps,
|
|
427
|
+
email: { type: "string", description: "Optional filter by email" }
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "add_suppression",
|
|
433
|
+
description: "Add an address to the suppression list.",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {
|
|
437
|
+
email: { type: "string" },
|
|
438
|
+
reason: { type: "string" }
|
|
439
|
+
},
|
|
440
|
+
required: ["email"]
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: "remove_suppression",
|
|
445
|
+
description: "Remove a suppression by id or email address.",
|
|
446
|
+
inputSchema: {
|
|
447
|
+
type: "object",
|
|
448
|
+
properties: {
|
|
449
|
+
id: { type: "string", description: "Suppression id" },
|
|
450
|
+
email: { type: "string", description: "Email address (alternative to id)" }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "list_templates",
|
|
456
|
+
description: "List email templates.",
|
|
457
|
+
inputSchema: {
|
|
458
|
+
type: "object",
|
|
459
|
+
properties: {
|
|
460
|
+
...paginationProps,
|
|
461
|
+
status: { type: "string", enum: ["draft", "published"] }
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "get_template",
|
|
467
|
+
description: "Get a template by id or alias.",
|
|
468
|
+
inputSchema: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
id: { type: "string", description: "Template id (tpl_\u2026)" },
|
|
472
|
+
alias: { type: "string", description: "Published alias (alternative to id)" }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "send_test_webhook_event",
|
|
478
|
+
description: "Simulate a webhook event (POST /emails/test) for CI or local webhook testing. Optionally deliver to subscribed endpoints.",
|
|
479
|
+
inputSchema: {
|
|
480
|
+
type: "object",
|
|
481
|
+
properties: {
|
|
482
|
+
event: {
|
|
483
|
+
type: "string",
|
|
484
|
+
enum: [...EMAIL_WEBHOOK_EVENTS],
|
|
485
|
+
description: "Webhook event type to simulate"
|
|
486
|
+
},
|
|
487
|
+
email_id: { type: "string", description: "Optional existing email id (msg_\u2026)" },
|
|
488
|
+
deliver: {
|
|
489
|
+
type: "boolean",
|
|
490
|
+
description: "When true (default), enqueue delivery to account webhooks"
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
required: ["event"]
|
|
494
|
+
}
|
|
495
|
+
},
|
|
110
496
|
{
|
|
111
497
|
name: "get_deliverability",
|
|
112
498
|
description: "Fetch best-effort deliverability metrics for recent sends (7d or 30d window).",
|
|
@@ -133,7 +519,7 @@ function resolveApiKey() {
|
|
|
133
519
|
function createMcpServer(apiKey, baseUrl) {
|
|
134
520
|
const client = new SuperSendTX(apiKey, baseUrl ? { baseUrl } : {});
|
|
135
521
|
const server = new Server(
|
|
136
|
-
{ name: "supersendtx-mcp", version: "0.
|
|
522
|
+
{ name: "supersendtx-mcp", version: "0.3.0" },
|
|
137
523
|
{ capabilities: { tools: {} } }
|
|
138
524
|
);
|
|
139
525
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -157,8 +543,153 @@ async function runStdioServer() {
|
|
|
157
543
|
await server.connect(transport);
|
|
158
544
|
}
|
|
159
545
|
|
|
546
|
+
// src/oauth-metadata.ts
|
|
547
|
+
var MCP_HTTP_PATH = "/mcp";
|
|
548
|
+
function buildProtectedResourceMetadata(origin) {
|
|
549
|
+
const resource = `${origin.replace(/\/$/, "")}${MCP_HTTP_PATH}`;
|
|
550
|
+
const appOrigin = process.env.SUPERSENDTX_OAUTH_ISSUER?.trim() || process.env.SUPERSENDTX_APP_URL?.trim() || "https://app.supersendtx.com";
|
|
551
|
+
return {
|
|
552
|
+
resource,
|
|
553
|
+
authorization_servers: [appOrigin.replace(/\/$/, "")],
|
|
554
|
+
scopes_supported: ["full"],
|
|
555
|
+
bearer_methods_supported: ["header"]
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function buildWwwAuthenticateChallenge(origin) {
|
|
559
|
+
const metadataUrl = `${origin.replace(/\/$/, "")}/.well-known/oauth-protected-resource`;
|
|
560
|
+
return `Bearer error="invalid_token", error_description="OAuth authentication required", resource_metadata="${metadataUrl}"`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/oauth-resolve.ts
|
|
564
|
+
async function resolveOAuthAccessToken(accessToken, baseUrl) {
|
|
565
|
+
const apiBase = (baseUrl || process.env.SUPERSENDTX_API_URL || "https://api.supersendtx.com").replace(
|
|
566
|
+
/\/$/,
|
|
567
|
+
""
|
|
568
|
+
);
|
|
569
|
+
const resolveSecret = process.env.SUPERSENDTX_MCP_RESOLVE_SECRET?.trim() || process.env.SUPERSENDTX_CREDENTIALS_ENCRYPTION_KEY?.trim() || (process.env.NODE_ENV === "production" ? "" : "supersendtx-dev-mcp-resolve");
|
|
570
|
+
if (!resolveSecret) return null;
|
|
571
|
+
const response = await fetch(`${apiBase}/mcp/oauth/resolve`, {
|
|
572
|
+
method: "POST",
|
|
573
|
+
headers: {
|
|
574
|
+
authorization: `Bearer ${accessToken}`,
|
|
575
|
+
"content-type": "application/json",
|
|
576
|
+
"x-supersendtx-mcp-resolve": resolveSecret
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
if (!response.ok) return null;
|
|
580
|
+
const data = await response.json().catch(() => null);
|
|
581
|
+
const apiKey = data?.apiKey?.trim();
|
|
582
|
+
return apiKey || null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/http.ts
|
|
586
|
+
function sendJson(res, status, body, headers = {}) {
|
|
587
|
+
const payload = JSON.stringify(body);
|
|
588
|
+
res.writeHead(status, {
|
|
589
|
+
"content-type": "application/json; charset=utf-8",
|
|
590
|
+
"content-length": Buffer.byteLength(payload),
|
|
591
|
+
...headers
|
|
592
|
+
});
|
|
593
|
+
res.end(payload);
|
|
594
|
+
}
|
|
595
|
+
function getRequestOrigin(req) {
|
|
596
|
+
const hostHeader = req.headers.host || "127.0.0.1";
|
|
597
|
+
return `http://${hostHeader}`;
|
|
598
|
+
}
|
|
599
|
+
async function resolveApiKeyFromAuthorization(authorization, baseUrl) {
|
|
600
|
+
const bearer = parseBearerToken(authorization);
|
|
601
|
+
if (!bearer) return null;
|
|
602
|
+
if (isSuperSendTxApiKey(bearer)) return bearer;
|
|
603
|
+
return resolveOAuthAccessToken(bearer, baseUrl);
|
|
604
|
+
}
|
|
605
|
+
async function handleHttpRequest(req, res, options = {}) {
|
|
606
|
+
const origin = getRequestOrigin(req);
|
|
607
|
+
const url = new URL(req.url || "/", origin);
|
|
608
|
+
if (req.method === "OPTIONS") {
|
|
609
|
+
res.writeHead(204, {
|
|
610
|
+
"access-control-allow-origin": "*",
|
|
611
|
+
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
612
|
+
"access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id, X-Supersendtx-Mcp-Resolve"
|
|
613
|
+
});
|
|
614
|
+
res.end();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (url.pathname === "/health") {
|
|
618
|
+
sendJson(res, 200, { ok: true, service: "supersendtx-mcp" });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (url.pathname === "/.well-known/oauth-protected-resource") {
|
|
622
|
+
sendJson(res, 200, buildProtectedResourceMetadata(origin), {
|
|
623
|
+
"cache-control": "public, max-age=300, s-maxage=3600"
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (url.pathname !== "/mcp" && url.pathname !== "/") {
|
|
628
|
+
sendJson(res, 404, { error: "Not found. MCP endpoint is POST/GET /mcp" });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const apiKey = await resolveApiKeyFromAuthorization(
|
|
632
|
+
typeof req.headers.authorization === "string" ? req.headers.authorization : void 0,
|
|
633
|
+
options.baseUrl
|
|
634
|
+
);
|
|
635
|
+
if (!apiKey) {
|
|
636
|
+
sendJson(
|
|
637
|
+
res,
|
|
638
|
+
401,
|
|
639
|
+
{ error: "Missing or invalid Authorization. Use Bearer stx_\u2026 or complete MCP OAuth." },
|
|
640
|
+
{ "www-authenticate": buildWwwAuthenticateChallenge(origin) }
|
|
641
|
+
);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const server = createMcpServer(apiKey, options.baseUrl);
|
|
645
|
+
const transport = new StreamableHTTPServerTransport({
|
|
646
|
+
sessionIdGenerator: void 0
|
|
647
|
+
});
|
|
648
|
+
try {
|
|
649
|
+
await server.connect(transport);
|
|
650
|
+
await transport.handleRequest(req, res);
|
|
651
|
+
} finally {
|
|
652
|
+
await transport.close().catch(() => void 0);
|
|
653
|
+
await server.close().catch(() => void 0);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function runHttpServer(options) {
|
|
657
|
+
const host = options.host ?? "127.0.0.1";
|
|
658
|
+
const port = options.port;
|
|
659
|
+
const baseUrl = process.env.SUPERSENDTX_API_URL?.trim() || void 0;
|
|
660
|
+
const httpServer = createServer((req, res) => {
|
|
661
|
+
void handleHttpRequest(req, res, { baseUrl }).catch((error) => {
|
|
662
|
+
if (!res.headersSent) {
|
|
663
|
+
sendJson(res, 500, {
|
|
664
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
665
|
+
});
|
|
666
|
+
} else {
|
|
667
|
+
res.end();
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
await new Promise((resolve, reject) => {
|
|
672
|
+
httpServer.once("error", reject);
|
|
673
|
+
httpServer.listen(port, host, () => resolve());
|
|
674
|
+
});
|
|
675
|
+
console.error(`SuperSend TX MCP HTTP listening on http://${host}:${port}/mcp`);
|
|
676
|
+
console.error("Authenticate with Bearer stx_\u2026 or MCP OAuth access token");
|
|
677
|
+
}
|
|
678
|
+
|
|
160
679
|
// src/index.ts
|
|
161
|
-
|
|
680
|
+
async function main() {
|
|
681
|
+
const options = parseCliArgs(process.argv);
|
|
682
|
+
if (options.mode === "help") {
|
|
683
|
+
printHelp();
|
|
684
|
+
process.exit(0);
|
|
685
|
+
}
|
|
686
|
+
if (options.mode === "http") {
|
|
687
|
+
await runHttpServer({ port: options.port, host: options.host });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
await runStdioServer();
|
|
691
|
+
}
|
|
692
|
+
main().catch((error) => {
|
|
162
693
|
console.error(error instanceof Error ? error.message : error);
|
|
163
694
|
process.exit(1);
|
|
164
695
|
});
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supersendtx-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "SuperSend TX MCP server —
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "SuperSend TX MCP server — stdio or local HTTP (--http) for Cursor and Claude",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"supersendtx-mcp": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"smithery.yaml"
|
|
12
14
|
],
|
|
13
15
|
"repository": {
|
|
14
16
|
"type": "git",
|
|
@@ -38,7 +40,7 @@
|
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
42
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
41
|
-
"supersendtx": "0.8.
|
|
43
|
+
"supersendtx": "0.8.2"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"tsup": "^8.5.0",
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Smithery configuration for supersendtx-mcp
|
|
2
|
+
# https://smithery.ai — submit/list after publish: npx @smithery/cli publish (or dashboard)
|
|
3
|
+
|
|
4
|
+
startCommand:
|
|
5
|
+
type: stdio
|
|
6
|
+
configSchema:
|
|
7
|
+
type: object
|
|
8
|
+
required: [apiKey]
|
|
9
|
+
properties:
|
|
10
|
+
apiKey:
|
|
11
|
+
type: string
|
|
12
|
+
title: SuperSend TX API Key
|
|
13
|
+
description: API key starting with stx_
|
|
14
|
+
apiUrl:
|
|
15
|
+
type: string
|
|
16
|
+
title: API Base URL
|
|
17
|
+
description: Optional override (default https://api.supersendtx.com)
|
|
18
|
+
commandFunction: |-
|
|
19
|
+
(config) => ({
|
|
20
|
+
command: 'npx',
|
|
21
|
+
args: ['-y', 'supersendtx-mcp'],
|
|
22
|
+
env: {
|
|
23
|
+
SUPERSENDTX_API_KEY: config.apiKey,
|
|
24
|
+
...(config.apiUrl ? { SUPERSENDTX_API_URL: config.apiUrl } : {}),
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
exampleConfig:
|
|
29
|
+
apiKey: stx_your_key_here
|