supersendtx-mcp 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +84 -19
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -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_…` |
12
12
  | **HTTP** (local) | `npx -y supersendtx-mcp --http --port 3000` | `Authorization: Bearer stx_…` on each request |
13
- | **HTTP** (hosted) | `https://mcp.supersendtx.com/mcp` | `Authorization: Bearer stx_…` on each request |
13
+ | **HTTP** (hosted) | `https://mcp.supersendtx.com/mcp` | **OAuth** (recommended) or `Authorization: Bearer stx_…` |
14
14
 
15
15
  Optional: `SUPERSENDTX_API_URL` (e.g. `http://localhost:3003/api` for local API).
16
16
 
17
17
  Local HTTP endpoint: `http://127.0.0.1:3000/mcp` · health: `GET /health`
18
18
  Hosted health: `GET https://mcp.supersendtx.com/health`
19
19
 
20
- OAuth for remote clients is planned separately; Bearer works on hosted today.
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
21
 
22
22
  ---
23
23
 
package/dist/index.js CHANGED
@@ -60,16 +60,16 @@ Environment:
60
60
  SUPERSENDTX_API_URL Optional API base (default https://api.supersendtx.com)
61
61
 
62
62
  HTTP auth:
63
- Authorization: Bearer stx_\u2026
63
+ Authorization: Bearer stx_\u2026 (API key) or MCP OAuth access token
64
64
  `);
65
65
  }
66
- function parseBearerApiKey(authorization) {
66
+ function isSuperSendTxApiKey(token) {
67
+ return /^stx_[a-f0-9]{48}$/i.test(token.trim());
68
+ }
69
+ function parseBearerToken(authorization) {
67
70
  if (!authorization) return null;
68
71
  const match = /^Bearer\s+(\S+)/i.exec(authorization.trim());
69
- if (!match) return null;
70
- const key = match[1];
71
- if (!key.startsWith("stx_")) return null;
72
- return key;
72
+ return match?.[1] || null;
73
73
  }
74
74
 
75
75
  // src/http.ts
@@ -543,23 +543,80 @@ async function runStdioServer() {
543
543
  await server.connect(transport);
544
544
  }
545
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
+
546
585
  // src/http.ts
547
- function sendJson(res, status, body) {
586
+ function sendJson(res, status, body, headers = {}) {
548
587
  const payload = JSON.stringify(body);
549
588
  res.writeHead(status, {
550
589
  "content-type": "application/json; charset=utf-8",
551
- "content-length": Buffer.byteLength(payload)
590
+ "content-length": Buffer.byteLength(payload),
591
+ ...headers
552
592
  });
553
593
  res.end(payload);
554
594
  }
595
+ function firstHeaderValue(value) {
596
+ if (!value) return void 0;
597
+ const raw = Array.isArray(value) ? value[0] : value;
598
+ const trimmed = raw?.split(",")[0]?.trim();
599
+ return trimmed || void 0;
600
+ }
601
+ function getRequestOrigin(req) {
602
+ const host = firstHeaderValue(req.headers["x-forwarded-host"]) || firstHeaderValue(req.headers.host) || "127.0.0.1";
603
+ const proto = firstHeaderValue(req.headers["x-forwarded-proto"]) || "http";
604
+ return `${proto}://${host}`;
605
+ }
606
+ async function resolveApiKeyFromAuthorization(authorization, baseUrl) {
607
+ const bearer = parseBearerToken(authorization);
608
+ if (!bearer) return null;
609
+ if (isSuperSendTxApiKey(bearer)) return bearer;
610
+ return resolveOAuthAccessToken(bearer, baseUrl);
611
+ }
555
612
  async function handleHttpRequest(req, res, options = {}) {
556
- const hostHeader = req.headers.host || "127.0.0.1";
557
- const url = new URL(req.url || "/", `http://${hostHeader}`);
613
+ const origin = getRequestOrigin(req);
614
+ const url = new URL(req.url || "/", origin);
558
615
  if (req.method === "OPTIONS") {
559
616
  res.writeHead(204, {
560
617
  "access-control-allow-origin": "*",
561
618
  "access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
562
- "access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id"
619
+ "access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id, X-Supersendtx-Mcp-Resolve"
563
620
  });
564
621
  res.end();
565
622
  return;
@@ -568,19 +625,27 @@ async function handleHttpRequest(req, res, options = {}) {
568
625
  sendJson(res, 200, { ok: true, service: "supersendtx-mcp" });
569
626
  return;
570
627
  }
628
+ if (url.pathname === "/.well-known/oauth-protected-resource") {
629
+ sendJson(res, 200, buildProtectedResourceMetadata(origin), {
630
+ "cache-control": "public, max-age=300, s-maxage=3600"
631
+ });
632
+ return;
633
+ }
571
634
  if (url.pathname !== "/mcp" && url.pathname !== "/") {
572
635
  sendJson(res, 404, { error: "Not found. MCP endpoint is POST/GET /mcp" });
573
636
  return;
574
637
  }
575
- const apiKey = parseBearerApiKey(
576
- typeof req.headers.authorization === "string" ? req.headers.authorization : void 0
638
+ const apiKey = await resolveApiKeyFromAuthorization(
639
+ typeof req.headers.authorization === "string" ? req.headers.authorization : void 0,
640
+ options.baseUrl
577
641
  );
578
642
  if (!apiKey) {
579
- res.writeHead(401, {
580
- "content-type": "application/json; charset=utf-8",
581
- "www-authenticate": "Bearer"
582
- });
583
- res.end(JSON.stringify({ error: "Missing or invalid Authorization: Bearer stx_\u2026 header" }));
643
+ sendJson(
644
+ res,
645
+ 401,
646
+ { error: "Missing or invalid Authorization. Use Bearer stx_\u2026 or complete MCP OAuth." },
647
+ { "www-authenticate": buildWwwAuthenticateChallenge(origin) }
648
+ );
584
649
  return;
585
650
  }
586
651
  const server = createMcpServer(apiKey, options.baseUrl);
@@ -615,7 +680,7 @@ async function runHttpServer(options) {
615
680
  httpServer.listen(port, host, () => resolve());
616
681
  });
617
682
  console.error(`SuperSend TX MCP HTTP listening on http://${host}:${port}/mcp`);
618
- console.error("Authenticate with: Authorization: Bearer stx_\u2026");
683
+ console.error("Authenticate with Bearer stx_\u2026 or MCP OAuth access token");
619
684
  }
620
685
 
621
686
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supersendtx-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "SuperSend TX MCP server — stdio or local HTTP (--http) 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.8.1"
43
+ "supersendtx": "0.8.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "tsup": "^8.5.0",