groundmail-mcp 0.0.0-stage → 0.5.5
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 +100 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +763 -0
- package/package.json +47 -4
- package/smithery.yaml +29 -0
package/README.md
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# groundmail-mcp
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
+
|
|
5
|
+
**Install:** `npx -y groundmail-mcp` · **Docs:** https://docs.groundmail.dev/ai/mcp
|
|
6
|
+
|
|
7
|
+
## Transports
|
|
8
|
+
|
|
9
|
+
| Mode | Command / URL | Auth |
|
|
10
|
+
|------|---------------|------|
|
|
11
|
+
| **stdio** (default) | `npx -y groundmail-mcp` | `GROUNDMAIL_API_KEY=gm_…` |
|
|
12
|
+
| **HTTP** (local) | `npx -y groundmail-mcp --http --port 3000` | `Authorization: Bearer gm_…` on each request |
|
|
13
|
+
| **HTTP** (hosted) | `https://mcp.groundmail.dev/mcp` | **OAuth** (recommended) or `Authorization: Bearer gm_…` |
|
|
14
|
+
|
|
15
|
+
Optional: `GROUNDMAIL_API_URL` (e.g. `http://localhost:3003/api` for local API).
|
|
16
|
+
|
|
17
|
+
Local HTTP endpoint: `http://127.0.0.1:3000/mcp` · health: `GET /health`
|
|
18
|
+
Hosted health: `GET https://mcp.groundmail.dev/health`
|
|
19
|
+
|
|
20
|
+
Hosted OAuth: add only `"url": "https://mcp.groundmail.dev/mcp"` — the client runs the browser consent flow. Bearer `gm_…` still works for advanced setups.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Cursor (stdio)
|
|
25
|
+
|
|
26
|
+
`.cursor/mcp.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"groundmail": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "groundmail-mcp"],
|
|
34
|
+
"env": {
|
|
35
|
+
"GROUNDMAIL_API_KEY": "gm_your_key_here"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Cursor (local HTTP)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx -y groundmail-mcp --http --port 3000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"groundmail": {
|
|
52
|
+
"url": "http://127.0.0.1:3000/mcp",
|
|
53
|
+
"headers": {
|
|
54
|
+
"Authorization": "Bearer gm_your_key_here"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Cursor (hosted)
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"groundmail": {
|
|
67
|
+
"url": "https://mcp.groundmail.dev/mcp",
|
|
68
|
+
"headers": {
|
|
69
|
+
"Authorization": "Bearer gm_your_key_here"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Claude Code
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# stdio
|
|
82
|
+
claude mcp add --transport stdio groundmail -- npx -y groundmail-mcp
|
|
83
|
+
|
|
84
|
+
# HTTP (local — after starting --http server)
|
|
85
|
+
claude mcp add --transport http groundmail http://127.0.0.1:3000/mcp
|
|
86
|
+
|
|
87
|
+
# HTTP (hosted)
|
|
88
|
+
claude mcp add --transport http groundmail https://mcp.groundmail.dev/mcp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Export `GROUNDMAIL_API_KEY` for stdio, or configure Bearer headers for HTTP.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Tools
|
|
96
|
+
|
|
97
|
+
See https://docs.groundmail.dev/ai/mcp — emails, domains, webhooks, suppressions, templates, deliverability, webhook test.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
var DEFAULT_HTTP_PORT = 3e3;
|
|
5
|
+
var DEFAULT_HTTP_HOST = "127.0.0.1";
|
|
6
|
+
function parseCliArgs(argv) {
|
|
7
|
+
const args = argv.slice(2);
|
|
8
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
9
|
+
return { mode: "help" };
|
|
10
|
+
}
|
|
11
|
+
const http = args.includes("--http");
|
|
12
|
+
if (!http) {
|
|
13
|
+
return { mode: "stdio" };
|
|
14
|
+
}
|
|
15
|
+
let port = DEFAULT_HTTP_PORT;
|
|
16
|
+
let host = DEFAULT_HTTP_HOST;
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
const arg = args[i];
|
|
19
|
+
if (arg === "--port" || arg === "-p") {
|
|
20
|
+
const raw = args[i + 1];
|
|
21
|
+
const parsed = Number(raw);
|
|
22
|
+
if (!raw || !Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
23
|
+
throw new Error(`Invalid --port value: ${raw ?? "(missing)"}`);
|
|
24
|
+
}
|
|
25
|
+
port = parsed;
|
|
26
|
+
i++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg?.startsWith("--port=")) {
|
|
30
|
+
const raw = arg.slice("--port=".length);
|
|
31
|
+
const parsed = Number(raw);
|
|
32
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
33
|
+
throw new Error(`Invalid --port value: ${raw}`);
|
|
34
|
+
}
|
|
35
|
+
port = parsed;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === "--host") {
|
|
39
|
+
const raw = args[i + 1]?.trim();
|
|
40
|
+
if (!raw) throw new Error("Missing --host value");
|
|
41
|
+
host = raw;
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (arg?.startsWith("--host=")) {
|
|
46
|
+
const raw = arg.slice("--host=".length).trim();
|
|
47
|
+
if (!raw) throw new Error("Missing --host value");
|
|
48
|
+
host = raw;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { mode: "http", port, host };
|
|
52
|
+
}
|
|
53
|
+
function printHelp() {
|
|
54
|
+
console.error(`Usage:
|
|
55
|
+
groundmail-mcp # stdio (default)
|
|
56
|
+
groundmail-mcp --http [--port 3000] [--host 127.0.0.1]
|
|
57
|
+
|
|
58
|
+
Environment:
|
|
59
|
+
GROUNDMAIL_API_KEY Required for stdio (Bearer gm_\u2026 for HTTP)
|
|
60
|
+
GROUNDMAIL_API_URL Optional API base (default https://api.groundmail.dev)
|
|
61
|
+
|
|
62
|
+
HTTP auth:
|
|
63
|
+
Authorization: Bearer gm_\u2026 (API key) or MCP OAuth access token
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
function isGroundmailApiKey(token) {
|
|
67
|
+
return /^gm_[a-f0-9]{48}$/i.test(token.trim());
|
|
68
|
+
}
|
|
69
|
+
function parseBearerToken(authorization) {
|
|
70
|
+
if (!authorization) return null;
|
|
71
|
+
const match = /^Bearer\s+(\S+)/i.exec(authorization.trim());
|
|
72
|
+
return match?.[1] || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/http.ts
|
|
76
|
+
import { createServer } from "http";
|
|
77
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
78
|
+
|
|
79
|
+
// src/server.ts
|
|
80
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
81
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
82
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
83
|
+
import { Groundmail } from "groundmail";
|
|
84
|
+
|
|
85
|
+
// src/tools.ts
|
|
86
|
+
function textResult(value, isError = false) {
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }],
|
|
89
|
+
...isError ? { isError: true } : {}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function optString(args, key) {
|
|
93
|
+
if (args[key] == null) return void 0;
|
|
94
|
+
const value = String(args[key]).trim();
|
|
95
|
+
return value || void 0;
|
|
96
|
+
}
|
|
97
|
+
function optNumber(args, key) {
|
|
98
|
+
if (args[key] == null || args[key] === "") return void 0;
|
|
99
|
+
const n = Number(args[key]);
|
|
100
|
+
return Number.isFinite(n) ? n : void 0;
|
|
101
|
+
}
|
|
102
|
+
function optBoolean(args, key) {
|
|
103
|
+
if (args[key] == null) return void 0;
|
|
104
|
+
if (typeof args[key] === "boolean") return args[key];
|
|
105
|
+
const raw = String(args[key]).trim().toLowerCase();
|
|
106
|
+
if (raw === "true" || raw === "1") return true;
|
|
107
|
+
if (raw === "false" || raw === "0") return false;
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
function optStringArray(args, key) {
|
|
111
|
+
const value = args[key];
|
|
112
|
+
if (value == null) return void 0;
|
|
113
|
+
if (Array.isArray(value)) return value.map((v) => String(v));
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
var EMAIL_WEBHOOK_EVENTS = /* @__PURE__ */ new Set([
|
|
117
|
+
"email.received",
|
|
118
|
+
"email.sent",
|
|
119
|
+
"email.delivered",
|
|
120
|
+
"email.delivery_delayed",
|
|
121
|
+
"email.bounced",
|
|
122
|
+
"email.complained",
|
|
123
|
+
"email.opened",
|
|
124
|
+
"email.clicked",
|
|
125
|
+
"email.failed",
|
|
126
|
+
"email.suppressed",
|
|
127
|
+
"email.scheduled"
|
|
128
|
+
]);
|
|
129
|
+
async function callMcpTool(client, name, args) {
|
|
130
|
+
try {
|
|
131
|
+
switch (name) {
|
|
132
|
+
case "send_email": {
|
|
133
|
+
const from = optString(args, "from");
|
|
134
|
+
const to = optString(args, "to");
|
|
135
|
+
const subject = optString(args, "subject");
|
|
136
|
+
const templateId = optString(args, "template_id") || optString(args, "template_alias");
|
|
137
|
+
if (!from) return textResult("Missing required argument: from", true);
|
|
138
|
+
if (!to) return textResult("Missing required argument: to", true);
|
|
139
|
+
if (!templateId && !subject) {
|
|
140
|
+
return textResult("Provide subject, or template_id / template_alias", true);
|
|
141
|
+
}
|
|
142
|
+
const variables = args.variables && typeof args.variables === "object" && !Array.isArray(args.variables) ? args.variables : void 0;
|
|
143
|
+
const result = await client.emails.send({
|
|
144
|
+
from,
|
|
145
|
+
to,
|
|
146
|
+
...subject ? { subject } : {},
|
|
147
|
+
html: optString(args, "html"),
|
|
148
|
+
text: optString(args, "text"),
|
|
149
|
+
reply_to: optString(args, "reply_to"),
|
|
150
|
+
...templateId ? { template: { id: templateId, ...variables ? { variables } : {} } } : {}
|
|
151
|
+
});
|
|
152
|
+
return textResult(result);
|
|
153
|
+
}
|
|
154
|
+
case "list_emails": {
|
|
155
|
+
const result = await client.emails.list({
|
|
156
|
+
limit: optNumber(args, "limit"),
|
|
157
|
+
cursor: optString(args, "cursor")
|
|
158
|
+
});
|
|
159
|
+
return textResult(result);
|
|
160
|
+
}
|
|
161
|
+
case "get_email": {
|
|
162
|
+
const id = optString(args, "id");
|
|
163
|
+
if (!id) return textResult("Missing required argument: id", true);
|
|
164
|
+
return textResult(await client.emails.get(id));
|
|
165
|
+
}
|
|
166
|
+
case "apply_domain_dns": {
|
|
167
|
+
const domain = String(args.domain ?? "").trim();
|
|
168
|
+
if (!domain) {
|
|
169
|
+
return textResult("Missing required argument: domain", true);
|
|
170
|
+
}
|
|
171
|
+
const providerRaw = typeof args.provider === "string" ? args.provider.trim().toLowerCase() : "";
|
|
172
|
+
if (providerRaw !== "cloudflare" && providerRaw !== "godaddy" && providerRaw !== "vercel") {
|
|
173
|
+
return textResult(
|
|
174
|
+
'Missing or invalid provider. Use provider "cloudflare", "godaddy", or "vercel".',
|
|
175
|
+
true
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const credentials = args.credentials && typeof args.credentials === "object" ? args.credentials : void 0;
|
|
179
|
+
const result = await client.domains.apply(domain, {
|
|
180
|
+
provider: providerRaw,
|
|
181
|
+
...credentials ? { credentials } : {}
|
|
182
|
+
});
|
|
183
|
+
return textResult(result);
|
|
184
|
+
}
|
|
185
|
+
case "verify_domain": {
|
|
186
|
+
const domain = optString(args, "domain");
|
|
187
|
+
if (!domain) return textResult("Missing required argument: domain", true);
|
|
188
|
+
const result = await client.domains.verify(domain);
|
|
189
|
+
return textResult(result);
|
|
190
|
+
}
|
|
191
|
+
case "list_domains": {
|
|
192
|
+
const result = await client.domains.list({
|
|
193
|
+
limit: optNumber(args, "limit"),
|
|
194
|
+
cursor: optString(args, "cursor")
|
|
195
|
+
});
|
|
196
|
+
return textResult(result);
|
|
197
|
+
}
|
|
198
|
+
case "create_domain": {
|
|
199
|
+
const domainName = optString(args, "name");
|
|
200
|
+
if (!domainName) return textResult("Missing required argument: name", true);
|
|
201
|
+
const inbound = optBoolean(args, "inbound_enabled");
|
|
202
|
+
return textResult(
|
|
203
|
+
await client.domains.create({
|
|
204
|
+
name: domainName,
|
|
205
|
+
...inbound !== void 0 ? { inbound_enabled: inbound } : {}
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
case "list_webhooks": {
|
|
210
|
+
return textResult(
|
|
211
|
+
await client.webhooks.list({
|
|
212
|
+
limit: optNumber(args, "limit"),
|
|
213
|
+
cursor: optString(args, "cursor")
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
case "create_webhook": {
|
|
218
|
+
const url = optString(args, "url");
|
|
219
|
+
if (!url) return textResult("Missing required argument: url", true);
|
|
220
|
+
const events = optStringArray(args, "events");
|
|
221
|
+
return textResult(
|
|
222
|
+
await client.webhooks.create({
|
|
223
|
+
url,
|
|
224
|
+
...events ? { events } : {}
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
case "delete_webhook": {
|
|
229
|
+
const id = optString(args, "id");
|
|
230
|
+
if (!id) return textResult("Missing required argument: id", true);
|
|
231
|
+
return textResult(await client.webhooks.delete(id));
|
|
232
|
+
}
|
|
233
|
+
case "list_suppressions": {
|
|
234
|
+
return textResult(
|
|
235
|
+
await client.suppressions.list({
|
|
236
|
+
limit: optNumber(args, "limit"),
|
|
237
|
+
cursor: optString(args, "cursor"),
|
|
238
|
+
email: optString(args, "email")
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
case "add_suppression": {
|
|
243
|
+
const email = optString(args, "email");
|
|
244
|
+
if (!email) return textResult("Missing required argument: email", true);
|
|
245
|
+
return textResult(
|
|
246
|
+
await client.suppressions.create({
|
|
247
|
+
email,
|
|
248
|
+
reason: optString(args, "reason")
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
case "remove_suppression": {
|
|
253
|
+
const idOrEmail = optString(args, "id") ?? optString(args, "email");
|
|
254
|
+
if (!idOrEmail) {
|
|
255
|
+
return textResult("Missing required argument: id or email", true);
|
|
256
|
+
}
|
|
257
|
+
return textResult(await client.suppressions.remove(idOrEmail));
|
|
258
|
+
}
|
|
259
|
+
case "list_templates": {
|
|
260
|
+
const status = optString(args, "status");
|
|
261
|
+
return textResult(
|
|
262
|
+
await client.templates.list({
|
|
263
|
+
limit: optNumber(args, "limit"),
|
|
264
|
+
cursor: optString(args, "cursor"),
|
|
265
|
+
...status === "draft" || status === "published" ? { status } : {}
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
case "get_template": {
|
|
270
|
+
const idOrAlias = optString(args, "id") ?? optString(args, "alias");
|
|
271
|
+
if (!idOrAlias) return textResult("Missing required argument: id or alias", true);
|
|
272
|
+
return textResult(await client.templates.get(idOrAlias));
|
|
273
|
+
}
|
|
274
|
+
case "create_template": {
|
|
275
|
+
const name2 = optString(args, "name");
|
|
276
|
+
const subjectArg = optString(args, "subject");
|
|
277
|
+
if (!name2) return textResult("Missing required argument: name", true);
|
|
278
|
+
if (!subjectArg) return textResult("Missing required argument: subject", true);
|
|
279
|
+
const format = optString(args, "format");
|
|
280
|
+
return textResult(
|
|
281
|
+
await client.templates.create({
|
|
282
|
+
name: name2,
|
|
283
|
+
subject: subjectArg,
|
|
284
|
+
...format === "blocks" || format === "html" ? { format } : {},
|
|
285
|
+
html: optString(args, "html"),
|
|
286
|
+
text: optString(args, "text"),
|
|
287
|
+
alias: optString(args, "alias")
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
case "publish_template": {
|
|
292
|
+
const idOrAlias = optString(args, "id") ?? optString(args, "alias");
|
|
293
|
+
if (!idOrAlias) return textResult("Missing required argument: id or alias", true);
|
|
294
|
+
return textResult(await client.templates.publish(idOrAlias));
|
|
295
|
+
}
|
|
296
|
+
case "send_test_webhook_event": {
|
|
297
|
+
const event = optString(args, "event");
|
|
298
|
+
if (!event || !EMAIL_WEBHOOK_EVENTS.has(event)) {
|
|
299
|
+
return textResult(
|
|
300
|
+
`Missing or invalid event. Use one of: ${[...EMAIL_WEBHOOK_EVENTS].join(", ")}`,
|
|
301
|
+
true
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return textResult(
|
|
305
|
+
await client.emails.testWebhook({
|
|
306
|
+
event,
|
|
307
|
+
email_id: optString(args, "email_id"),
|
|
308
|
+
deliver: optBoolean(args, "deliver")
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
case "get_deliverability": {
|
|
313
|
+
const window = args.window === "7d" ? "7d" : "30d";
|
|
314
|
+
const result = await client.emails.insights(window);
|
|
315
|
+
return textResult(result);
|
|
316
|
+
}
|
|
317
|
+
default:
|
|
318
|
+
return textResult(`Unknown tool: ${name}`, true);
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const message = error && typeof error === "object" && "message" in error ? String(error.message) : "Request failed";
|
|
322
|
+
return textResult(message, true);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
var paginationProps = {
|
|
326
|
+
limit: { type: "number", description: "Page size (1\u2013100)" },
|
|
327
|
+
cursor: { type: "string", description: "Opaque cursor for the next page" }
|
|
328
|
+
};
|
|
329
|
+
var MCP_TOOLS = [
|
|
330
|
+
{
|
|
331
|
+
name: "send_email",
|
|
332
|
+
description: "Send a transactional email via Groundmail. Provide subject + html/text, or a published template_id / template_alias with optional variables.",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: "object",
|
|
335
|
+
properties: {
|
|
336
|
+
from: { type: "string", description: "Sender address (must match a verified domain or sandbox sender)" },
|
|
337
|
+
to: { type: "string", description: "Recipient email address" },
|
|
338
|
+
subject: { type: "string", description: "Optional when using a published template" },
|
|
339
|
+
html: { type: "string" },
|
|
340
|
+
text: { type: "string" },
|
|
341
|
+
reply_to: { type: "string" },
|
|
342
|
+
template_id: { type: "string", description: "Published template UUID" },
|
|
343
|
+
template_alias: { type: "string", description: "Published template alias" },
|
|
344
|
+
variables: {
|
|
345
|
+
type: "object",
|
|
346
|
+
description: "Template variables when using template_id or template_alias",
|
|
347
|
+
additionalProperties: true
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
required: ["from", "to"]
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: "list_emails",
|
|
355
|
+
description: "List recent emails sent from this account.",
|
|
356
|
+
inputSchema: {
|
|
357
|
+
type: "object",
|
|
358
|
+
properties: { ...paginationProps }
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: "get_email",
|
|
363
|
+
description: "Get a single email by public id (msg_\u2026).",
|
|
364
|
+
inputSchema: {
|
|
365
|
+
type: "object",
|
|
366
|
+
properties: {
|
|
367
|
+
id: { type: "string", description: "Email id (msg_\u2026)" }
|
|
368
|
+
},
|
|
369
|
+
required: ["id"]
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "list_domains",
|
|
374
|
+
description: "List sending domains for this account.",
|
|
375
|
+
inputSchema: {
|
|
376
|
+
type: "object",
|
|
377
|
+
properties: { ...paginationProps }
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "create_domain",
|
|
382
|
+
description: "Add a sending domain. DNS must still be applied and verified before production sends.",
|
|
383
|
+
inputSchema: {
|
|
384
|
+
type: "object",
|
|
385
|
+
properties: {
|
|
386
|
+
name: { type: "string", description: "Domain name (example.com)" },
|
|
387
|
+
inbound_enabled: { type: "boolean", description: "Enable inbound receiving on this domain" }
|
|
388
|
+
},
|
|
389
|
+
required: ["name"]
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "apply_domain_dns",
|
|
394
|
+
description: "Apply Groundmail DNS records for a domain. Cloudflare and GoDaddy can use credentials stored in the dashboard; GoDaddy also accepts one-time credentials on the request.",
|
|
395
|
+
inputSchema: {
|
|
396
|
+
type: "object",
|
|
397
|
+
properties: {
|
|
398
|
+
domain: { type: "string" },
|
|
399
|
+
provider: { type: "string", enum: ["cloudflare", "godaddy", "vercel"] },
|
|
400
|
+
credentials: {
|
|
401
|
+
type: "object",
|
|
402
|
+
description: "Optional inline registrar credentials (GoDaddy api key/secret)"
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
required: ["domain", "provider"]
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: "verify_domain",
|
|
410
|
+
description: "Verify DNS for a sending domain after records are applied.",
|
|
411
|
+
inputSchema: {
|
|
412
|
+
type: "object",
|
|
413
|
+
properties: {
|
|
414
|
+
domain: { type: "string" }
|
|
415
|
+
},
|
|
416
|
+
required: ["domain"]
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: "list_webhooks",
|
|
421
|
+
description: "List webhook endpoints for this account.",
|
|
422
|
+
inputSchema: {
|
|
423
|
+
type: "object",
|
|
424
|
+
properties: { ...paginationProps }
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: "create_webhook",
|
|
429
|
+
description: "Create a webhook endpoint. Returns the endpoint plus a signing secret (shown once).",
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: "object",
|
|
432
|
+
properties: {
|
|
433
|
+
url: { type: "string", description: "HTTPS URL that receives events" },
|
|
434
|
+
events: {
|
|
435
|
+
type: "array",
|
|
436
|
+
items: { type: "string" },
|
|
437
|
+
description: "Optional event types (defaults apply when omitted)"
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
required: ["url"]
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: "delete_webhook",
|
|
445
|
+
description: "Delete a webhook endpoint by id.",
|
|
446
|
+
inputSchema: {
|
|
447
|
+
type: "object",
|
|
448
|
+
properties: {
|
|
449
|
+
id: { type: "string", description: "Webhook id (wh_\u2026)" }
|
|
450
|
+
},
|
|
451
|
+
required: ["id"]
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "list_suppressions",
|
|
456
|
+
description: "List suppressed recipient addresses.",
|
|
457
|
+
inputSchema: {
|
|
458
|
+
type: "object",
|
|
459
|
+
properties: {
|
|
460
|
+
...paginationProps,
|
|
461
|
+
email: { type: "string", description: "Optional filter by email" }
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "add_suppression",
|
|
467
|
+
description: "Add an address to the suppression list.",
|
|
468
|
+
inputSchema: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
email: { type: "string" },
|
|
472
|
+
reason: { type: "string" }
|
|
473
|
+
},
|
|
474
|
+
required: ["email"]
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: "remove_suppression",
|
|
479
|
+
description: "Remove a suppression by id or email address.",
|
|
480
|
+
inputSchema: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {
|
|
483
|
+
id: { type: "string", description: "Suppression id" },
|
|
484
|
+
email: { type: "string", description: "Email address (alternative to id)" }
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: "list_templates",
|
|
490
|
+
description: "List email templates.",
|
|
491
|
+
inputSchema: {
|
|
492
|
+
type: "object",
|
|
493
|
+
properties: {
|
|
494
|
+
...paginationProps,
|
|
495
|
+
status: { type: "string", enum: ["draft", "published"] }
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: "get_template",
|
|
501
|
+
description: "Get a template by id or alias.",
|
|
502
|
+
inputSchema: {
|
|
503
|
+
type: "object",
|
|
504
|
+
properties: {
|
|
505
|
+
id: { type: "string", description: "Template id (tpl_\u2026)" },
|
|
506
|
+
alias: { type: "string", description: "Published alias (alternative to id)" }
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: "create_template",
|
|
512
|
+
description: "Create a draft email template (html or blocks format).",
|
|
513
|
+
inputSchema: {
|
|
514
|
+
type: "object",
|
|
515
|
+
properties: {
|
|
516
|
+
name: { type: "string" },
|
|
517
|
+
subject: { type: "string" },
|
|
518
|
+
format: { type: "string", enum: ["html", "blocks"] },
|
|
519
|
+
html: { type: "string" },
|
|
520
|
+
text: { type: "string" },
|
|
521
|
+
alias: { type: "string" }
|
|
522
|
+
},
|
|
523
|
+
required: ["name", "subject"]
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "publish_template",
|
|
528
|
+
description: "Publish a template, snapshotting an immutable version for sends.",
|
|
529
|
+
inputSchema: {
|
|
530
|
+
type: "object",
|
|
531
|
+
properties: {
|
|
532
|
+
id: { type: "string" },
|
|
533
|
+
alias: { type: "string" }
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: "send_test_webhook_event",
|
|
539
|
+
description: "Simulate a webhook event (POST /emails/test) for CI or local webhook testing. Optionally deliver to subscribed endpoints.",
|
|
540
|
+
inputSchema: {
|
|
541
|
+
type: "object",
|
|
542
|
+
properties: {
|
|
543
|
+
event: {
|
|
544
|
+
type: "string",
|
|
545
|
+
enum: [...EMAIL_WEBHOOK_EVENTS],
|
|
546
|
+
description: "Webhook event type to simulate"
|
|
547
|
+
},
|
|
548
|
+
email_id: { type: "string", description: "Optional existing email id (msg_\u2026)" },
|
|
549
|
+
deliver: {
|
|
550
|
+
type: "boolean",
|
|
551
|
+
description: "When true (default), enqueue delivery to account webhooks"
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
required: ["event"]
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: "get_deliverability",
|
|
559
|
+
description: "Fetch best-effort deliverability metrics for recent sends (7d or 30d window).",
|
|
560
|
+
inputSchema: {
|
|
561
|
+
type: "object",
|
|
562
|
+
properties: {
|
|
563
|
+
window: { type: "string", enum: ["7d", "30d"] }
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
];
|
|
568
|
+
function resolveApiKey() {
|
|
569
|
+
const key = process.env.GROUNDMAIL_API_KEY?.trim();
|
|
570
|
+
if (!key) {
|
|
571
|
+
throw new Error("GROUNDMAIL_API_KEY is required");
|
|
572
|
+
}
|
|
573
|
+
if (!key.startsWith("gm_")) {
|
|
574
|
+
throw new Error("GROUNDMAIL_API_KEY must start with gm_");
|
|
575
|
+
}
|
|
576
|
+
return key;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/server.ts
|
|
580
|
+
function createMcpServer(apiKey, baseUrl) {
|
|
581
|
+
const client = new Groundmail(apiKey, baseUrl ? { baseUrl } : {});
|
|
582
|
+
const server = new Server(
|
|
583
|
+
{ name: "groundmail-mcp", version: "0.3.0" },
|
|
584
|
+
{ capabilities: { tools: {} } }
|
|
585
|
+
);
|
|
586
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
587
|
+
tools: MCP_TOOLS.map((tool) => ({
|
|
588
|
+
name: tool.name,
|
|
589
|
+
description: tool.description,
|
|
590
|
+
inputSchema: tool.inputSchema
|
|
591
|
+
}))
|
|
592
|
+
}));
|
|
593
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
594
|
+
const result = await callMcpTool(client, request.params.name, request.params.arguments ?? {});
|
|
595
|
+
return result;
|
|
596
|
+
});
|
|
597
|
+
return server;
|
|
598
|
+
}
|
|
599
|
+
async function runStdioServer() {
|
|
600
|
+
const apiKey = resolveApiKey();
|
|
601
|
+
const baseUrl = process.env.GROUNDMAIL_API_URL?.trim();
|
|
602
|
+
const server = createMcpServer(apiKey, baseUrl || void 0);
|
|
603
|
+
const transport = new StdioServerTransport();
|
|
604
|
+
await server.connect(transport);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/oauth-metadata.ts
|
|
608
|
+
var MCP_HTTP_PATH = "/mcp";
|
|
609
|
+
function buildProtectedResourceMetadata(origin) {
|
|
610
|
+
const resource = `${origin.replace(/\/$/, "")}${MCP_HTTP_PATH}`;
|
|
611
|
+
const appOrigin = process.env.GROUNDMAIL_OAUTH_ISSUER?.trim() || process.env.GROUNDMAIL_APP_URL?.trim() || "https://app.groundmail.dev";
|
|
612
|
+
return {
|
|
613
|
+
resource,
|
|
614
|
+
authorization_servers: [appOrigin.replace(/\/$/, "")],
|
|
615
|
+
scopes_supported: ["full"],
|
|
616
|
+
bearer_methods_supported: ["header"]
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function buildWwwAuthenticateChallenge(origin) {
|
|
620
|
+
const metadataUrl = `${origin.replace(/\/$/, "")}/.well-known/oauth-protected-resource`;
|
|
621
|
+
return `Bearer error="invalid_token", error_description="OAuth authentication required", resource_metadata="${metadataUrl}"`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/oauth-resolve.ts
|
|
625
|
+
function getOAuthResolveUrl() {
|
|
626
|
+
const appBase = (process.env.GROUNDMAIL_APP_URL?.trim() || process.env.GROUNDMAIL_OAUTH_ISSUER?.trim() || "https://app.groundmail.dev").replace(/\/$/, "");
|
|
627
|
+
return `${appBase}/api/mcp/oauth/resolve`;
|
|
628
|
+
}
|
|
629
|
+
async function resolveOAuthAccessToken(accessToken) {
|
|
630
|
+
const resolveSecret = process.env.GROUNDMAIL_MCP_RESOLVE_SECRET?.trim() || process.env.GROUNDMAIL_CREDENTIALS_ENCRYPTION_KEY?.trim() || (process.env.NODE_ENV === "production" ? "" : "groundmail-dev-mcp-resolve");
|
|
631
|
+
if (!resolveSecret) return null;
|
|
632
|
+
const response = await fetch(getOAuthResolveUrl(), {
|
|
633
|
+
method: "POST",
|
|
634
|
+
headers: {
|
|
635
|
+
authorization: `Bearer ${accessToken}`,
|
|
636
|
+
"content-type": "application/json",
|
|
637
|
+
"x-groundmail-mcp-resolve": resolveSecret
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
if (!response.ok) return null;
|
|
641
|
+
const data = await response.json().catch(() => null);
|
|
642
|
+
const apiKey = data?.apiKey?.trim();
|
|
643
|
+
return apiKey || null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/http.ts
|
|
647
|
+
function sendJson(res, status, body, headers = {}) {
|
|
648
|
+
const payload = JSON.stringify(body);
|
|
649
|
+
res.writeHead(status, {
|
|
650
|
+
"content-type": "application/json; charset=utf-8",
|
|
651
|
+
"content-length": Buffer.byteLength(payload),
|
|
652
|
+
...headers
|
|
653
|
+
});
|
|
654
|
+
res.end(payload);
|
|
655
|
+
}
|
|
656
|
+
function firstHeaderValue(value) {
|
|
657
|
+
if (!value) return void 0;
|
|
658
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
659
|
+
const trimmed = raw?.split(",")[0]?.trim();
|
|
660
|
+
return trimmed || void 0;
|
|
661
|
+
}
|
|
662
|
+
function getRequestOrigin(req) {
|
|
663
|
+
const host = firstHeaderValue(req.headers["x-forwarded-host"]) || firstHeaderValue(req.headers.host) || "127.0.0.1";
|
|
664
|
+
const proto = firstHeaderValue(req.headers["x-forwarded-proto"]) || "http";
|
|
665
|
+
return `${proto}://${host}`;
|
|
666
|
+
}
|
|
667
|
+
async function resolveApiKeyFromAuthorization(authorization, baseUrl) {
|
|
668
|
+
const bearer = parseBearerToken(authorization);
|
|
669
|
+
if (!bearer) return null;
|
|
670
|
+
if (isGroundmailApiKey(bearer)) return bearer;
|
|
671
|
+
return resolveOAuthAccessToken(bearer);
|
|
672
|
+
}
|
|
673
|
+
async function handleHttpRequest(req, res, options = {}) {
|
|
674
|
+
const origin = getRequestOrigin(req);
|
|
675
|
+
const url = new URL(req.url || "/", origin);
|
|
676
|
+
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
|
+
});
|
|
682
|
+
res.end();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (url.pathname === "/health") {
|
|
686
|
+
sendJson(res, 200, { ok: true, service: "groundmail-mcp" });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (url.pathname === "/.well-known/oauth-protected-resource") {
|
|
690
|
+
sendJson(res, 200, buildProtectedResourceMetadata(origin), {
|
|
691
|
+
"cache-control": "public, max-age=300, s-maxage=3600"
|
|
692
|
+
});
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (url.pathname !== "/mcp" && url.pathname !== "/") {
|
|
696
|
+
sendJson(res, 404, { error: "Not found. MCP endpoint is POST/GET /mcp" });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const apiKey = await resolveApiKeyFromAuthorization(
|
|
700
|
+
typeof req.headers.authorization === "string" ? req.headers.authorization : void 0,
|
|
701
|
+
options.baseUrl
|
|
702
|
+
);
|
|
703
|
+
if (!apiKey) {
|
|
704
|
+
sendJson(
|
|
705
|
+
res,
|
|
706
|
+
401,
|
|
707
|
+
{ error: "Missing or invalid Authorization. Use Bearer gm_\u2026 or complete MCP OAuth." },
|
|
708
|
+
{ "www-authenticate": buildWwwAuthenticateChallenge(origin) }
|
|
709
|
+
);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const server = createMcpServer(apiKey, options.baseUrl);
|
|
713
|
+
const transport = new StreamableHTTPServerTransport({
|
|
714
|
+
sessionIdGenerator: void 0
|
|
715
|
+
});
|
|
716
|
+
try {
|
|
717
|
+
await server.connect(transport);
|
|
718
|
+
await transport.handleRequest(req, res);
|
|
719
|
+
} finally {
|
|
720
|
+
await transport.close().catch(() => void 0);
|
|
721
|
+
await server.close().catch(() => void 0);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function runHttpServer(options) {
|
|
725
|
+
const host = options.host ?? "127.0.0.1";
|
|
726
|
+
const port = options.port;
|
|
727
|
+
const baseUrl = process.env.GROUNDMAIL_API_URL?.trim() || void 0;
|
|
728
|
+
const httpServer = createServer((req, res) => {
|
|
729
|
+
void handleHttpRequest(req, res, { baseUrl }).catch((error) => {
|
|
730
|
+
if (!res.headersSent) {
|
|
731
|
+
sendJson(res, 500, {
|
|
732
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
733
|
+
});
|
|
734
|
+
} else {
|
|
735
|
+
res.end();
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
await new Promise((resolve, reject) => {
|
|
740
|
+
httpServer.once("error", reject);
|
|
741
|
+
httpServer.listen(port, host, () => resolve());
|
|
742
|
+
});
|
|
743
|
+
console.error(`Groundmail MCP HTTP listening on http://${host}:${port}/mcp`);
|
|
744
|
+
console.error("Authenticate with Bearer gm_\u2026 or MCP OAuth access token");
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/index.ts
|
|
748
|
+
async function main() {
|
|
749
|
+
const options = parseCliArgs(process.argv);
|
|
750
|
+
if (options.mode === "help") {
|
|
751
|
+
printHelp();
|
|
752
|
+
process.exit(0);
|
|
753
|
+
}
|
|
754
|
+
if (options.mode === "http") {
|
|
755
|
+
await runHttpServer({ port: options.port, host: options.host });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
await runStdioServer();
|
|
759
|
+
}
|
|
760
|
+
main().catch((error) => {
|
|
761
|
+
console.error(error instanceof Error ? error.message : error);
|
|
762
|
+
process.exit(1);
|
|
763
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groundmail-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.5",
|
|
4
|
+
"description": "Groundmail MCP server \u2014 stdio or local HTTP (--http) for Cursor and Claude",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"groundmail-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"smithery.yaml"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Groundmail/groundmail-sdks.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://docs.groundmail.dev/ai/mcp",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"email",
|
|
23
|
+
"transactional",
|
|
24
|
+
"groundmail",
|
|
25
|
+
"cursor",
|
|
26
|
+
"claude"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
42
|
+
"groundmail": "0.14.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"tsup": "^8.5.0",
|
|
46
|
+
"typescript": "^5",
|
|
47
|
+
"vitest": "^3.2.4"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Smithery configuration for groundmail-mcp
|
|
2
|
+
# https://smithery.ai — submit/list after publish: npx @smithery/cli publish (or dashboard)
|
|
3
|
+
|
|
4
|
+
startCommand:
|
|
5
|
+
type: stdio
|
|
6
|
+
configSchema:
|
|
7
|
+
type: object
|
|
8
|
+
required: [apiKey]
|
|
9
|
+
properties:
|
|
10
|
+
apiKey:
|
|
11
|
+
type: string
|
|
12
|
+
title: Groundmail API Key
|
|
13
|
+
description: API key starting with gm_
|
|
14
|
+
apiUrl:
|
|
15
|
+
type: string
|
|
16
|
+
title: API Base URL
|
|
17
|
+
description: Optional override (default https://api.groundmail.dev)
|
|
18
|
+
commandFunction: |-
|
|
19
|
+
(config) => ({
|
|
20
|
+
command: 'npx',
|
|
21
|
+
args: ['-y', 'groundmail-mcp'],
|
|
22
|
+
env: {
|
|
23
|
+
GROUNDMAIL_API_KEY: config.apiKey,
|
|
24
|
+
...(config.apiUrl ? { GROUNDMAIL_API_URL: config.apiUrl } : {}),
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
exampleConfig:
|
|
29
|
+
apiKey: gm_your_key_here
|