groundmail-mcp 0.5.5 → 0.5.7
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 +11 -8
- package/dist/index.js +99 -20
- package/package.json +4 -3
- package/smithery.yaml +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# groundmail-mcp
|
|
2
2
|
|
|
3
|
-
MCP server for [Groundmail](https://groundmail.dev)
|
|
3
|
+
MCP server for [Groundmail](https://groundmail.dev): send email and manage domains, webhooks, suppressions and templates from Claude, Claude Code, ChatGPT, Cursor and other MCP clients.
|
|
4
4
|
|
|
5
5
|
**Install:** `npx -y groundmail-mcp` · **Docs:** https://docs.groundmail.dev/ai/mcp
|
|
6
6
|
|
|
@@ -78,17 +78,20 @@ npx -y groundmail-mcp --http --port 3000
|
|
|
78
78
|
## Claude Code
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
#
|
|
82
|
-
claude mcp add --transport
|
|
81
|
+
# Hosted, OAuth: then run /mcp, pick groundmail, Authenticate
|
|
82
|
+
claude mcp add --transport http groundmail https://mcp.groundmail.dev/mcp
|
|
83
83
|
|
|
84
|
-
#
|
|
85
|
-
claude mcp add --transport http groundmail
|
|
84
|
+
# Hosted, API key
|
|
85
|
+
claude mcp add --transport http groundmail https://mcp.groundmail.dev/mcp \
|
|
86
|
+
--header "Authorization: Bearer $GROUNDMAIL_API_KEY"
|
|
86
87
|
|
|
87
|
-
#
|
|
88
|
-
claude mcp add --
|
|
88
|
+
# Local stdio
|
|
89
|
+
claude mcp add groundmail --env GROUNDMAIL_API_KEY=gm_your_key_here -- npx -y groundmail-mcp
|
|
89
90
|
```
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
## Claude and ChatGPT
|
|
93
|
+
|
|
94
|
+
Add a custom connector with the URL `https://mcp.groundmail.dev/mcp` and sign in to Groundmail when prompted (ChatGPT needs developer mode). Step-by-step: https://docs.groundmail.dev/ai/mcp
|
|
92
95
|
|
|
93
96
|
---
|
|
94
97
|
|
package/dist/index.js
CHANGED
|
@@ -318,10 +318,36 @@ async function callMcpTool(client, name, args) {
|
|
|
318
318
|
return textResult(`Unknown tool: ${name}`, true);
|
|
319
319
|
}
|
|
320
320
|
} catch (error) {
|
|
321
|
-
|
|
322
|
-
return textResult(message, true);
|
|
321
|
+
return textResult(formatToolError(error), true);
|
|
323
322
|
}
|
|
324
323
|
}
|
|
324
|
+
function formatToolError(error) {
|
|
325
|
+
if (!error || typeof error !== "object") return "Request failed";
|
|
326
|
+
const err = error;
|
|
327
|
+
const text = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
328
|
+
const lines = [text(err.message) ?? "Request failed"];
|
|
329
|
+
const code = typeof err.code === "string" && /^[a-z_]+$/.test(err.code) ? err.code : null;
|
|
330
|
+
if (code) {
|
|
331
|
+
const status = typeof err.status === "number" ? ` (HTTP ${err.status})` : "";
|
|
332
|
+
lines.push(`code: ${code}${status}`);
|
|
333
|
+
}
|
|
334
|
+
const requiredScope = text(err.requiredScope);
|
|
335
|
+
if (requiredScope) lines.push(`required scope: ${requiredScope}`);
|
|
336
|
+
if (err.details !== void 0 && err.details !== null) {
|
|
337
|
+
lines.push(`details: ${JSON.stringify(err.details)}`);
|
|
338
|
+
}
|
|
339
|
+
const upgradeUrl = text(err.upgradeUrl);
|
|
340
|
+
if (upgradeUrl) {
|
|
341
|
+
lines.push(
|
|
342
|
+
`upgrade: ${/^https?:\/\//.test(upgradeUrl) ? upgradeUrl : `https://app.groundmail.dev${upgradeUrl.startsWith("/") ? "" : "/"}${upgradeUrl}`}`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const hint = text(err.hint);
|
|
346
|
+
if (hint) lines.push(`hint: ${hint}`);
|
|
347
|
+
const docsUrl = text(err.docsUrl) ?? (code ? `https://docs.groundmail.dev/errors#${code}` : null);
|
|
348
|
+
if (docsUrl) lines.push(`how to fix: ${docsUrl}`);
|
|
349
|
+
return lines.join("\n");
|
|
350
|
+
}
|
|
325
351
|
var paginationProps = {
|
|
326
352
|
limit: { type: "number", description: "Page size (1\u2013100)" },
|
|
327
353
|
cursor: { type: "string", description: "Opaque cursor for the next page" }
|
|
@@ -577,11 +603,19 @@ function resolveApiKey() {
|
|
|
577
603
|
}
|
|
578
604
|
|
|
579
605
|
// src/server.ts
|
|
606
|
+
var MCP_SERVER_VERSION = "0.5.7";
|
|
607
|
+
var MCP_SERVER_INSTRUCTIONS = [
|
|
608
|
+
"Groundmail sends transactional email from domains the user has verified.",
|
|
609
|
+
'To test before a domain is verified, call send_email with from "noreply@mail.groundmail.dev" and to set to the Groundmail account email; sandbox sends to any other address are rejected.',
|
|
610
|
+
"Sending to other recipients needs a verified domain (list_domains, create_domain, apply_domain_dns, verify_domain) and a payment method on the account (no charge on the Free plan).",
|
|
611
|
+
"Confirm with the user before sending email to anyone other than themselves.",
|
|
612
|
+
"Errors include a stable code and a link to https://docs.groundmail.dev/errors#<code> that says how to fix them."
|
|
613
|
+
].join(" ");
|
|
580
614
|
function createMcpServer(apiKey, baseUrl) {
|
|
581
615
|
const client = new Groundmail(apiKey, baseUrl ? { baseUrl } : {});
|
|
582
616
|
const server = new Server(
|
|
583
|
-
{ name: "groundmail-mcp",
|
|
584
|
-
{ capabilities: { tools: {} } }
|
|
617
|
+
{ name: "groundmail-mcp", title: "Groundmail", version: MCP_SERVER_VERSION },
|
|
618
|
+
{ capabilities: { tools: {} }, instructions: MCP_SERVER_INSTRUCTIONS }
|
|
585
619
|
);
|
|
586
620
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
587
621
|
tools: MCP_TOOLS.map((tool) => ({
|
|
@@ -606,18 +640,43 @@ async function runStdioServer() {
|
|
|
606
640
|
|
|
607
641
|
// src/oauth-metadata.ts
|
|
608
642
|
var MCP_HTTP_PATH = "/mcp";
|
|
643
|
+
var PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
644
|
+
var PROTECTED_RESOURCE_METADATA_PATHS = /* @__PURE__ */ new Set([
|
|
645
|
+
PROTECTED_RESOURCE_METADATA_PATH,
|
|
646
|
+
`${PROTECTED_RESOURCE_METADATA_PATH}${MCP_HTTP_PATH}`
|
|
647
|
+
]);
|
|
648
|
+
var DOCS_URL = "https://docs.groundmail.dev/ai/mcp";
|
|
649
|
+
var MCP_SCOPES_SUPPORTED = [
|
|
650
|
+
"full",
|
|
651
|
+
"emails:send",
|
|
652
|
+
"emails:read",
|
|
653
|
+
"domains:read",
|
|
654
|
+
"domains:write",
|
|
655
|
+
"webhooks:write",
|
|
656
|
+
"templates:write",
|
|
657
|
+
"suppressions:write",
|
|
658
|
+
"account:read"
|
|
659
|
+
];
|
|
660
|
+
function trimOrigin(origin) {
|
|
661
|
+
return origin.replace(/\/$/, "");
|
|
662
|
+
}
|
|
609
663
|
function buildProtectedResourceMetadata(origin) {
|
|
610
|
-
const resource = `${origin
|
|
664
|
+
const resource = `${trimOrigin(origin)}${MCP_HTTP_PATH}`;
|
|
611
665
|
const appOrigin = process.env.GROUNDMAIL_OAUTH_ISSUER?.trim() || process.env.GROUNDMAIL_APP_URL?.trim() || "https://app.groundmail.dev";
|
|
612
666
|
return {
|
|
613
667
|
resource,
|
|
668
|
+
resource_name: "Groundmail",
|
|
669
|
+
resource_documentation: DOCS_URL,
|
|
614
670
|
authorization_servers: [appOrigin.replace(/\/$/, "")],
|
|
615
|
-
scopes_supported: [
|
|
671
|
+
scopes_supported: [...MCP_SCOPES_SUPPORTED],
|
|
616
672
|
bearer_methods_supported: ["header"]
|
|
617
673
|
};
|
|
618
674
|
}
|
|
619
|
-
function buildWwwAuthenticateChallenge(origin) {
|
|
620
|
-
const metadataUrl = `${origin
|
|
675
|
+
function buildWwwAuthenticateChallenge(origin, options = {}) {
|
|
676
|
+
const metadataUrl = `${trimOrigin(origin)}${PROTECTED_RESOURCE_METADATA_PATH}`;
|
|
677
|
+
if (options.invalidToken === false) {
|
|
678
|
+
return `Bearer resource_metadata="${metadataUrl}"`;
|
|
679
|
+
}
|
|
621
680
|
return `Bearer error="invalid_token", error_description="OAuth authentication required", resource_metadata="${metadataUrl}"`;
|
|
622
681
|
}
|
|
623
682
|
|
|
@@ -644,15 +703,27 @@ async function resolveOAuthAccessToken(accessToken) {
|
|
|
644
703
|
}
|
|
645
704
|
|
|
646
705
|
// src/http.ts
|
|
706
|
+
var CORS_HEADERS = {
|
|
707
|
+
"access-control-allow-origin": "*",
|
|
708
|
+
"access-control-expose-headers": "WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version"
|
|
709
|
+
};
|
|
710
|
+
var CORS_PREFLIGHT_HEADERS = {
|
|
711
|
+
...CORS_HEADERS,
|
|
712
|
+
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
713
|
+
"access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, X-Groundmail-Mcp-Resolve",
|
|
714
|
+
"access-control-max-age": "86400"
|
|
715
|
+
};
|
|
647
716
|
function sendJson(res, status, body, headers = {}) {
|
|
648
717
|
const payload = JSON.stringify(body);
|
|
649
718
|
res.writeHead(status, {
|
|
650
719
|
"content-type": "application/json; charset=utf-8",
|
|
651
720
|
"content-length": Buffer.byteLength(payload),
|
|
721
|
+
...CORS_HEADERS,
|
|
652
722
|
...headers
|
|
653
723
|
});
|
|
654
724
|
res.end(payload);
|
|
655
725
|
}
|
|
726
|
+
var UNAUTHORIZED_HINT = "Connect with OAuth: add https://mcp.groundmail.dev/mcp to your MCP client with no headers and sign in when it opens a browser. For scripts, send Authorization: Bearer gm_\u2026 with an API key from https://app.groundmail.dev/api-keys.";
|
|
656
727
|
function firstHeaderValue(value) {
|
|
657
728
|
if (!value) return void 0;
|
|
658
729
|
const raw = Array.isArray(value) ? value[0] : value;
|
|
@@ -674,11 +745,7 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
674
745
|
const origin = getRequestOrigin(req);
|
|
675
746
|
const url = new URL(req.url || "/", origin);
|
|
676
747
|
if (req.method === "OPTIONS") {
|
|
677
|
-
res.writeHead(204,
|
|
678
|
-
"access-control-allow-origin": "*",
|
|
679
|
-
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
680
|
-
"access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id, X-Groundmail-Mcp-Resolve"
|
|
681
|
-
});
|
|
748
|
+
res.writeHead(204, CORS_PREFLIGHT_HEADERS);
|
|
682
749
|
res.end();
|
|
683
750
|
return;
|
|
684
751
|
}
|
|
@@ -686,7 +753,7 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
686
753
|
sendJson(res, 200, { ok: true, service: "groundmail-mcp" });
|
|
687
754
|
return;
|
|
688
755
|
}
|
|
689
|
-
if (url.pathname
|
|
756
|
+
if (PROTECTED_RESOURCE_METADATA_PATHS.has(url.pathname)) {
|
|
690
757
|
sendJson(res, 200, buildProtectedResourceMetadata(origin), {
|
|
691
758
|
"cache-control": "public, max-age=300, s-maxage=3600"
|
|
692
759
|
});
|
|
@@ -696,16 +763,25 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
696
763
|
sendJson(res, 404, { error: "Not found. MCP endpoint is POST/GET /mcp" });
|
|
697
764
|
return;
|
|
698
765
|
}
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
options.baseUrl
|
|
702
|
-
);
|
|
766
|
+
const authorization = typeof req.headers.authorization === "string" ? req.headers.authorization : void 0;
|
|
767
|
+
const apiKey = await resolveApiKeyFromAuthorization(authorization, options.baseUrl);
|
|
703
768
|
if (!apiKey) {
|
|
769
|
+
const message = "Missing or invalid Authorization. Use Bearer gm_\u2026 or complete MCP OAuth.";
|
|
704
770
|
sendJson(
|
|
705
771
|
res,
|
|
706
772
|
401,
|
|
707
|
-
{
|
|
708
|
-
|
|
773
|
+
{
|
|
774
|
+
error: message,
|
|
775
|
+
code: "unauthorized",
|
|
776
|
+
message,
|
|
777
|
+
hint: UNAUTHORIZED_HINT,
|
|
778
|
+
docs_url: "https://docs.groundmail.dev/errors#unauthorized"
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
"www-authenticate": buildWwwAuthenticateChallenge(origin, {
|
|
782
|
+
invalidToken: Boolean(parseBearerToken(authorization))
|
|
783
|
+
})
|
|
784
|
+
}
|
|
709
785
|
);
|
|
710
786
|
return;
|
|
711
787
|
}
|
|
@@ -713,6 +789,9 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
713
789
|
const transport = new StreamableHTTPServerTransport({
|
|
714
790
|
sessionIdGenerator: void 0
|
|
715
791
|
});
|
|
792
|
+
for (const [name, value] of Object.entries(CORS_HEADERS)) {
|
|
793
|
+
res.setHeader(name, value);
|
|
794
|
+
}
|
|
716
795
|
try {
|
|
717
796
|
await server.connect(transport);
|
|
718
797
|
await transport.handleRequest(req, res);
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groundmail-mcp",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.5.7",
|
|
4
|
+
"mcpName": "io.github.Groundmail/groundmail",
|
|
5
|
+
"description": "Groundmail MCP server: send transactional email, manage domains, templates, webhooks and suppressions from Claude, Cursor, ChatGPT and other MCP clients",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"bin": {
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
42
|
-
"groundmail": "0.14.
|
|
43
|
+
"groundmail": "0.14.2"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"tsup": "^8.5.0",
|
package/smithery.yaml
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Smithery configuration for groundmail-mcp
|
|
2
|
-
# https://smithery.ai
|
|
2
|
+
# Local stdio listing on https://smithery.ai. The hosted server (https://mcp.groundmail.dev/mcp)
|
|
3
|
+
# is listed separately by URL; Smithery handles its OAuth.
|
|
3
4
|
|
|
4
5
|
startCommand:
|
|
5
6
|
type: stdio
|