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 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,29 @@ 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 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", version: "0.3.0" },
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.replace(/\/$/, "")}${MCP_HTTP_PATH}`;
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.replace(/\/$/, "")}/.well-known/oauth-protected-resource`;
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 === "/.well-known/oauth-protected-resource") {
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 apiKey = await resolveApiKeyFromAuthorization(
700
- typeof req.headers.authorization === "string" ? req.headers.authorization : void 0,
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
- { error: "Missing or invalid Authorization. Use Bearer gm_\u2026 or complete MCP OAuth." },
708
- { "www-authenticate": buildWwwAuthenticateChallenge(origin) }
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.5",
4
- "description": "Groundmail MCP server \u2014 stdio or local HTTP (--http) for Cursor and Claude",
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 — 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