supersendtx-mcp 0.3.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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +77 -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,73 @@ 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
  }
555
- async function handleHttpRequest(req, res, options = {}) {
595
+ function getRequestOrigin(req) {
556
596
  const hostHeader = req.headers.host || "127.0.0.1";
557
- const url = new URL(req.url || "/", `http://${hostHeader}`);
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);
558
608
  if (req.method === "OPTIONS") {
559
609
  res.writeHead(204, {
560
610
  "access-control-allow-origin": "*",
561
611
  "access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
562
- "access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id"
612
+ "access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id, X-Supersendtx-Mcp-Resolve"
563
613
  });
564
614
  res.end();
565
615
  return;
@@ -568,19 +618,27 @@ async function handleHttpRequest(req, res, options = {}) {
568
618
  sendJson(res, 200, { ok: true, service: "supersendtx-mcp" });
569
619
  return;
570
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
+ }
571
627
  if (url.pathname !== "/mcp" && url.pathname !== "/") {
572
628
  sendJson(res, 404, { error: "Not found. MCP endpoint is POST/GET /mcp" });
573
629
  return;
574
630
  }
575
- const apiKey = parseBearerApiKey(
576
- typeof req.headers.authorization === "string" ? req.headers.authorization : void 0
631
+ const apiKey = await resolveApiKeyFromAuthorization(
632
+ typeof req.headers.authorization === "string" ? req.headers.authorization : void 0,
633
+ options.baseUrl
577
634
  );
578
635
  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" }));
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
+ );
584
642
  return;
585
643
  }
586
644
  const server = createMcpServer(apiKey, options.baseUrl);
@@ -615,7 +673,7 @@ async function runHttpServer(options) {
615
673
  httpServer.listen(port, host, () => resolve());
616
674
  });
617
675
  console.error(`SuperSend TX MCP HTTP listening on http://${host}:${port}/mcp`);
618
- console.error("Authenticate with: Authorization: Bearer stx_\u2026");
676
+ console.error("Authenticate with Bearer stx_\u2026 or MCP OAuth access token");
619
677
  }
620
678
 
621
679
  // 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.0",
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",