groundmail-mcp 0.9.0 → 0.11.0

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +420 -18
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -55,7 +55,7 @@ When you connect, you choose the access: **Full**, **Send only** or **Read only*
55
55
  >
56
56
  > "How is my deliverability over the last 7 days?"
57
57
 
58
- Tools cover emails (send, schedule, cancel, look up), domains (add, apply DNS, verify), templates, webhooks, suppressions and deliverability. Each tool carries read-only and destructive annotations and an output schema. Full list: [docs.groundmail.dev/ai/mcp](https://docs.groundmail.dev/ai/mcp).
58
+ Tools cover emails (send, schedule, cancel, look up), domains (add, apply DNS, verify), templates, webhooks, suppressions, deliverability and tenants (one per customer you send for: create, update, pause, resume, stats; `tenant` on sends, domains and suppressions). Each tool carries read-only and destructive annotations and an output schema. Full list: [docs.groundmail.dev/ai/mcp](https://docs.groundmail.dev/ai/mcp).
59
59
 
60
60
  ## Transports
61
61
 
package/dist/index.js CHANGED
@@ -161,7 +161,8 @@ var emailItem = object(
161
161
  delivered_at: nullableString("When the email was delivered (ISO 8601)"),
162
162
  bounced_at: nullableString("When the email bounced (ISO 8601)"),
163
163
  opened_at: nullableString("First open (ISO 8601); needs open tracking on the sending domain"),
164
- clicked_at: nullableString("First tracked-link click (ISO 8601); needs click tracking on the sending domain")
164
+ clicked_at: nullableString("First tracked-link click (ISO 8601); needs click tracking on the sending domain"),
165
+ tenant: nullableString("External id of the tenant the email was sent as. Present only when it was sent as a tenant")
165
166
  },
166
167
  ["id", "from", "to", "subject", "status"]
167
168
  );
@@ -179,7 +180,8 @@ var domainItem = object(
179
180
  tls_mode: string("TLS mode for outgoing mail: opportunistic or enforced"),
180
181
  inbound_enabled: boolean("Whether inbound receiving is enabled"),
181
182
  inbound_status: string("Inbound status: disabled, pending, active or error"),
182
- inbound_error: nullableString("Inbound setup error, if any")
183
+ inbound_error: nullableString("Inbound setup error, if any"),
184
+ tenant: nullableString("External id of the tenant that owns the domain. Absent for a domain the whole account can use")
183
185
  },
184
186
  ["id", "name", "status"]
185
187
  );
@@ -200,6 +202,7 @@ var webhookItem = object(
200
202
  url: string("URL that receives events"),
201
203
  events: stringArray("Event types delivered to this endpoint"),
202
204
  enabled: boolean("Whether the endpoint receives events"),
205
+ tenants: stringArray("Present only when the endpoint receives email events for some tenants only: their external ids"),
203
206
  created_at: string("When the endpoint was created (ISO 8601)"),
204
207
  updated_at: string("When the endpoint was last changed (ISO 8601)")
205
208
  },
@@ -211,6 +214,7 @@ var suppressionItem = object(
211
214
  email: string("Suppressed email address"),
212
215
  reason: nullableString("Why the address was suppressed"),
213
216
  source: string("How the suppression was created, for example api or bounce"),
217
+ tenant: string("External id of the tenant, on an entry of a tenant's own suppression list"),
214
218
  created_at: string("When the suppression was created (ISO 8601)"),
215
219
  updated_at: string("When the suppression was last changed (ISO 8601)")
216
220
  },
@@ -393,6 +397,37 @@ var deliverabilityOutput = object(
393
397
  ["window", "summary"]
394
398
  );
395
399
  var anyObject = (description) => ({ type: "object", description });
400
+ var dmarcOutput = object(
401
+ {
402
+ dmarc: described(
403
+ object(
404
+ {
405
+ domain: string("Monitored domain"),
406
+ enabled: boolean("True when monitoring is on"),
407
+ status: string("waiting (no report yet), receiving, or disabled"),
408
+ report_address: string("Where receivers send the domain's aggregate reports"),
409
+ rua: string("The rua= value to add to the DMARC record (mailto:<report_address>)"),
410
+ dns: anyObject("The DMARC record now (host, record, policy), whether it lists the report address (rua_present), and suggested_record: the record to publish"),
411
+ digest_enabled: boolean("True when the weekly digest email is on"),
412
+ last_report_at: nullableString("End of the newest report received (ISO 8601)"),
413
+ window_days: number("Days of reports the summary covers"),
414
+ summary: anyObject("Totals: messages, dmarc_pass, dmarc_fail, pass_rate (0-1), reports, reporters"),
415
+ sources: arrayOf(
416
+ anyObject("A sending source: name, provider, status (passing, failing, or unknown = possible spoofing), messages, pass_rate, spf_aligned_rate, dkim_aligned_rate, ips"),
417
+ "Who sends mail as the domain, grouped by provider, busiest first"
418
+ ),
419
+ daily: arrayOf(anyObject("date, messages, dmarc_pass, pass_rate"), "Pass rate by day"),
420
+ readiness: anyObject("Whether it is safe to enforce DMARC: verdict (no_data, collecting, fix_sources, ready_quarantine, ready_reject, enforced), label, detail, blocking_sources")
421
+ },
422
+ ["domain", "enabled"]
423
+ ),
424
+ "DMARC monitoring for the domain"
425
+ ),
426
+ applied: { type: ["object", "null"], description: "When apply was used: the DNS provider and what changed" },
427
+ apply_error: string("Why the DNS provider could not update the record (add it by hand instead)")
428
+ },
429
+ ["dmarc"]
430
+ );
396
431
  var migrationOutput = object(
397
432
  {
398
433
  migration: described(
@@ -429,6 +464,80 @@ var migrationOutput = object(
429
464
  },
430
465
  ["migration"]
431
466
  );
467
+ var nullableNumber = (description) => ({ type: ["number", "null"], description });
468
+ var tenantItem = object(
469
+ {
470
+ id: string("Groundmail tenant id"),
471
+ object: string("Always tenant"),
472
+ external_id: string("Your id for this customer; unique in the account"),
473
+ name: nullableString("Display name"),
474
+ status: { type: "string", enum: ["active", "paused"], description: "active, or paused (new sends are refused)" },
475
+ paused_reason: nullableString("Why the tenant was paused"),
476
+ paused_at: nullableString("When the tenant was paused (ISO 8601)"),
477
+ pause_source: {
478
+ type: ["string", "null"],
479
+ enum: ["api", "admin", "automated", null],
480
+ description: "Who paused it: api (you), admin (Groundmail support) or automated (Groundmail's bounce and complaint checks). Only a tenant paused over the API can be resumed over the API"
481
+ },
482
+ default_from_domain: nullableString("Completes a bare from (billing \u2192 billing@<domain>) on sends that name this tenant"),
483
+ stream_policy: string("all, or transactional_only (product email is refused for this tenant)"),
484
+ daily_limit: nullableNumber("Most recipients per UTC day for this tenant; null for no tenant limit"),
485
+ metadata: { type: ["object", "null"], description: "Your own data about the tenant" },
486
+ created_at: string("When the tenant was created (ISO 8601)"),
487
+ updated_at: string("When the tenant was last changed (ISO 8601)")
488
+ },
489
+ ["id", "external_id", "status"]
490
+ );
491
+ var tenantOutput = object({ tenant: described(tenantItem, "The tenant") }, ["tenant"]);
492
+ var streamCounts = (description) => ({
493
+ ...object({
494
+ transactional: nullableNumber("Transactional email recipients"),
495
+ product: nullableNumber("Product email recipients")
496
+ }),
497
+ description
498
+ });
499
+ var tenantStatsOutput = object(
500
+ {
501
+ tenant: string("External id of the tenant"),
502
+ stats: described(
503
+ object(
504
+ {
505
+ object: string("Always tenant_stats"),
506
+ days: number("Days covered"),
507
+ from: string("Start of the window (ISO 8601)"),
508
+ total: number("Emails created in the window"),
509
+ queued: number("Emails waiting to be sent"),
510
+ scheduled: number("Emails still scheduled"),
511
+ sent: number("Emails handed off for delivery"),
512
+ delivered: number("Emails delivered"),
513
+ bounced: number("Emails that bounced"),
514
+ complained: number("Emails reported as spam"),
515
+ failed: number("Emails that failed"),
516
+ suppressed: number("Emails not sent because the recipient was suppressed"),
517
+ opened: number("Emails opened"),
518
+ clicked: number("Emails with a tracked click"),
519
+ bounce_rate: number("Bounced / (delivered + bounced + complained), as a fraction"),
520
+ complaint_rate: number("Complained / (delivered + bounced + complained), as a fraction")
521
+ },
522
+ ["days", "total"]
523
+ ),
524
+ "Email counts and rates over the window"
525
+ ),
526
+ ramp: described(
527
+ object({
528
+ object: string("Always tenant_ramp"),
529
+ active: boolean("True when a ramp-up daily limit applies to this tenant today"),
530
+ stage: nullableNumber("Ramp-up stage, 1-based; null when the ramp does not apply"),
531
+ daily_limit: { ...streamCounts("Today's limit for each stream"), type: ["object", "null"] },
532
+ used_today: streamCounts("Recipients sent today on each stream"),
533
+ completed: boolean("True when the tenant finished its ramp-up"),
534
+ resets_at: string("The next 00:00 UTC (ISO 8601)")
535
+ }),
536
+ "The tenant's sending ramp today, when known"
537
+ )
538
+ },
539
+ ["tenant", "stats"]
540
+ );
432
541
 
433
542
  // src/tools.ts
434
543
  function textResult(value, isError = false) {
@@ -463,6 +572,34 @@ function optStringArray(args, key) {
463
572
  if (Array.isArray(value)) return value.map((v) => String(v));
464
573
  return void 0;
465
574
  }
575
+ function tenantSettings(args) {
576
+ const out = {};
577
+ if (args.name !== void 0) out.name = args.name == null ? null : String(args.name);
578
+ if (args.default_from_domain !== void 0) {
579
+ out.default_from_domain = args.default_from_domain == null ? null : String(args.default_from_domain);
580
+ }
581
+ if (args.stream_policy !== void 0) {
582
+ if (args.stream_policy !== "all" && args.stream_policy !== "transactional_only") {
583
+ return "stream_policy must be all or transactional_only";
584
+ }
585
+ out.stream_policy = args.stream_policy;
586
+ }
587
+ if (args.daily_limit !== void 0) {
588
+ if (args.daily_limit === null) out.daily_limit = null;
589
+ else {
590
+ const n = Number(args.daily_limit);
591
+ if (!Number.isInteger(n) || n < 0) return "daily_limit must be a whole number, or null for no limit";
592
+ out.daily_limit = n;
593
+ }
594
+ }
595
+ if (args.metadata !== void 0) {
596
+ if (args.metadata !== null && (typeof args.metadata !== "object" || Array.isArray(args.metadata))) {
597
+ return "metadata must be an object, or null";
598
+ }
599
+ out.metadata = args.metadata;
600
+ }
601
+ return out;
602
+ }
466
603
  var EMAIL_WEBHOOK_EVENTS = /* @__PURE__ */ new Set([
467
604
  "email.received",
468
605
  "email.sent",
@@ -497,6 +634,7 @@ async function callMcpTool(client, name, args) {
497
634
  html: optString(args, "html"),
498
635
  text: optString(args, "text"),
499
636
  reply_to: optString(args, "reply_to"),
637
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {},
500
638
  ...templateId ? { template: { id: templateId, ...variables ? { variables } : {} } } : {}
501
639
  });
502
640
  return textResult(result);
@@ -505,7 +643,8 @@ async function callMcpTool(client, name, args) {
505
643
  const result = await client.emails.list({
506
644
  limit: optNumber(args, "limit"),
507
645
  cursor: optString(args, "cursor"),
508
- ...typeof args.test === "boolean" ? { test: args.test } : {}
646
+ ...typeof args.test === "boolean" ? { test: args.test } : {},
647
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
509
648
  });
510
649
  return textResult(result);
511
650
  }
@@ -542,7 +681,8 @@ async function callMcpTool(client, name, args) {
542
681
  case "list_domains": {
543
682
  const result = await client.domains.list({
544
683
  limit: optNumber(args, "limit"),
545
- cursor: optString(args, "cursor")
684
+ cursor: optString(args, "cursor"),
685
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
546
686
  });
547
687
  return textResult(result);
548
688
  }
@@ -553,7 +693,8 @@ async function callMcpTool(client, name, args) {
553
693
  return textResult(
554
694
  await client.domains.create({
555
695
  name: domainName,
556
- ...inbound !== void 0 ? { inbound_enabled: inbound } : {}
696
+ ...inbound !== void 0 ? { inbound_enabled: inbound } : {},
697
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
557
698
  })
558
699
  );
559
700
  }
@@ -569,10 +710,12 @@ async function callMcpTool(client, name, args) {
569
710
  const url = optString(args, "url");
570
711
  if (!url) return textResult("Missing required argument: url", true);
571
712
  const events = optStringArray(args, "events");
713
+ const tenants = optStringArray(args, "tenants");
572
714
  return textResult(
573
715
  await client.webhooks.create({
574
716
  url,
575
- ...events ? { events } : {}
717
+ ...events ? { events } : {},
718
+ ...tenants?.length ? { tenants } : {}
576
719
  })
577
720
  );
578
721
  }
@@ -586,7 +729,8 @@ async function callMcpTool(client, name, args) {
586
729
  await client.suppressions.list({
587
730
  limit: optNumber(args, "limit"),
588
731
  cursor: optString(args, "cursor"),
589
- email: optString(args, "email")
732
+ email: optString(args, "email"),
733
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
590
734
  })
591
735
  );
592
736
  }
@@ -601,7 +745,8 @@ async function callMcpTool(client, name, args) {
601
745
  return textResult(
602
746
  await client.suppressions.create({
603
747
  email,
604
- reason: optString(args, "reason")
748
+ reason: optString(args, "reason"),
749
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
605
750
  })
606
751
  );
607
752
  }
@@ -610,7 +755,8 @@ async function callMcpTool(client, name, args) {
610
755
  if (!idOrEmail) {
611
756
  return textResult("Missing required argument: id or email", true);
612
757
  }
613
- return textResult(await client.suppressions.remove(idOrEmail));
758
+ const tenant = optString(args, "tenant");
759
+ return textResult(tenant ? await client.suppressions.remove(idOrEmail, { tenant }) : await client.suppressions.remove(idOrEmail));
614
760
  }
615
761
  case "list_templates": {
616
762
  const status = optString(args, "status");
@@ -661,10 +807,79 @@ async function callMcpTool(client, name, args) {
661
807
  await client.emails.testWebhook({
662
808
  event,
663
809
  email_id: optString(args, "email_id"),
664
- deliver: optBoolean(args, "deliver")
810
+ deliver: optBoolean(args, "deliver"),
811
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
665
812
  })
666
813
  );
667
814
  }
815
+ case "list_tenants": {
816
+ const status = optString(args, "status");
817
+ return textResult(
818
+ await client.tenants.list({
819
+ limit: optNumber(args, "limit"),
820
+ cursor: optString(args, "cursor"),
821
+ ...status === "active" || status === "paused" ? { status } : {}
822
+ })
823
+ );
824
+ }
825
+ case "create_tenant": {
826
+ const externalId = optString(args, "external_id");
827
+ if (!externalId) return textResult("Missing required argument: external_id", true);
828
+ const settings = tenantSettings(args);
829
+ if (typeof settings === "string") return textResult(settings, true);
830
+ const { name: name2, ...rest } = settings;
831
+ return textResult(
832
+ await client.tenants.create({ external_id: externalId, ...name2 ? { name: name2 } : {}, ...rest })
833
+ );
834
+ }
835
+ case "get_tenant": {
836
+ const tenant = optString(args, "tenant");
837
+ if (!tenant) return textResult("Missing required argument: tenant", true);
838
+ return textResult(await client.tenants.get(tenant));
839
+ }
840
+ case "update_tenant": {
841
+ const tenant = optString(args, "tenant");
842
+ if (!tenant) return textResult("Missing required argument: tenant", true);
843
+ const settings = tenantSettings(args);
844
+ if (typeof settings === "string") return textResult(settings, true);
845
+ if (!Object.keys(settings).length) {
846
+ return textResult(
847
+ "Nothing to update: pass name, default_from_domain, stream_policy, daily_limit or metadata (use pause_tenant / resume_tenant to pause or resume)",
848
+ true
849
+ );
850
+ }
851
+ return textResult(await client.tenants.update(tenant, settings));
852
+ }
853
+ case "pause_tenant": {
854
+ const tenant = optString(args, "tenant");
855
+ if (!tenant) return textResult("Missing required argument: tenant", true);
856
+ return textResult(await client.tenants.pause(tenant, optString(args, "reason")));
857
+ }
858
+ case "resume_tenant": {
859
+ const tenant = optString(args, "tenant");
860
+ if (!tenant) return textResult("Missing required argument: tenant", true);
861
+ return textResult(await client.tenants.resume(tenant));
862
+ }
863
+ case "get_tenant_stats": {
864
+ const tenant = optString(args, "tenant");
865
+ if (!tenant) return textResult("Missing required argument: tenant", true);
866
+ const days = optNumber(args, "days");
867
+ return textResult(await client.tenants.stats(tenant, days !== void 0 ? { days } : {}));
868
+ }
869
+ case "get_dmarc_report": {
870
+ const domain = optString(args, "domain");
871
+ if (!domain) return textResult("Missing required argument: domain", true);
872
+ return textResult(await client.dmarc.get(domain));
873
+ }
874
+ case "enable_dmarc_monitoring": {
875
+ const domain = optString(args, "domain");
876
+ if (!domain) return textResult("Missing required argument: domain", true);
877
+ const apply = optString(args, "apply");
878
+ if (apply && apply !== "cloudflare" && apply !== "vercel" && apply !== "godaddy") {
879
+ return textResult("apply must be cloudflare, vercel or godaddy", true);
880
+ }
881
+ return textResult(await client.dmarc.enable(domain, apply ? { apply } : {}));
882
+ }
668
883
  case "get_deliverability": {
669
884
  const window = args.window === "7d" ? "7d" : "30d";
670
885
  const result = await client.emails.insights(window);
@@ -738,6 +953,27 @@ var paginationProps = {
738
953
  limit: { type: "number", description: "Page size (1\u2013100)" },
739
954
  cursor: { type: "string", description: "Opaque cursor for the next page" }
740
955
  };
956
+ var tenantProp = (description) => ({
957
+ type: "string",
958
+ description: `${description}. The tenant's external id (your id for that customer) or its Groundmail id`
959
+ });
960
+ var tenantSettingsProps = {
961
+ name: { type: "string", description: "Display name" },
962
+ default_from_domain: {
963
+ type: ["string", "null"],
964
+ description: "Domain that completes a bare from (billing \u2192 billing@<domain>) on sends naming this tenant; null to clear"
965
+ },
966
+ stream_policy: {
967
+ type: "string",
968
+ enum: ["all", "transactional_only"],
969
+ description: "all (default), or transactional_only to refuse product email for this tenant"
970
+ },
971
+ daily_limit: {
972
+ type: ["number", "null"],
973
+ description: "Most recipients per UTC day for this tenant; null for no tenant limit"
974
+ },
975
+ metadata: { type: ["object", "null"], description: "Your own data about the tenant (JSON object); null to clear" }
976
+ };
741
977
  var readOnly = (title) => ({
742
978
  title,
743
979
  readOnlyHint: true,
@@ -767,7 +1003,10 @@ var MCP_TOOLS = [
767
1003
  type: "object",
768
1004
  description: "Template variables when using template_id or template_alias",
769
1005
  additionalProperties: true
770
- }
1006
+ },
1007
+ tenant: tenantProp(
1008
+ "Send as this tenant: the send uses its domains, suppression list, pause switch and daily limit. Omit to send from the account as a whole"
1009
+ )
771
1010
  },
772
1011
  required: ["from", "to"]
773
1012
  },
@@ -787,7 +1026,8 @@ var MCP_TOOLS = [
787
1026
  type: "object",
788
1027
  properties: {
789
1028
  ...paginationProps,
790
- test: { type: "boolean", description: "true: only test emails; false: only live ones. Omit for both" }
1029
+ test: { type: "boolean", description: "true: only test emails; false: only live ones. Omit for both" },
1030
+ tenant: tenantProp("Only emails sent as this tenant")
791
1031
  }
792
1032
  },
793
1033
  outputSchema: pagedOutput("emails", emailItem, "Emails on this page, newest first"),
@@ -811,7 +1051,7 @@ var MCP_TOOLS = [
811
1051
  description: "List sending domains for this account.",
812
1052
  inputSchema: {
813
1053
  type: "object",
814
- properties: { ...paginationProps }
1054
+ properties: { ...paginationProps, tenant: tenantProp("Only domains this tenant owns") }
815
1055
  },
816
1056
  outputSchema: pagedOutput("domains", domainItem, "Sending domains on this page"),
817
1057
  annotations: readOnly("List domains")
@@ -823,7 +1063,8 @@ var MCP_TOOLS = [
823
1063
  type: "object",
824
1064
  properties: {
825
1065
  name: { type: "string", description: "Domain name (example.com)" },
826
- inbound_enabled: { type: "boolean", description: "Enable inbound receiving on this domain" }
1066
+ inbound_enabled: { type: "boolean", description: "Enable inbound receiving on this domain" },
1067
+ tenant: tenantProp("Give the domain to this tenant; only that tenant can then send from it")
827
1068
  },
828
1069
  required: ["name"]
829
1070
  },
@@ -907,6 +1148,11 @@ var MCP_TOOLS = [
907
1148
  type: "array",
908
1149
  items: { type: "string" },
909
1150
  description: "Optional event types (defaults apply when omitted)"
1151
+ },
1152
+ tenants: {
1153
+ type: "array",
1154
+ items: { type: "string" },
1155
+ description: "Optional: only these tenants' email events (external ids or Groundmail ids). Omit for all"
910
1156
  }
911
1157
  },
912
1158
  required: ["url"]
@@ -946,7 +1192,8 @@ var MCP_TOOLS = [
946
1192
  type: "object",
947
1193
  properties: {
948
1194
  ...paginationProps,
949
- email: { type: "string", description: "Optional filter by email" }
1195
+ email: { type: "string", description: "Optional filter by email" },
1196
+ tenant: tenantProp("List this tenant's own suppression list instead of the account's")
950
1197
  }
951
1198
  },
952
1199
  outputSchema: pagedOutput("data", suppressionItem, "Suppressed addresses on this page"),
@@ -972,7 +1219,8 @@ var MCP_TOOLS = [
972
1219
  type: "object",
973
1220
  properties: {
974
1221
  email: { type: "string", description: "Email address to stop sending to" },
975
- reason: { type: "string", description: "Optional note on why the address is suppressed" }
1222
+ reason: { type: "string", description: "Optional note on why the address is suppressed" },
1223
+ tenant: tenantProp("Add to this tenant's own list instead of the account's")
976
1224
  },
977
1225
  required: ["email"]
978
1226
  },
@@ -993,7 +1241,8 @@ var MCP_TOOLS = [
993
1241
  type: "object",
994
1242
  properties: {
995
1243
  id: { type: "string", description: "Suppression id" },
996
- email: { type: "string", description: "Email address (alternative to id)" }
1244
+ email: { type: "string", description: "Email address (alternative to id)" },
1245
+ tenant: tenantProp("Remove the address (by email) from this tenant's own list")
997
1246
  }
998
1247
  },
999
1248
  outputSchema: okOutput,
@@ -1090,7 +1339,8 @@ var MCP_TOOLS = [
1090
1339
  deliver: {
1091
1340
  type: "boolean",
1092
1341
  description: "When true (default), enqueue delivery to account webhooks"
1093
- }
1342
+ },
1343
+ tenant: tenantProp("Put this tenant on the test event, to test endpoints that only receive some tenants")
1094
1344
  },
1095
1345
  required: ["event"]
1096
1346
  },
@@ -1103,6 +1353,158 @@ var MCP_TOOLS = [
1103
1353
  openWorldHint: true
1104
1354
  }
1105
1355
  },
1356
+ {
1357
+ name: "list_tenants",
1358
+ description: "List tenants: one per customer you send email for, each with its own domains, suppression list, stats, pause switch and daily limit. Tenants are included on the Scale and Dedicated plans.",
1359
+ inputSchema: {
1360
+ type: "object",
1361
+ properties: {
1362
+ ...paginationProps,
1363
+ status: { type: "string", enum: ["active", "paused"], description: "Optional filter by status" }
1364
+ }
1365
+ },
1366
+ outputSchema: pagedOutput("data", tenantItem, "Tenants on this page, newest first"),
1367
+ annotations: readOnly("List tenants")
1368
+ },
1369
+ {
1370
+ name: "create_tenant",
1371
+ description: "Create a tenant for one of your customers. Then send with tenant set to its external id, and give it its own domains with create_domain (tenant).",
1372
+ inputSchema: {
1373
+ type: "object",
1374
+ properties: {
1375
+ external_id: {
1376
+ type: "string",
1377
+ description: "Your id for this customer: 1\u2013128 characters of letters, digits and . _ : @ -. Cannot be changed later"
1378
+ },
1379
+ ...tenantSettingsProps
1380
+ },
1381
+ required: ["external_id"]
1382
+ },
1383
+ outputSchema: tenantOutput,
1384
+ annotations: {
1385
+ title: "Create tenant",
1386
+ readOnlyHint: false,
1387
+ destructiveHint: false,
1388
+ idempotentHint: false,
1389
+ openWorldHint: false
1390
+ }
1391
+ },
1392
+ {
1393
+ name: "get_tenant",
1394
+ description: "Get one tenant, including whether it is paused and why.",
1395
+ inputSchema: {
1396
+ type: "object",
1397
+ properties: { tenant: tenantProp("The tenant") },
1398
+ required: ["tenant"]
1399
+ },
1400
+ outputSchema: tenantOutput,
1401
+ annotations: readOnly("Get tenant")
1402
+ },
1403
+ {
1404
+ name: "update_tenant",
1405
+ description: "Change a tenant's name, default from domain, stream policy, daily limit or metadata.",
1406
+ inputSchema: {
1407
+ type: "object",
1408
+ properties: { tenant: tenantProp("The tenant to change"), ...tenantSettingsProps },
1409
+ required: ["tenant"]
1410
+ },
1411
+ outputSchema: tenantOutput,
1412
+ annotations: {
1413
+ title: "Update tenant",
1414
+ readOnlyHint: false,
1415
+ destructiveHint: false,
1416
+ idempotentHint: true,
1417
+ openWorldHint: false
1418
+ }
1419
+ },
1420
+ {
1421
+ name: "pause_tenant",
1422
+ description: "Pause a tenant: its new sends are refused until it is resumed. The rest of the account keeps sending. Confirm with the user first.",
1423
+ inputSchema: {
1424
+ type: "object",
1425
+ properties: {
1426
+ tenant: tenantProp("The tenant to pause"),
1427
+ reason: { type: "string", description: "Why it is paused (shown on the tenant)" }
1428
+ },
1429
+ required: ["tenant"]
1430
+ },
1431
+ outputSchema: tenantOutput,
1432
+ annotations: {
1433
+ title: "Pause tenant",
1434
+ readOnlyHint: false,
1435
+ destructiveHint: false,
1436
+ idempotentHint: true,
1437
+ openWorldHint: false
1438
+ }
1439
+ },
1440
+ {
1441
+ name: "resume_tenant",
1442
+ description: "Resume a paused tenant so it can send again. A tenant Groundmail paused (pause_source automated or admin) cannot be resumed this way; the user should fix the cause and contact support.",
1443
+ inputSchema: {
1444
+ type: "object",
1445
+ properties: { tenant: tenantProp("The tenant to resume") },
1446
+ required: ["tenant"]
1447
+ },
1448
+ outputSchema: tenantOutput,
1449
+ annotations: {
1450
+ title: "Resume tenant",
1451
+ readOnlyHint: false,
1452
+ destructiveHint: false,
1453
+ idempotentHint: true,
1454
+ openWorldHint: false
1455
+ }
1456
+ },
1457
+ {
1458
+ name: "get_tenant_stats",
1459
+ description: "A tenant's email counts, bounce and complaint rates over the last days (1\u201390, default 7), and its sending ramp today.",
1460
+ inputSchema: {
1461
+ type: "object",
1462
+ properties: {
1463
+ tenant: tenantProp("The tenant"),
1464
+ days: { type: "number", description: "Days to cover, 1\u201390 (default 7)" }
1465
+ },
1466
+ required: ["tenant"]
1467
+ },
1468
+ outputSchema: tenantStatsOutput,
1469
+ annotations: readOnly("Get tenant stats")
1470
+ },
1471
+ {
1472
+ name: "get_dmarc_report",
1473
+ description: "DMARC monitoring for a sending domain: who sends mail as it (grouped by provider, with unknown senders flagged as possible spoofing), SPF/DKIM/DMARC pass rates over the last 30 days, and whether it is safe to move the DMARC policy to quarantine. If monitoring is off, shows the current DMARC record; turn it on with enable_dmarc_monitoring.",
1474
+ inputSchema: {
1475
+ type: "object",
1476
+ properties: {
1477
+ domain: { type: "string", description: "Domain name or id (example.com)" }
1478
+ },
1479
+ required: ["domain"]
1480
+ },
1481
+ outputSchema: dmarcOutput,
1482
+ annotations: readOnly("Get DMARC report")
1483
+ },
1484
+ {
1485
+ name: "enable_dmarc_monitoring",
1486
+ description: "Turn on free DMARC monitoring for a sending domain. Returns the report address and the exact DMARC record to publish (the current record with the address added to rua=, keeping the policy and other report addresses). Pass apply to update the record through a DNS provider connected in the dashboard. Reports start arriving within 24 to 48 hours.",
1487
+ inputSchema: {
1488
+ type: "object",
1489
+ properties: {
1490
+ domain: { type: "string", description: "Domain name or id (example.com)" },
1491
+ apply: {
1492
+ type: "string",
1493
+ enum: ["cloudflare", "vercel", "godaddy"],
1494
+ description: "Also write the DMARC record through this DNS provider (connected under Settings \u2192 Integrations)"
1495
+ }
1496
+ },
1497
+ required: ["domain"]
1498
+ },
1499
+ outputSchema: dmarcOutput,
1500
+ annotations: {
1501
+ title: "Enable DMARC monitoring",
1502
+ readOnlyHint: false,
1503
+ destructiveHint: false,
1504
+ idempotentHint: true,
1505
+ openWorldHint: true
1506
+ }
1507
+ },
1106
1508
  {
1107
1509
  name: "get_deliverability",
1108
1510
  description: "Fetch best-effort deliverability metrics for recent sends (7d or 30d window).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundmail-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "mcpName": "io.github.Groundmail/groundmail",
5
5
  "description": "Groundmail MCP server: send transactional email, manage domains, templates, webhooks and suppressions from Claude, Cursor, ChatGPT and other MCP clients",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@modelcontextprotocol/sdk": "^1.30.0",
44
- "groundmail": "0.17.0"
44
+ "groundmail": "0.19.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.5.0",