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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # groundmail-mcp
2
2
 
3
- MCP server for [Groundmail](https://groundmail.dev) — send email and manage domains, webhooks, suppressions, and templates from Cursor, Claude Code, and other MCP clients.
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
- # stdio
82
- claude mcp add --transport stdio groundmail -- npx -y groundmail-mcp
81
+ # Hosted, OAuth: then run /mcp, pick groundmail, Authenticate
82
+ claude mcp add --transport http groundmail https://mcp.groundmail.dev/mcp
83
83
 
84
- # HTTP (local — after starting --http server)
85
- claude mcp add --transport http groundmail http://127.0.0.1:3000/mcp
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
- # HTTP (hosted)
88
- claude mcp add --transport http groundmail https://mcp.groundmail.dev/mcp
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
- Export `GROUNDMAIL_API_KEY` for stdio, or configure Bearer headers for HTTP.
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
- const message = error && typeof error === "object" && "message" in error ? String(error.message) : "Request failed";
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", version: "0.3.0" },
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.replace(/\/$/, "")}${MCP_HTTP_PATH}`;
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: ["full"],
671
+ scopes_supported: [...MCP_SCOPES_SUPPORTED],
616
672
  bearer_methods_supported: ["header"]
617
673
  };
618
674
  }
619
- function buildWwwAuthenticateChallenge(origin) {
620
- const metadataUrl = `${origin.replace(/\/$/, "")}/.well-known/oauth-protected-resource`;
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 === "/.well-known/oauth-protected-resource") {
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 apiKey = await resolveApiKeyFromAuthorization(
700
- typeof req.headers.authorization === "string" ? req.headers.authorization : void 0,
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
- { error: "Missing or invalid Authorization. Use Bearer gm_\u2026 or complete MCP OAuth." },
708
- { "www-authenticate": buildWwwAuthenticateChallenge(origin) }
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.5",
4
- "description": "Groundmail MCP server \u2014 stdio or local HTTP (--http) for Cursor and Claude",
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.1"
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 — submit/list after publish: npx @smithery/cli publish (or dashboard)
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