groundmail-mcp 0.8.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 +435 -22
  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
@@ -150,6 +150,9 @@ var emailItem = object(
150
150
  heldBackItem(),
151
151
  "Recipients Groundmail validation held back before sending, with why; empty when none were"
152
152
  ),
153
+ test: boolean(
154
+ "True when the email never reached the mail server: sent with a test key (gm_test_\u2026) or only to simulator addresses. Its status and events are simulated"
155
+ ),
153
156
  tags: { description: "Tags attached to the email" },
154
157
  scheduled_at: nullableString("When the email is scheduled to send (ISO 8601)"),
155
158
  cancelled_at: nullableString("When the scheduled email was cancelled (ISO 8601)"),
@@ -158,7 +161,8 @@ var emailItem = object(
158
161
  delivered_at: nullableString("When the email was delivered (ISO 8601)"),
159
162
  bounced_at: nullableString("When the email bounced (ISO 8601)"),
160
163
  opened_at: nullableString("First open (ISO 8601); needs open tracking on the sending domain"),
161
- 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")
162
166
  },
163
167
  ["id", "from", "to", "subject", "status"]
164
168
  );
@@ -176,7 +180,8 @@ var domainItem = object(
176
180
  tls_mode: string("TLS mode for outgoing mail: opportunistic or enforced"),
177
181
  inbound_enabled: boolean("Whether inbound receiving is enabled"),
178
182
  inbound_status: string("Inbound status: disabled, pending, active or error"),
179
- 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")
180
185
  },
181
186
  ["id", "name", "status"]
182
187
  );
@@ -197,6 +202,7 @@ var webhookItem = object(
197
202
  url: string("URL that receives events"),
198
203
  events: stringArray("Event types delivered to this endpoint"),
199
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"),
200
206
  created_at: string("When the endpoint was created (ISO 8601)"),
201
207
  updated_at: string("When the endpoint was last changed (ISO 8601)")
202
208
  },
@@ -208,6 +214,7 @@ var suppressionItem = object(
208
214
  email: string("Suppressed email address"),
209
215
  reason: nullableString("Why the address was suppressed"),
210
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"),
211
218
  created_at: string("When the suppression was created (ISO 8601)"),
212
219
  updated_at: string("When the suppression was last changed (ISO 8601)")
213
220
  },
@@ -267,7 +274,8 @@ var sendEmailOutput = object(
267
274
  held_back: arrayOf(
268
275
  heldBackItem(),
269
276
  "Present when Groundmail validation held some recipients back; the email went to the rest"
270
- )
277
+ ),
278
+ test: boolean("Present and true on a test send (test key or simulator addresses): nothing was delivered")
271
279
  },
272
280
  ["id", "status"]
273
281
  );
@@ -389,6 +397,37 @@ var deliverabilityOutput = object(
389
397
  ["window", "summary"]
390
398
  );
391
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
+ );
392
431
  var migrationOutput = object(
393
432
  {
394
433
  migration: described(
@@ -425,6 +464,80 @@ var migrationOutput = object(
425
464
  },
426
465
  ["migration"]
427
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
+ );
428
541
 
429
542
  // src/tools.ts
430
543
  function textResult(value, isError = false) {
@@ -459,6 +572,34 @@ function optStringArray(args, key) {
459
572
  if (Array.isArray(value)) return value.map((v) => String(v));
460
573
  return void 0;
461
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
+ }
462
603
  var EMAIL_WEBHOOK_EVENTS = /* @__PURE__ */ new Set([
463
604
  "email.received",
464
605
  "email.sent",
@@ -493,6 +634,7 @@ async function callMcpTool(client, name, args) {
493
634
  html: optString(args, "html"),
494
635
  text: optString(args, "text"),
495
636
  reply_to: optString(args, "reply_to"),
637
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {},
496
638
  ...templateId ? { template: { id: templateId, ...variables ? { variables } : {} } } : {}
497
639
  });
498
640
  return textResult(result);
@@ -500,7 +642,9 @@ async function callMcpTool(client, name, args) {
500
642
  case "list_emails": {
501
643
  const result = await client.emails.list({
502
644
  limit: optNumber(args, "limit"),
503
- cursor: optString(args, "cursor")
645
+ cursor: optString(args, "cursor"),
646
+ ...typeof args.test === "boolean" ? { test: args.test } : {},
647
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
504
648
  });
505
649
  return textResult(result);
506
650
  }
@@ -537,7 +681,8 @@ async function callMcpTool(client, name, args) {
537
681
  case "list_domains": {
538
682
  const result = await client.domains.list({
539
683
  limit: optNumber(args, "limit"),
540
- cursor: optString(args, "cursor")
684
+ cursor: optString(args, "cursor"),
685
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
541
686
  });
542
687
  return textResult(result);
543
688
  }
@@ -548,7 +693,8 @@ async function callMcpTool(client, name, args) {
548
693
  return textResult(
549
694
  await client.domains.create({
550
695
  name: domainName,
551
- ...inbound !== void 0 ? { inbound_enabled: inbound } : {}
696
+ ...inbound !== void 0 ? { inbound_enabled: inbound } : {},
697
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
552
698
  })
553
699
  );
554
700
  }
@@ -564,10 +710,12 @@ async function callMcpTool(client, name, args) {
564
710
  const url = optString(args, "url");
565
711
  if (!url) return textResult("Missing required argument: url", true);
566
712
  const events = optStringArray(args, "events");
713
+ const tenants = optStringArray(args, "tenants");
567
714
  return textResult(
568
715
  await client.webhooks.create({
569
716
  url,
570
- ...events ? { events } : {}
717
+ ...events ? { events } : {},
718
+ ...tenants?.length ? { tenants } : {}
571
719
  })
572
720
  );
573
721
  }
@@ -581,7 +729,8 @@ async function callMcpTool(client, name, args) {
581
729
  await client.suppressions.list({
582
730
  limit: optNumber(args, "limit"),
583
731
  cursor: optString(args, "cursor"),
584
- email: optString(args, "email")
732
+ email: optString(args, "email"),
733
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
585
734
  })
586
735
  );
587
736
  }
@@ -596,7 +745,8 @@ async function callMcpTool(client, name, args) {
596
745
  return textResult(
597
746
  await client.suppressions.create({
598
747
  email,
599
- reason: optString(args, "reason")
748
+ reason: optString(args, "reason"),
749
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
600
750
  })
601
751
  );
602
752
  }
@@ -605,7 +755,8 @@ async function callMcpTool(client, name, args) {
605
755
  if (!idOrEmail) {
606
756
  return textResult("Missing required argument: id or email", true);
607
757
  }
608
- 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));
609
760
  }
610
761
  case "list_templates": {
611
762
  const status = optString(args, "status");
@@ -656,10 +807,79 @@ async function callMcpTool(client, name, args) {
656
807
  await client.emails.testWebhook({
657
808
  event,
658
809
  email_id: optString(args, "email_id"),
659
- deliver: optBoolean(args, "deliver")
810
+ deliver: optBoolean(args, "deliver"),
811
+ ...optString(args, "tenant") ? { tenant: optString(args, "tenant") } : {}
660
812
  })
661
813
  );
662
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
+ }
663
883
  case "get_deliverability": {
664
884
  const window = args.window === "7d" ? "7d" : "30d";
665
885
  const result = await client.emails.insights(window);
@@ -733,6 +953,27 @@ var paginationProps = {
733
953
  limit: { type: "number", description: "Page size (1\u2013100)" },
734
954
  cursor: { type: "string", description: "Opaque cursor for the next page" }
735
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
+ };
736
977
  var readOnly = (title) => ({
737
978
  title,
738
979
  readOnlyHint: true,
@@ -743,12 +984,15 @@ var readOnly = (title) => ({
743
984
  var MCP_TOOLS = [
744
985
  {
745
986
  name: "send_email",
746
- description: "Send a transactional email via Groundmail. Provide subject + html/text, or a published template_id / template_alias with optional variables.",
987
+ description: "Send a transactional email via Groundmail. Provide subject + html/text, or a published template_id / template_alias with optional variables. To test without delivering anything, send to a simulator address: delivered@, bounce@, block@, complaint@, suppressed@ or delayed@simulator.groundmail.dev (plus addressing like bounce+test1@ works); the result has test: true and webhooks fire as for real mail.",
747
988
  inputSchema: {
748
989
  type: "object",
749
990
  properties: {
750
991
  from: { type: "string", description: "Sender address (must match a verified domain or sandbox sender)" },
751
- to: { type: "string", description: "Recipient email address" },
992
+ to: {
993
+ type: "string",
994
+ description: "Recipient email address. A \u2026@simulator.groundmail.dev address simulates an outcome and never delivers"
995
+ },
752
996
  subject: { type: "string", description: "Subject line. Optional when using a published template" },
753
997
  html: { type: "string", description: "HTML body. Do not combine with template_id or template_alias" },
754
998
  text: { type: "string", description: "Plain-text body. Do not combine with template_id or template_alias" },
@@ -759,7 +1003,10 @@ var MCP_TOOLS = [
759
1003
  type: "object",
760
1004
  description: "Template variables when using template_id or template_alias",
761
1005
  additionalProperties: true
762
- }
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
+ )
763
1010
  },
764
1011
  required: ["from", "to"]
765
1012
  },
@@ -774,10 +1021,14 @@ var MCP_TOOLS = [
774
1021
  },
775
1022
  {
776
1023
  name: "list_emails",
777
- description: "List recent emails sent from this account.",
1024
+ description: "List recent emails sent from this account. Test emails (test keys, simulator addresses) have test: true.",
778
1025
  inputSchema: {
779
1026
  type: "object",
780
- properties: { ...paginationProps }
1027
+ properties: {
1028
+ ...paginationProps,
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")
1031
+ }
781
1032
  },
782
1033
  outputSchema: pagedOutput("emails", emailItem, "Emails on this page, newest first"),
783
1034
  annotations: readOnly("List emails")
@@ -800,7 +1051,7 @@ var MCP_TOOLS = [
800
1051
  description: "List sending domains for this account.",
801
1052
  inputSchema: {
802
1053
  type: "object",
803
- properties: { ...paginationProps }
1054
+ properties: { ...paginationProps, tenant: tenantProp("Only domains this tenant owns") }
804
1055
  },
805
1056
  outputSchema: pagedOutput("domains", domainItem, "Sending domains on this page"),
806
1057
  annotations: readOnly("List domains")
@@ -812,7 +1063,8 @@ var MCP_TOOLS = [
812
1063
  type: "object",
813
1064
  properties: {
814
1065
  name: { type: "string", description: "Domain name (example.com)" },
815
- 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")
816
1068
  },
817
1069
  required: ["name"]
818
1070
  },
@@ -896,6 +1148,11 @@ var MCP_TOOLS = [
896
1148
  type: "array",
897
1149
  items: { type: "string" },
898
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"
899
1156
  }
900
1157
  },
901
1158
  required: ["url"]
@@ -935,7 +1192,8 @@ var MCP_TOOLS = [
935
1192
  type: "object",
936
1193
  properties: {
937
1194
  ...paginationProps,
938
- 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")
939
1197
  }
940
1198
  },
941
1199
  outputSchema: pagedOutput("data", suppressionItem, "Suppressed addresses on this page"),
@@ -961,7 +1219,8 @@ var MCP_TOOLS = [
961
1219
  type: "object",
962
1220
  properties: {
963
1221
  email: { type: "string", description: "Email address to stop sending to" },
964
- 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")
965
1224
  },
966
1225
  required: ["email"]
967
1226
  },
@@ -982,7 +1241,8 @@ var MCP_TOOLS = [
982
1241
  type: "object",
983
1242
  properties: {
984
1243
  id: { type: "string", description: "Suppression id" },
985
- 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")
986
1246
  }
987
1247
  },
988
1248
  outputSchema: okOutput,
@@ -1079,7 +1339,8 @@ var MCP_TOOLS = [
1079
1339
  deliver: {
1080
1340
  type: "boolean",
1081
1341
  description: "When true (default), enqueue delivery to account webhooks"
1082
- }
1342
+ },
1343
+ tenant: tenantProp("Put this tenant on the test event, to test endpoints that only receive some tenants")
1083
1344
  },
1084
1345
  required: ["event"]
1085
1346
  },
@@ -1092,6 +1353,158 @@ var MCP_TOOLS = [
1092
1353
  openWorldHint: true
1093
1354
  }
1094
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
+ },
1095
1508
  {
1096
1509
  name: "get_deliverability",
1097
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.8.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.16.0"
44
+ "groundmail": "0.19.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.5.0",