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.
- package/README.md +2 -2
- package/dist/index.js +84 -19
- 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_…`
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
557
|
-
const url = new URL(req.url || "/",
|
|
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 =
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
|
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
|
+
"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.
|
|
43
|
+
"supersendtx": "0.8.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"tsup": "^8.5.0",
|