groundmail-mcp 0.5.5 → 0.5.6
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 +80 -19
- package/package.json +3 -2
- 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,29 @@ 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 lines = [typeof err.message === "string" && err.message ? err.message : "Request failed"];
|
|
328
|
+
const code = typeof err.code === "string" && /^[a-z_]+$/.test(err.code) ? err.code : null;
|
|
329
|
+
if (code) {
|
|
330
|
+
const status = typeof err.status === "number" ? ` (HTTP ${err.status})` : "";
|
|
331
|
+
lines.push(`code: ${code}${status}`);
|
|
332
|
+
}
|
|
333
|
+
if (err.details !== void 0 && err.details !== null) {
|
|
334
|
+
lines.push(`details: ${JSON.stringify(err.details)}`);
|
|
335
|
+
}
|
|
336
|
+
if (typeof err.upgradeUrl === "string" && err.upgradeUrl) {
|
|
337
|
+
lines.push(`upgrade: https://app.groundmail.dev${err.upgradeUrl.startsWith("/") ? "" : "/"}${err.upgradeUrl}`);
|
|
338
|
+
}
|
|
339
|
+
if (code) {
|
|
340
|
+
lines.push(`how to fix: https://docs.groundmail.dev/errors#${code}`);
|
|
341
|
+
}
|
|
342
|
+
return lines.join("\n");
|
|
343
|
+
}
|
|
325
344
|
var paginationProps = {
|
|
326
345
|
limit: { type: "number", description: "Page size (1\u2013100)" },
|
|
327
346
|
cursor: { type: "string", description: "Opaque cursor for the next page" }
|
|
@@ -577,11 +596,19 @@ function resolveApiKey() {
|
|
|
577
596
|
}
|
|
578
597
|
|
|
579
598
|
// src/server.ts
|
|
599
|
+
var MCP_SERVER_VERSION = "0.5.6";
|
|
600
|
+
var MCP_SERVER_INSTRUCTIONS = [
|
|
601
|
+
"Groundmail sends transactional email from domains the user has verified.",
|
|
602
|
+
'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.',
|
|
603
|
+
"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).",
|
|
604
|
+
"Confirm with the user before sending email to anyone other than themselves.",
|
|
605
|
+
"Errors include a stable code and a link to https://docs.groundmail.dev/errors#<code> that says how to fix them."
|
|
606
|
+
].join(" ");
|
|
580
607
|
function createMcpServer(apiKey, baseUrl) {
|
|
581
608
|
const client = new Groundmail(apiKey, baseUrl ? { baseUrl } : {});
|
|
582
609
|
const server = new Server(
|
|
583
|
-
{ name: "groundmail-mcp",
|
|
584
|
-
{ capabilities: { tools: {} } }
|
|
610
|
+
{ name: "groundmail-mcp", title: "Groundmail", version: MCP_SERVER_VERSION },
|
|
611
|
+
{ capabilities: { tools: {} }, instructions: MCP_SERVER_INSTRUCTIONS }
|
|
585
612
|
);
|
|
586
613
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
587
614
|
tools: MCP_TOOLS.map((tool) => ({
|
|
@@ -606,18 +633,32 @@ async function runStdioServer() {
|
|
|
606
633
|
|
|
607
634
|
// src/oauth-metadata.ts
|
|
608
635
|
var MCP_HTTP_PATH = "/mcp";
|
|
636
|
+
var PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
637
|
+
var PROTECTED_RESOURCE_METADATA_PATHS = /* @__PURE__ */ new Set([
|
|
638
|
+
PROTECTED_RESOURCE_METADATA_PATH,
|
|
639
|
+
`${PROTECTED_RESOURCE_METADATA_PATH}${MCP_HTTP_PATH}`
|
|
640
|
+
]);
|
|
641
|
+
var DOCS_URL = "https://docs.groundmail.dev/ai/mcp";
|
|
642
|
+
function trimOrigin(origin) {
|
|
643
|
+
return origin.replace(/\/$/, "");
|
|
644
|
+
}
|
|
609
645
|
function buildProtectedResourceMetadata(origin) {
|
|
610
|
-
const resource = `${origin
|
|
646
|
+
const resource = `${trimOrigin(origin)}${MCP_HTTP_PATH}`;
|
|
611
647
|
const appOrigin = process.env.GROUNDMAIL_OAUTH_ISSUER?.trim() || process.env.GROUNDMAIL_APP_URL?.trim() || "https://app.groundmail.dev";
|
|
612
648
|
return {
|
|
613
649
|
resource,
|
|
650
|
+
resource_name: "Groundmail",
|
|
651
|
+
resource_documentation: DOCS_URL,
|
|
614
652
|
authorization_servers: [appOrigin.replace(/\/$/, "")],
|
|
615
653
|
scopes_supported: ["full"],
|
|
616
654
|
bearer_methods_supported: ["header"]
|
|
617
655
|
};
|
|
618
656
|
}
|
|
619
|
-
function buildWwwAuthenticateChallenge(origin) {
|
|
620
|
-
const metadataUrl = `${origin
|
|
657
|
+
function buildWwwAuthenticateChallenge(origin, options = {}) {
|
|
658
|
+
const metadataUrl = `${trimOrigin(origin)}${PROTECTED_RESOURCE_METADATA_PATH}`;
|
|
659
|
+
if (options.invalidToken === false) {
|
|
660
|
+
return `Bearer resource_metadata="${metadataUrl}"`;
|
|
661
|
+
}
|
|
621
662
|
return `Bearer error="invalid_token", error_description="OAuth authentication required", resource_metadata="${metadataUrl}"`;
|
|
622
663
|
}
|
|
623
664
|
|
|
@@ -644,15 +685,27 @@ async function resolveOAuthAccessToken(accessToken) {
|
|
|
644
685
|
}
|
|
645
686
|
|
|
646
687
|
// src/http.ts
|
|
688
|
+
var CORS_HEADERS = {
|
|
689
|
+
"access-control-allow-origin": "*",
|
|
690
|
+
"access-control-expose-headers": "WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version"
|
|
691
|
+
};
|
|
692
|
+
var CORS_PREFLIGHT_HEADERS = {
|
|
693
|
+
...CORS_HEADERS,
|
|
694
|
+
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
695
|
+
"access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, X-Groundmail-Mcp-Resolve",
|
|
696
|
+
"access-control-max-age": "86400"
|
|
697
|
+
};
|
|
647
698
|
function sendJson(res, status, body, headers = {}) {
|
|
648
699
|
const payload = JSON.stringify(body);
|
|
649
700
|
res.writeHead(status, {
|
|
650
701
|
"content-type": "application/json; charset=utf-8",
|
|
651
702
|
"content-length": Buffer.byteLength(payload),
|
|
703
|
+
...CORS_HEADERS,
|
|
652
704
|
...headers
|
|
653
705
|
});
|
|
654
706
|
res.end(payload);
|
|
655
707
|
}
|
|
708
|
+
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
709
|
function firstHeaderValue(value) {
|
|
657
710
|
if (!value) return void 0;
|
|
658
711
|
const raw = Array.isArray(value) ? value[0] : value;
|
|
@@ -674,11 +727,7 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
674
727
|
const origin = getRequestOrigin(req);
|
|
675
728
|
const url = new URL(req.url || "/", origin);
|
|
676
729
|
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
|
-
});
|
|
730
|
+
res.writeHead(204, CORS_PREFLIGHT_HEADERS);
|
|
682
731
|
res.end();
|
|
683
732
|
return;
|
|
684
733
|
}
|
|
@@ -686,7 +735,7 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
686
735
|
sendJson(res, 200, { ok: true, service: "groundmail-mcp" });
|
|
687
736
|
return;
|
|
688
737
|
}
|
|
689
|
-
if (url.pathname
|
|
738
|
+
if (PROTECTED_RESOURCE_METADATA_PATHS.has(url.pathname)) {
|
|
690
739
|
sendJson(res, 200, buildProtectedResourceMetadata(origin), {
|
|
691
740
|
"cache-control": "public, max-age=300, s-maxage=3600"
|
|
692
741
|
});
|
|
@@ -696,16 +745,25 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
696
745
|
sendJson(res, 404, { error: "Not found. MCP endpoint is POST/GET /mcp" });
|
|
697
746
|
return;
|
|
698
747
|
}
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
options.baseUrl
|
|
702
|
-
);
|
|
748
|
+
const authorization = typeof req.headers.authorization === "string" ? req.headers.authorization : void 0;
|
|
749
|
+
const apiKey = await resolveApiKeyFromAuthorization(authorization, options.baseUrl);
|
|
703
750
|
if (!apiKey) {
|
|
751
|
+
const message = "Missing or invalid Authorization. Use Bearer gm_\u2026 or complete MCP OAuth.";
|
|
704
752
|
sendJson(
|
|
705
753
|
res,
|
|
706
754
|
401,
|
|
707
|
-
{
|
|
708
|
-
|
|
755
|
+
{
|
|
756
|
+
error: message,
|
|
757
|
+
code: "unauthorized",
|
|
758
|
+
message,
|
|
759
|
+
hint: UNAUTHORIZED_HINT,
|
|
760
|
+
docs_url: "https://docs.groundmail.dev/errors#unauthorized"
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
"www-authenticate": buildWwwAuthenticateChallenge(origin, {
|
|
764
|
+
invalidToken: Boolean(parseBearerToken(authorization))
|
|
765
|
+
})
|
|
766
|
+
}
|
|
709
767
|
);
|
|
710
768
|
return;
|
|
711
769
|
}
|
|
@@ -713,6 +771,9 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
713
771
|
const transport = new StreamableHTTPServerTransport({
|
|
714
772
|
sessionIdGenerator: void 0
|
|
715
773
|
});
|
|
774
|
+
for (const [name, value] of Object.entries(CORS_HEADERS)) {
|
|
775
|
+
res.setHeader(name, value);
|
|
776
|
+
}
|
|
716
777
|
try {
|
|
717
778
|
await server.connect(transport);
|
|
718
779
|
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.6",
|
|
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": {
|
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
|