groundmail-mcp 0.9.0 → 0.11.1

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 +421 -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,11 +572,40 @@ 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",
469
606
  "email.delivered",
470
607
  "email.delivery_delayed",
608
+ "email.held",
471
609
  "email.bounced",
472
610
  "email.complained",
473
611
  "email.opened",
@@ -497,6 +635,7 @@ async function callMcpTool(client, name, args) {
497
635
  html: optString(args, "html"),
498
636
  text: optString(args, "text"),
499
637
  reply_to: optString(args, "reply_to"),
638
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {},
500
639
  ...templateId ? { template: { id: templateId, ...variables ? { variables } : {} } } : {}
501
640
  });
502
641
  return textResult(result);
@@ -505,7 +644,8 @@ async function callMcpTool(client, name, args) {
505
644
  const result = await client.emails.list({
506
645
  limit: optNumber(args, "limit"),
507
646
  cursor: optString(args, "cursor"),
508
- ...typeof args.test === "boolean" ? { test: args.test } : {}
647
+ ...typeof args.test === "boolean" ? { test: args.test } : {},
648
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
509
649
  });
510
650
  return textResult(result);
511
651
  }
@@ -542,7 +682,8 @@ async function callMcpTool(client, name, args) {
542
682
  case "list_domains": {
543
683
  const result = await client.domains.list({
544
684
  limit: optNumber(args, "limit"),
545
- cursor: optString(args, "cursor")
685
+ cursor: optString(args, "cursor"),
686
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
546
687
  });
547
688
  return textResult(result);
548
689
  }
@@ -553,7 +694,8 @@ async function callMcpTool(client, name, args) {
553
694
  return textResult(
554
695
  await client.domains.create({
555
696
  name: domainName,
556
- ...inbound !== void 0 ? { inbound_enabled: inbound } : {}
697
+ ...inbound !== void 0 ? { inbound_enabled: inbound } : {},
698
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
557
699
  })
558
700
  );
559
701
  }
@@ -569,10 +711,12 @@ async function callMcpTool(client, name, args) {
569
711
  const url = optString(args, "url");
570
712
  if (!url) return textResult("Missing required argument: url", true);
571
713
  const events = optStringArray(args, "events");
714
+ const tenants = optStringArray(args, "tenants");
572
715
  return textResult(
573
716
  await client.webhooks.create({
574
717
  url,
575
- ...events ? { events } : {}
718
+ ...events ? { events } : {},
719
+ ...tenants?.length ? { tenants } : {}
576
720
  })
577
721
  );
578
722
  }
@@ -586,7 +730,8 @@ async function callMcpTool(client, name, args) {
586
730
  await client.suppressions.list({
587
731
  limit: optNumber(args, "limit"),
588
732
  cursor: optString(args, "cursor"),
589
- email: optString(args, "email")
733
+ email: optString(args, "email"),
734
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
590
735
  })
591
736
  );
592
737
  }
@@ -601,7 +746,8 @@ async function callMcpTool(client, name, args) {
601
746
  return textResult(
602
747
  await client.suppressions.create({
603
748
  email,
604
- reason: optString(args, "reason")
749
+ reason: optString(args, "reason"),
750
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
605
751
  })
606
752
  );
607
753
  }
@@ -610,7 +756,8 @@ async function callMcpTool(client, name, args) {
610
756
  if (!idOrEmail) {
611
757
  return textResult("Missing required argument: id or email", true);
612
758
  }
613
- return textResult(await client.suppressions.remove(idOrEmail));
759
+ const tenant = optString(args, "tenant");
760
+ return textResult(tenant ? await client.suppressions.remove(idOrEmail, { tenant }) : await client.suppressions.remove(idOrEmail));
614
761
  }
615
762
  case "list_templates": {
616
763
  const status = optString(args, "status");
@@ -661,10 +808,79 @@ async function callMcpTool(client, name, args) {
661
808
  await client.emails.testWebhook({
662
809
  event,
663
810
  email_id: optString(args, "email_id"),
664
- deliver: optBoolean(args, "deliver")
811
+ deliver: optBoolean(args, "deliver"),
812
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
665
813
  })
666
814
  );
667
815
  }
816
+ case "list_tenants": {
817
+ const status = optString(args, "status");
818
+ return textResult(
819
+ await client.tenants.list({
820
+ limit: optNumber(args, "limit"),
821
+ cursor: optString(args, "cursor"),
822
+ ...status === "active" || status === "paused" ? { status } : {}
823
+ })
824
+ );
825
+ }
826
+ case "create_tenant": {
827
+ const externalId = optString(args, "external_id");
828
+ if (!externalId) return textResult("Missing required argument: external_id", true);
829
+ const settings = tenantSettings(args);
830
+ if (typeof settings === "string") return textResult(settings, true);
831
+ const { name: name2, ...rest } = settings;
832
+ return textResult(
833
+ await client.tenants.create({ external_id: externalId, ...name2 ? { name: name2 } : {}, ...rest })
834
+ );
835
+ }
836
+ case "get_tenant": {
837
+ const tenant = optString(args, "tenant");
838
+ if (!tenant) return textResult("Missing required argument: tenant", true);
839
+ return textResult(await client.tenants.get(tenant));
840
+ }
841
+ case "update_tenant": {
842
+ const tenant = optString(args, "tenant");
843
+ if (!tenant) return textResult("Missing required argument: tenant", true);
844
+ const settings = tenantSettings(args);
845
+ if (typeof settings === "string") return textResult(settings, true);
846
+ if (!Object.keys(settings).length) {
847
+ return textResult(
848
+ "Nothing to update: pass name, default_from_domain, stream_policy, daily_limit or metadata (use pause_tenant / resume_tenant to pause or resume)",
849
+ true
850
+ );
851
+ }
852
+ return textResult(await client.tenants.update(tenant, settings));
853
+ }
854
+ case "pause_tenant": {
855
+ const tenant = optString(args, "tenant");
856
+ if (!tenant) return textResult("Missing required argument: tenant", true);
857
+ return textResult(await client.tenants.pause(tenant, optString(args, "reason")));
858
+ }
859
+ case "resume_tenant": {
860
+ const tenant = optString(args, "tenant");
861
+ if (!tenant) return textResult("Missing required argument: tenant", true);
862
+ return textResult(await client.tenants.resume(tenant));
863
+ }
864
+ case "get_tenant_stats": {
865
+ const tenant = optString(args, "tenant");
866
+ if (!tenant) return textResult("Missing required argument: tenant", true);
867
+ const days = optNumber(args, "days");
868
+ return textResult(await client.tenants.stats(tenant, days !== void 0 ? { days } : {}));
869
+ }
870
+ case "get_dmarc_report": {
871
+ const domain = optString(args, "domain");
872
+ if (!domain) return textResult("Missing required argument: domain", true);
873
+ return textResult(await client.dmarc.get(domain));
874
+ }
875
+ case "enable_dmarc_monitoring": {
876
+ const domain = optString(args, "domain");
877
+ if (!domain) return textResult("Missing required argument: domain", true);
878
+ const apply = optString(args, "apply");
879
+ if (apply && apply !== "cloudflare" && apply !== "vercel" && apply !== "godaddy") {
880
+ return textResult("apply must be cloudflare, vercel or godaddy", true);
881
+ }
882
+ return textResult(await client.dmarc.enable(domain, apply ? { apply } : {}));
883
+ }
668
884
  case "get_deliverability": {
669
885
  const window = args.window === "7d" ? "7d" : "30d";
670
886
  const result = await client.emails.insights(window);
@@ -738,6 +954,27 @@ var paginationProps = {
738
954
  limit: { type: "number", description: "Page size (1\u2013100)" },
739
955
  cursor: { type: "string", description: "Opaque cursor for the next page" }
740
956
  };
957
+ var tenantProp = (description) => ({
958
+ type: "string",
959
+ description: `${description}. The tenant's external id (your id for that customer) or its Groundmail id`
960
+ });
961
+ var tenantSettingsProps = {
962
+ name: { type: "string", description: "Display name" },
963
+ default_from_domain: {
964
+ type: ["string", "null"],
965
+ description: "Domain that completes a bare from (billing \u2192 billing@<domain>) on sends naming this tenant; null to clear"
966
+ },
967
+ stream_policy: {
968
+ type: "string",
969
+ enum: ["all", "transactional_only"],
970
+ description: "all (default), or transactional_only to refuse product email for this tenant"
971
+ },
972
+ daily_limit: {
973
+ type: ["number", "null"],
974
+ description: "Most recipients per UTC day for this tenant; null for no tenant limit"
975
+ },
976
+ metadata: { type: ["object", "null"], description: "Your own data about the tenant (JSON object); null to clear" }
977
+ };
741
978
  var readOnly = (title) => ({
742
979
  title,
743
980
  readOnlyHint: true,
@@ -767,7 +1004,10 @@ var MCP_TOOLS = [
767
1004
  type: "object",
768
1005
  description: "Template variables when using template_id or template_alias",
769
1006
  additionalProperties: true
770
- }
1007
+ },
1008
+ tenant: tenantProp(
1009
+ "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"
1010
+ )
771
1011
  },
772
1012
  required: ["from", "to"]
773
1013
  },
@@ -787,7 +1027,8 @@ var MCP_TOOLS = [
787
1027
  type: "object",
788
1028
  properties: {
789
1029
  ...paginationProps,
790
- test: { type: "boolean", description: "true: only test emails; false: only live ones. Omit for both" }
1030
+ test: { type: "boolean", description: "true: only test emails; false: only live ones. Omit for both" },
1031
+ tenant: tenantProp("Only emails sent as this tenant")
791
1032
  }
792
1033
  },
793
1034
  outputSchema: pagedOutput("emails", emailItem, "Emails on this page, newest first"),
@@ -811,7 +1052,7 @@ var MCP_TOOLS = [
811
1052
  description: "List sending domains for this account.",
812
1053
  inputSchema: {
813
1054
  type: "object",
814
- properties: { ...paginationProps }
1055
+ properties: { ...paginationProps, tenant: tenantProp("Only domains this tenant owns") }
815
1056
  },
816
1057
  outputSchema: pagedOutput("domains", domainItem, "Sending domains on this page"),
817
1058
  annotations: readOnly("List domains")
@@ -823,7 +1064,8 @@ var MCP_TOOLS = [
823
1064
  type: "object",
824
1065
  properties: {
825
1066
  name: { type: "string", description: "Domain name (example.com)" },
826
- inbound_enabled: { type: "boolean", description: "Enable inbound receiving on this domain" }
1067
+ inbound_enabled: { type: "boolean", description: "Enable inbound receiving on this domain" },
1068
+ tenant: tenantProp("Give the domain to this tenant; only that tenant can then send from it")
827
1069
  },
828
1070
  required: ["name"]
829
1071
  },
@@ -907,6 +1149,11 @@ var MCP_TOOLS = [
907
1149
  type: "array",
908
1150
  items: { type: "string" },
909
1151
  description: "Optional event types (defaults apply when omitted)"
1152
+ },
1153
+ tenants: {
1154
+ type: "array",
1155
+ items: { type: "string" },
1156
+ description: "Optional: only these tenants' email events (external ids or Groundmail ids). Omit for all"
910
1157
  }
911
1158
  },
912
1159
  required: ["url"]
@@ -946,7 +1193,8 @@ var MCP_TOOLS = [
946
1193
  type: "object",
947
1194
  properties: {
948
1195
  ...paginationProps,
949
- email: { type: "string", description: "Optional filter by email" }
1196
+ email: { type: "string", description: "Optional filter by email" },
1197
+ tenant: tenantProp("List this tenant's own suppression list instead of the account's")
950
1198
  }
951
1199
  },
952
1200
  outputSchema: pagedOutput("data", suppressionItem, "Suppressed addresses on this page"),
@@ -972,7 +1220,8 @@ var MCP_TOOLS = [
972
1220
  type: "object",
973
1221
  properties: {
974
1222
  email: { type: "string", description: "Email address to stop sending to" },
975
- reason: { type: "string", description: "Optional note on why the address is suppressed" }
1223
+ reason: { type: "string", description: "Optional note on why the address is suppressed" },
1224
+ tenant: tenantProp("Add to this tenant's own list instead of the account's")
976
1225
  },
977
1226
  required: ["email"]
978
1227
  },
@@ -993,7 +1242,8 @@ var MCP_TOOLS = [
993
1242
  type: "object",
994
1243
  properties: {
995
1244
  id: { type: "string", description: "Suppression id" },
996
- email: { type: "string", description: "Email address (alternative to id)" }
1245
+ email: { type: "string", description: "Email address (alternative to id)" },
1246
+ tenant: tenantProp("Remove the address (by email) from this tenant's own list")
997
1247
  }
998
1248
  },
999
1249
  outputSchema: okOutput,
@@ -1090,7 +1340,8 @@ var MCP_TOOLS = [
1090
1340
  deliver: {
1091
1341
  type: "boolean",
1092
1342
  description: "When true (default), enqueue delivery to account webhooks"
1093
- }
1343
+ },
1344
+ tenant: tenantProp("Put this tenant on the test event, to test endpoints that only receive some tenants")
1094
1345
  },
1095
1346
  required: ["event"]
1096
1347
  },
@@ -1103,6 +1354,158 @@ var MCP_TOOLS = [
1103
1354
  openWorldHint: true
1104
1355
  }
1105
1356
  },
1357
+ {
1358
+ name: "list_tenants",
1359
+ 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.",
1360
+ inputSchema: {
1361
+ type: "object",
1362
+ properties: {
1363
+ ...paginationProps,
1364
+ status: { type: "string", enum: ["active", "paused"], description: "Optional filter by status" }
1365
+ }
1366
+ },
1367
+ outputSchema: pagedOutput("data", tenantItem, "Tenants on this page, newest first"),
1368
+ annotations: readOnly("List tenants")
1369
+ },
1370
+ {
1371
+ name: "create_tenant",
1372
+ 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).",
1373
+ inputSchema: {
1374
+ type: "object",
1375
+ properties: {
1376
+ external_id: {
1377
+ type: "string",
1378
+ description: "Your id for this customer: 1\u2013128 characters of letters, digits and . _ : @ -. Cannot be changed later"
1379
+ },
1380
+ ...tenantSettingsProps
1381
+ },
1382
+ required: ["external_id"]
1383
+ },
1384
+ outputSchema: tenantOutput,
1385
+ annotations: {
1386
+ title: "Create tenant",
1387
+ readOnlyHint: false,
1388
+ destructiveHint: false,
1389
+ idempotentHint: false,
1390
+ openWorldHint: false
1391
+ }
1392
+ },
1393
+ {
1394
+ name: "get_tenant",
1395
+ description: "Get one tenant, including whether it is paused and why.",
1396
+ inputSchema: {
1397
+ type: "object",
1398
+ properties: { tenant: tenantProp("The tenant") },
1399
+ required: ["tenant"]
1400
+ },
1401
+ outputSchema: tenantOutput,
1402
+ annotations: readOnly("Get tenant")
1403
+ },
1404
+ {
1405
+ name: "update_tenant",
1406
+ description: "Change a tenant's name, default from domain, stream policy, daily limit or metadata.",
1407
+ inputSchema: {
1408
+ type: "object",
1409
+ properties: { tenant: tenantProp("The tenant to change"), ...tenantSettingsProps },
1410
+ required: ["tenant"]
1411
+ },
1412
+ outputSchema: tenantOutput,
1413
+ annotations: {
1414
+ title: "Update tenant",
1415
+ readOnlyHint: false,
1416
+ destructiveHint: false,
1417
+ idempotentHint: true,
1418
+ openWorldHint: false
1419
+ }
1420
+ },
1421
+ {
1422
+ name: "pause_tenant",
1423
+ 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.",
1424
+ inputSchema: {
1425
+ type: "object",
1426
+ properties: {
1427
+ tenant: tenantProp("The tenant to pause"),
1428
+ reason: { type: "string", description: "Why it is paused (shown on the tenant)" }
1429
+ },
1430
+ required: ["tenant"]
1431
+ },
1432
+ outputSchema: tenantOutput,
1433
+ annotations: {
1434
+ title: "Pause tenant",
1435
+ readOnlyHint: false,
1436
+ destructiveHint: false,
1437
+ idempotentHint: true,
1438
+ openWorldHint: false
1439
+ }
1440
+ },
1441
+ {
1442
+ name: "resume_tenant",
1443
+ 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.",
1444
+ inputSchema: {
1445
+ type: "object",
1446
+ properties: { tenant: tenantProp("The tenant to resume") },
1447
+ required: ["tenant"]
1448
+ },
1449
+ outputSchema: tenantOutput,
1450
+ annotations: {
1451
+ title: "Resume tenant",
1452
+ readOnlyHint: false,
1453
+ destructiveHint: false,
1454
+ idempotentHint: true,
1455
+ openWorldHint: false
1456
+ }
1457
+ },
1458
+ {
1459
+ name: "get_tenant_stats",
1460
+ description: "A tenant's email counts, bounce and complaint rates over the last days (1\u201390, default 7), and its sending ramp today.",
1461
+ inputSchema: {
1462
+ type: "object",
1463
+ properties: {
1464
+ tenant: tenantProp("The tenant"),
1465
+ days: { type: "number", description: "Days to cover, 1\u201390 (default 7)" }
1466
+ },
1467
+ required: ["tenant"]
1468
+ },
1469
+ outputSchema: tenantStatsOutput,
1470
+ annotations: readOnly("Get tenant stats")
1471
+ },
1472
+ {
1473
+ name: "get_dmarc_report",
1474
+ 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.",
1475
+ inputSchema: {
1476
+ type: "object",
1477
+ properties: {
1478
+ domain: { type: "string", description: "Domain name or id (example.com)" }
1479
+ },
1480
+ required: ["domain"]
1481
+ },
1482
+ outputSchema: dmarcOutput,
1483
+ annotations: readOnly("Get DMARC report")
1484
+ },
1485
+ {
1486
+ name: "enable_dmarc_monitoring",
1487
+ 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.",
1488
+ inputSchema: {
1489
+ type: "object",
1490
+ properties: {
1491
+ domain: { type: "string", description: "Domain name or id (example.com)" },
1492
+ apply: {
1493
+ type: "string",
1494
+ enum: ["cloudflare", "vercel", "godaddy"],
1495
+ description: "Also write the DMARC record through this DNS provider (connected under Settings \u2192 Integrations)"
1496
+ }
1497
+ },
1498
+ required: ["domain"]
1499
+ },
1500
+ outputSchema: dmarcOutput,
1501
+ annotations: {
1502
+ title: "Enable DMARC monitoring",
1503
+ readOnlyHint: false,
1504
+ destructiveHint: false,
1505
+ idempotentHint: true,
1506
+ openWorldHint: true
1507
+ }
1508
+ },
1106
1509
  {
1107
1510
  name: "get_deliverability",
1108
1511
  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.1",
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.20.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.5.0",