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 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: String(args.from),
22
- to: String(args.to),
23
- subject: String(args.subject),
24
- html: args.html != null ? String(args.html) : void 0,
25
- text: args.text != null ? String(args.text) : void 0,
26
- reply_to: args.reply_to != null ? String(args.reply_to) : void 0
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 result = await client.domains.verify(String(args.domain));
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.1.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
- runStdioServer().catch((error) => {
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.1.0",
4
- "description": "SuperSend TX MCP server — send email and manage domains from Cursor or Claude",
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.0"
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