shipmail-mcp 0.5.4 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ var MCP_MEMBER_PERMISSION_GROUP_NAMES = [
12
12
  "mail_read",
13
13
  "drafts",
14
14
  "mail_organize",
15
+ "mailbox_rules",
15
16
  "send_mail",
16
17
  "calendar_read",
17
18
  "calendar_write"
@@ -80,6 +81,13 @@ var MCP_PERMISSION_GROUPS = [
80
81
  scopes: ["mailbox_forwarding:read", "mailbox_forwarding:write"],
81
82
  persistent: true
82
83
  },
84
+ {
85
+ name: "mailbox_rules",
86
+ label: "Inbox rules",
87
+ description: "Read and manage persistent server-side sorting, move, read, and star rules.",
88
+ scopes: ["mailbox_rules:read", "mailbox_rules:write"],
89
+ persistent: true
90
+ },
83
91
  {
84
92
  name: "webhooks",
85
93
  label: "Webhooks",
@@ -173,6 +181,8 @@ var CAPABILITY_ROWS = [
173
181
  ["shipmail_update_mailbox", "updateMailbox", "mailboxes:write"],
174
182
  ["shipmail_delete_mailbox", "deleteMailbox", "mailboxes:write"],
175
183
  ["shipmail_list_mailbox_folders", "listMailboxFolders", "mailboxes:read"],
184
+ ["shipmail_get_mailbox_rules", "getMailboxRules", "mailbox_rules:read"],
185
+ ["shipmail_set_mailbox_rules", "updateMailboxRules", "mailbox_rules:write"],
176
186
  ["shipmail_create_mailbox_folder", "createMailboxFolder", "mailboxes:write"],
177
187
  ["shipmail_update_mailbox_folder", "updateMailboxFolder", "mailboxes:write"],
178
188
  ["shipmail_delete_mailbox_folder", "deleteMailboxFolder", "mailboxes:write"],
@@ -334,7 +344,8 @@ var DESTRUCTIVE_TOOLS = /* @__PURE__ */ new Set([
334
344
  "shipmail_create_mailbox_app_password",
335
345
  "shipmail_create_mailbox_export",
336
346
  "shipmail_rotate_webhook_secret",
337
- "shipmail_consume_partner_mailbox_credential_grant"
347
+ "shipmail_consume_partner_mailbox_credential_grant",
348
+ "shipmail_set_mailbox_rules"
338
349
  ]);
339
350
  var EXTERNAL_WRITE_TOOLS = /* @__PURE__ */ new Set([
340
351
  "shipmail_send_message",
@@ -382,6 +393,7 @@ function permissionGroupFor(toolName, requiredScope) {
382
393
  if (requiredScope.startsWith("domains:")) return "domain_admin";
383
394
  if (requiredScope.startsWith("mailbox_credentials:")) return "credentials";
384
395
  if (requiredScope.startsWith("mailbox_forwarding:")) return "forwarding";
396
+ if (requiredScope.startsWith("mailbox_rules:")) return "mailbox_rules";
385
397
  if (requiredScope === "mailboxes:export") return "mailbox_export";
386
398
  if (requiredScope === "drafts:write") return "drafts";
387
399
  if (requiredScope === "messages:send") return "send_mail";
@@ -417,7 +429,7 @@ function durationFor(toolName) {
417
429
  if (toolName.includes("app_password") || toolName.includes("credential_grant") || toolName === "shipmail_rotate_webhook_secret") {
418
430
  return "credential";
419
431
  }
420
- if (toolName.includes("forwarding") || toolName.includes("webhook") || toolName.includes("booking_page")) {
432
+ if (toolName.includes("forwarding") || toolName.includes("mailbox_rules") || toolName.includes("webhook") || toolName.includes("booking_page")) {
421
433
  return "persistent";
422
434
  }
423
435
  if (toolName === "shipmail_send_message" || toolName.includes("schedule_") || toolName.includes("scheduled_message")) {
@@ -566,14 +578,154 @@ function readConfig(argv = process.argv.slice(2)) {
566
578
  }
567
579
 
568
580
  // src/server.ts
569
- import { McpServer as McpServer4 } from "@modelcontextprotocol/sdk/server/mcp.js";
581
+ import { McpServer as McpServer5 } from "@modelcontextprotocol/sdk/server/mcp.js";
570
582
  import { ShipMailClient } from "shipmail";
571
583
 
572
- // src/prompts.ts
584
+ // src/cross-organization-tools.ts
585
+ import { performance } from "perf_hooks";
573
586
  import "@modelcontextprotocol/sdk/server/mcp.js";
574
- import { WEBHOOK_EVENT_TYPES as WEBHOOK_EVENT_TYPES2 } from "shipmail";
587
+ import { ShipMailError as ShipMailError2 } from "shipmail";
575
588
  import { z as z2 } from "zod/v4";
576
589
 
590
+ // src/result.ts
591
+ import { ShipMailError, ValidationError } from "shipmail";
592
+
593
+ // src/sanitize.ts
594
+ var MAX_STRING_LENGTH = 16384;
595
+ var TRUNCATION_MARKER = "\n\u2026[truncated]";
596
+ var DANGEROUS_CHARS_REGEX = (
597
+ // eslint-disable-next-line no-control-regex
598
+ /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F؜‎‏‪-‮⁦-⁩]/gu
599
+ );
600
+ function sanitizeString(value, maxLength = MAX_STRING_LENGTH) {
601
+ const stripped = value.replace(DANGEROUS_CHARS_REGEX, "");
602
+ if (stripped.length <= maxLength) return stripped;
603
+ const keep = Math.max(0, maxLength - TRUNCATION_MARKER.length);
604
+ return stripped.slice(0, keep) + TRUNCATION_MARKER;
605
+ }
606
+ function isPlainObject(value) {
607
+ return typeof value === "object" && value !== null && !Array.isArray(value);
608
+ }
609
+ function sanitizeValue(value, maxStringLength = MAX_STRING_LENGTH) {
610
+ if (typeof value === "string") {
611
+ return sanitizeString(value, maxStringLength);
612
+ }
613
+ if (Array.isArray(value)) {
614
+ return value.map((item) => sanitizeValue(item, maxStringLength));
615
+ }
616
+ if (isPlainObject(value)) {
617
+ const out = {};
618
+ for (const [key, item] of Object.entries(value)) {
619
+ out[key] = sanitizeValue(item, maxStringLength);
620
+ }
621
+ return out;
622
+ }
623
+ return value;
624
+ }
625
+ function sanitizeRecord(value, maxStringLength = MAX_STRING_LENGTH) {
626
+ const out = {};
627
+ for (const [key, item] of Object.entries(value)) {
628
+ out[key] = sanitizeValue(item, maxStringLength);
629
+ }
630
+ return out;
631
+ }
632
+
633
+ // src/result.ts
634
+ var MAX_ERROR_MESSAGE_LENGTH = 500;
635
+ var SAFE_ERROR_TYPES = /* @__PURE__ */ new Set([
636
+ "validation_error",
637
+ "not_found",
638
+ "conflict",
639
+ "rate_limit_error",
640
+ "authentication_error",
641
+ "authorization_error",
642
+ "quota_exceeded"
643
+ ]);
644
+ var GENERIC_INTERNAL_MESSAGE = "Internal MCP error. The original message is logged on the MCP server stderr.";
645
+ var MCP_RATE_LIMIT_MARKER = "[mcp.rate_limit]";
646
+ var MCP_SCHEMA_VIOLATION_MARKER = "[mcp.schema_violation]";
647
+ var SAFE_MARKERS = [MCP_RATE_LIMIT_MARKER, MCP_SCHEMA_VIOLATION_MARKER];
648
+ function jsonResult(structuredContent) {
649
+ const sanitized = sanitizeRecord(structuredContent);
650
+ return {
651
+ content: [
652
+ {
653
+ type: "text",
654
+ text: JSON.stringify(sanitized, null, 2)
655
+ }
656
+ ],
657
+ structuredContent: sanitized
658
+ };
659
+ }
660
+ function formatShipMailError(error) {
661
+ const parts = [];
662
+ const isSafeMessage = error.type !== void 0 && SAFE_ERROR_TYPES.has(error.type);
663
+ if (isSafeMessage) {
664
+ parts.push(sanitizeString(error.message, MAX_ERROR_MESSAGE_LENGTH));
665
+ } else {
666
+ parts.push("ShipMail request failed. Contact ShipMail support with the request_id below.");
667
+ }
668
+ if (error.type) parts.push(`type=${error.type}`);
669
+ if (error.status !== void 0) parts.push(`status=${error.status}`);
670
+ if (error.requestId) parts.push(`request_id=${error.requestId}`);
671
+ if (isSafeMessage && error instanceof ValidationError && error.details?.length) {
672
+ const fields = error.details.map((detail) => sanitizeString(detail.field, 100)).filter((field) => field.length > 0).slice(0, 25).join(", ");
673
+ if (fields.length > 0) parts.push(`fields=${fields}`);
674
+ }
675
+ return parts.join(" | ");
676
+ }
677
+ function safeMarkerStrip(message) {
678
+ for (const marker of SAFE_MARKERS) {
679
+ if (message.startsWith(marker)) {
680
+ return message.slice(marker.length).trim();
681
+ }
682
+ }
683
+ return null;
684
+ }
685
+ function errorResult(error) {
686
+ let text;
687
+ if (error instanceof ShipMailError) {
688
+ text = formatShipMailError(error);
689
+ } else if (error instanceof Error && safeMarkerStrip(error.message) !== null) {
690
+ text = sanitizeString(safeMarkerStrip(error.message) ?? "", MAX_ERROR_MESSAGE_LENGTH);
691
+ } else {
692
+ if (error instanceof Error) {
693
+ process.stderr.write(
694
+ `${JSON.stringify({
695
+ tool: "mcp_internal_error",
696
+ name: error.name,
697
+ message: sanitizeString(error.message, MAX_ERROR_MESSAGE_LENGTH)
698
+ })}
699
+ `
700
+ );
701
+ } else {
702
+ process.stderr.write(
703
+ `${JSON.stringify({
704
+ tool: "mcp_internal_error",
705
+ message: sanitizeString(String(error), MAX_ERROR_MESSAGE_LENGTH)
706
+ })}
707
+ `
708
+ );
709
+ }
710
+ text = GENERIC_INTERNAL_MESSAGE;
711
+ }
712
+ return {
713
+ content: [{ type: "text", text }],
714
+ isError: true
715
+ };
716
+ }
717
+ function asTextResource(uri, value) {
718
+ return {
719
+ contents: [
720
+ {
721
+ uri,
722
+ mimeType: "application/json",
723
+ text: JSON.stringify(sanitizeValue(value), null, 2)
724
+ }
725
+ ]
726
+ };
727
+ }
728
+
577
729
  // src/schemas.ts
578
730
  import {
579
731
  CONFERENCING_PROVIDER_IDS,
@@ -727,6 +879,8 @@ var SYSTEM_FOLDER_NAMES = [
727
879
  "junk",
728
880
  "trash"
729
881
  ];
882
+ var MAILBOX_RULE_MATCH_MODES = ["all", "any"];
883
+ var MAILBOX_RULE_SYSTEM_TARGET_ROLES = ["inbox", "archive", "junk", "trash"];
730
884
  var JMAP_KEYWORDS = ["$flagged", "$seen", "$draft", "$answered", "$forwarded"];
731
885
  var NEWSLETTER_STATUSES = [
732
886
  "draft",
@@ -948,6 +1102,70 @@ var mailboxFoldersSchema = z.object({
948
1102
  address: z.string(),
949
1103
  data: z.array(mailboxFolderSchema)
950
1104
  });
1105
+ var mailboxRuleConditionSchema = z.lazy(
1106
+ () => z.union([
1107
+ z.object({
1108
+ type: z.enum([
1109
+ "from_is",
1110
+ "from_contains",
1111
+ "recipient_is",
1112
+ "plus_tag_is",
1113
+ "subject_contains"
1114
+ ]),
1115
+ value: noControlString(256, "condition value").min(1)
1116
+ }),
1117
+ z.object({
1118
+ type: z.enum(["has_attachment", "list_unsubscribe_exists"])
1119
+ }),
1120
+ z.object({
1121
+ type: z.literal("group"),
1122
+ match_mode: z.enum(MAILBOX_RULE_MATCH_MODES),
1123
+ conditions: z.array(mailboxRuleConditionSchema).min(1).max(10)
1124
+ })
1125
+ ])
1126
+ );
1127
+ var mailboxRuleActionSchema = z.union([
1128
+ z.object({
1129
+ type: z.literal("move"),
1130
+ target: z.union([
1131
+ z.object({
1132
+ kind: z.literal("system"),
1133
+ role: z.enum(MAILBOX_RULE_SYSTEM_TARGET_ROLES)
1134
+ }),
1135
+ z.object({
1136
+ kind: z.literal("custom"),
1137
+ folder_id: noControlString(256, "folder_id").min(1)
1138
+ })
1139
+ ])
1140
+ }),
1141
+ z.object({ type: z.enum(["mark_read", "star"]) }),
1142
+ z.object({ type: z.literal("send_webhook") }).strict()
1143
+ ]);
1144
+ var mailboxRuleSchema = z.object({
1145
+ id: z.uuid("Rule ID must be a UUID."),
1146
+ name: noControlString(120, "name").trim().min(1),
1147
+ enabled: z.boolean(),
1148
+ position: z.number().int().min(0),
1149
+ match_mode: z.enum(MAILBOX_RULE_MATCH_MODES),
1150
+ stop: z.boolean(),
1151
+ conditions: z.array(mailboxRuleConditionSchema).min(1).max(10),
1152
+ actions: z.array(mailboxRuleActionSchema).min(1).max(4)
1153
+ });
1154
+ var mailboxRulesSchema = z.object({
1155
+ object: z.literal("mailbox_rules"),
1156
+ mailbox_id: z.string(),
1157
+ address: z.string(),
1158
+ rules: z.array(mailboxRuleSchema),
1159
+ folders: z.array(
1160
+ z.object({
1161
+ id: z.string(),
1162
+ name: z.string(),
1163
+ parent_id: z.string().nullable(),
1164
+ role: z.string().nullable(),
1165
+ kind: z.enum(["custom", "system"])
1166
+ })
1167
+ )
1168
+ });
951
1169
  var mailboxIdentitySchema = z.object({
952
1170
  object: z.literal("mailbox_identity"),
953
1171
  id: z.string(),
@@ -1230,6 +1448,10 @@ var messageSchema = z.object({
1230
1448
  source: z.enum(MESSAGE_SOURCES),
1231
1449
  mode: z.enum(["live", "test"]),
1232
1450
  status: z.enum(MESSAGE_STATUSES),
1451
+ rule_disposition: z.object({
1452
+ matched_rule_ids: z.array(z.string()).describe("Inbox rule IDs that matched this inbound message, in evaluation order."),
1453
+ stop_rule_id: z.string().nullable().describe("Inbox rule ID that stopped further processing, if any.")
1454
+ }).nullable().describe("Inbox rule evaluation recorded for this tracked message at delivery."),
1233
1455
  scheduled_at: z.string().nullable(),
1234
1456
  created_at: z.string(),
1235
1457
  updated_at: z.string()
@@ -1369,7 +1591,12 @@ var acknowledgmentSchema = z.object({
1369
1591
  ok: z.literal(true),
1370
1592
  id: z.string()
1371
1593
  });
1372
- var statusOutputSchema = z.object({ status: statusSchema });
1594
+ var statusOutputSchema = z.object({
1595
+ status: statusSchema,
1596
+ // Present only when the connection covers more than one organization. Use an id here as
1597
+ // organization_id on other tools.
1598
+ organizations: z.array(z.object({ id: z.string(), name: z.string() })).optional()
1599
+ });
1373
1600
  var domainOutputSchema = z.object({ domain: domainSchema });
1374
1601
  var domainDnsRecordsOutputSchema = z.object({ dns_records: domainDnsRecordSetSchema });
1375
1602
  var mailboxOutputSchema = z.object({ mailbox: mailboxSchema });
@@ -1396,6 +1623,7 @@ var partnerMailboxCredentialGrantsOutputSchema = z.object({
1396
1623
  });
1397
1624
  var mailboxFolderOutputSchema = z.object({ folder: mailboxFolderSchema });
1398
1625
  var mailboxFoldersOutputSchema = z.object({ folders: mailboxFoldersSchema });
1626
+ var mailboxRulesOutputSchema = z.object({ rules: mailboxRulesSchema });
1399
1627
  var mailboxIdentitiesOutputSchema = z.object({ identities: mailboxIdentitiesSchema });
1400
1628
  var inboxMessagesOutputSchema = z.object({ inbox_messages: inboxMessagesSchema });
1401
1629
  var inboxMessageSummariesOutputSchema = z.object({
@@ -1649,6 +1877,11 @@ var deleteMailboxFolderInputSchema = z.object({
1649
1877
  id: idSchema,
1650
1878
  folder_id: folderIdSchema
1651
1879
  });
1880
+ var updateMailboxRulesInputSchema = z.object({
1881
+ id: idSchema,
1882
+ rules: z.array(mailboxRuleSchema).max(50),
1883
+ idempotency_key: idempotencyKeySchema
1884
+ });
1652
1885
  var resetPasswordInputSchema = z.object({
1653
1886
  id: idSchema,
1654
1887
  password: z.string().min(8).max(128).refine((value) => /[a-z]/.test(value), "Password must include a lowercase letter.").refine((value) => /[A-Z]/.test(value), "Password must include an uppercase letter.").refine((value) => /[0-9]/.test(value), "Password must include a number."),
@@ -2824,20 +3057,313 @@ var consumePartnerMailboxCredentialGrantInputSchema = z.object({
2824
3057
  allowed_cidrs: z.array(appPasswordCidrInputSchema).max(20).optional()
2825
3058
  });
2826
3059
 
3060
+ // src/cross-organization-tools.ts
3061
+ var MAX_CONCURRENT_ORGANIZATIONS = 4;
3062
+ var SAFE_ERROR_TYPES2 = /* @__PURE__ */ new Set([
3063
+ "validation_error",
3064
+ "authentication_error",
3065
+ "authorization_error",
3066
+ "quota_exceeded",
3067
+ "not_found",
3068
+ "rate_limit_error",
3069
+ "conflict"
3070
+ ]);
3071
+ var cursorSchema = z2.string().regex(/^[A-Za-z0-9_\-=.+/]{1,512}$/, "Cursor must be 1-512 characters of [A-Za-z0-9_\\-=.+/].");
3072
+ var crossOrganizationListInputSchema = z2.object({
3073
+ limit: z2.number().int().min(1).max(100).default(25).describe("Maximum results to return from each organization."),
3074
+ cursor_by_organization: z2.record(z2.string().min(1).max(128), cursorSchema).optional().describe(
3075
+ "Independent pagination cursors keyed by organization ID. Copy each successful section's next_cursor into the matching key on the next call."
3076
+ )
3077
+ });
3078
+ var organizationSchema = z2.object({
3079
+ id: z2.string(),
3080
+ name: z2.string()
3081
+ });
3082
+ var organizationErrorSchema = z2.object({
3083
+ type: z2.string(),
3084
+ message: z2.string(),
3085
+ status: z2.number().int().nullable(),
3086
+ request_id: z2.string().nullable(),
3087
+ retryable: z2.boolean()
3088
+ });
3089
+ function crossOrganizationListOutputSchema(itemSchema) {
3090
+ return z2.object({
3091
+ organizations: z2.array(
3092
+ z2.discriminatedUnion("status", [
3093
+ z2.object({
3094
+ organization: organizationSchema,
3095
+ status: z2.literal("ok"),
3096
+ data: z2.array(itemSchema),
3097
+ pagination: paginationSchema
3098
+ }),
3099
+ z2.object({
3100
+ organization: organizationSchema,
3101
+ status: z2.literal("error"),
3102
+ error: organizationErrorSchema
3103
+ })
3104
+ ])
3105
+ )
3106
+ });
3107
+ }
3108
+ var CROSS_ORGANIZATION_LIST_BEHAVIORS = [
3109
+ {
3110
+ toolName: "shipmail_list_domains_across_organizations",
3111
+ baseToolName: "shipmail_list_domains",
3112
+ title: "List Domains Across Organizations",
3113
+ description: "List domains in every organization on this connection. Returns one independently paginated success or failure section per organization.",
3114
+ itemSchema: domainSchema,
3115
+ list: (client, params) => client.domains.list(params)
3116
+ },
3117
+ {
3118
+ toolName: "shipmail_list_mailboxes_across_organizations",
3119
+ baseToolName: "shipmail_list_mailboxes",
3120
+ title: "List Mailboxes Across Organizations",
3121
+ description: "List mailboxes in every organization on this connection. Returns one independently paginated success or failure section per organization.",
3122
+ itemSchema: mailboxSchema,
3123
+ list: (client, params) => client.mailboxes.list(params)
3124
+ },
3125
+ {
3126
+ toolName: "shipmail_list_webhooks_across_organizations",
3127
+ baseToolName: "shipmail_list_webhooks",
3128
+ title: "List Webhooks Across Organizations",
3129
+ description: "List webhooks in every organization on this connection. Returns one independently paginated success or failure section per organization.",
3130
+ itemSchema: webhookSchema,
3131
+ list: (client, params) => client.webhooks.list(params)
3132
+ },
3133
+ {
3134
+ toolName: "shipmail_list_suppressions_across_organizations",
3135
+ baseToolName: "shipmail_list_suppressions",
3136
+ title: "List Suppressions Across Organizations",
3137
+ description: "List suppressions in every organization on this connection. Returns one independently paginated success or failure section per organization.",
3138
+ itemSchema: suppressionSchema,
3139
+ list: (client, params) => client.suppressions.list(params)
3140
+ },
3141
+ {
3142
+ toolName: "shipmail_list_newsletters_across_organizations",
3143
+ baseToolName: "shipmail_list_newsletters",
3144
+ title: "List Newsletters Across Organizations",
3145
+ description: "List newsletters in every organization on this connection. Returns one independently paginated success or failure section per organization.",
3146
+ itemSchema: newsletterSchema,
3147
+ list: (client, params) => client.newsletters.list(params)
3148
+ },
3149
+ {
3150
+ toolName: "shipmail_list_newsletter_domains_across_organizations",
3151
+ baseToolName: "shipmail_list_newsletter_domains",
3152
+ title: "List Newsletter Domains Across Organizations",
3153
+ description: "List newsletter sending domains in every organization on this connection. Returns one independently paginated success or failure section per organization.",
3154
+ itemSchema: newsletterDomainSchema,
3155
+ list: (client, params) => client.newsletters.domains.list(params)
3156
+ },
3157
+ {
3158
+ toolName: "shipmail_list_newsletter_assets_across_organizations",
3159
+ baseToolName: "shipmail_list_newsletter_assets",
3160
+ title: "List Newsletter Assets Across Organizations",
3161
+ description: "List newsletter assets in every organization on this connection. Returns one independently paginated success or failure section per organization.",
3162
+ itemSchema: newsletterAssetSchema,
3163
+ list: (client, params) => client.newsletters.assets.list(params)
3164
+ },
3165
+ {
3166
+ toolName: "shipmail_list_audiences_across_organizations",
3167
+ baseToolName: "shipmail_list_audiences",
3168
+ title: "List Audiences Across Organizations",
3169
+ description: "List audiences in every organization on this connection. Returns one independently paginated success or failure section per organization.",
3170
+ itemSchema: audienceSchema,
3171
+ list: (client, params) => client.audiences.list(params)
3172
+ },
3173
+ {
3174
+ toolName: "shipmail_list_booking_pages_across_organizations",
3175
+ baseToolName: "shipmail_list_booking_pages",
3176
+ title: "List Booking Pages Across Organizations",
3177
+ description: "List booking pages in every organization on this connection. Returns one independently paginated success or failure section per organization.",
3178
+ itemSchema: bookingPageSchema,
3179
+ list: (client, params) => client.bookingPages.list(params)
3180
+ }
3181
+ ];
3182
+ var CROSS_ORGANIZATION_TOOL_NAMES = new Set(
3183
+ CROSS_ORGANIZATION_LIST_BEHAVIORS.map((behavior) => behavior.toolName)
3184
+ );
3185
+ function permissionError(behavior) {
3186
+ return {
3187
+ type: "authorization_error",
3188
+ message: `${behavior.baseToolName} is not permitted for this organization.`,
3189
+ status: 403,
3190
+ request_id: null,
3191
+ retryable: false
3192
+ };
3193
+ }
3194
+ function requestError(error) {
3195
+ if (error instanceof ShipMailError2) {
3196
+ const isSafeMessage = error.type !== void 0 && SAFE_ERROR_TYPES2.has(error.type);
3197
+ return {
3198
+ type: error.type ?? "api_error",
3199
+ message: isSafeMessage ? sanitizeString(error.message, 500) : "ShipMail could not list this organization. Contact support with the request_id.",
3200
+ status: error.status ?? null,
3201
+ request_id: error.requestId ?? null,
3202
+ retryable: error.retryable
3203
+ };
3204
+ }
3205
+ return {
3206
+ type: "internal_error",
3207
+ message: "The organization could not be listed because of an unexpected error.",
3208
+ status: null,
3209
+ request_id: null,
3210
+ retryable: false
3211
+ };
3212
+ }
3213
+ function upstreamSchemaError(toolName, organizationId, issues) {
3214
+ process.stderr.write(
3215
+ `${JSON.stringify({
3216
+ tool: toolName,
3217
+ organization_id: organizationId,
3218
+ output_schema_violation: issues
3219
+ })}
3220
+ `
3221
+ );
3222
+ }
3223
+ async function listOneOrganization(behavior, grant, args) {
3224
+ const organization = { id: grant.id, name: grant.name };
3225
+ if (!grant.allowedTools.has(behavior.baseToolName)) {
3226
+ return {
3227
+ organization,
3228
+ status: "error",
3229
+ error: permissionError(behavior)
3230
+ };
3231
+ }
3232
+ try {
3233
+ const cursor = args.cursor_by_organization?.[grant.id];
3234
+ const raw = await behavior.list(grant.client, {
3235
+ limit: args.limit,
3236
+ ...cursor ? { cursor } : {}
3237
+ });
3238
+ const pageSchema = z2.object({
3239
+ data: z2.array(behavior.itemSchema),
3240
+ pagination: paginationSchema
3241
+ });
3242
+ const page = pageSchema.safeParse(raw);
3243
+ if (!page.success) {
3244
+ const issues = page.error.issues.slice(0, 5).map(
3245
+ (issue) => `${issue.path.length === 0 ? "(root)" : issue.path.join(".")}: ${issue.message}`
3246
+ ).join("; ");
3247
+ upstreamSchemaError(behavior.toolName, grant.id, issues);
3248
+ return {
3249
+ organization,
3250
+ status: "error",
3251
+ error: {
3252
+ type: "internal_error",
3253
+ message: "ShipMail returned an unexpected response shape for this organization.",
3254
+ status: 500,
3255
+ request_id: null,
3256
+ retryable: false
3257
+ }
3258
+ };
3259
+ }
3260
+ return {
3261
+ organization,
3262
+ status: "ok",
3263
+ data: page.data.data,
3264
+ pagination: page.data.pagination
3265
+ };
3266
+ } catch (error) {
3267
+ return {
3268
+ organization,
3269
+ status: "error",
3270
+ error: requestError(error)
3271
+ };
3272
+ }
3273
+ }
3274
+ async function listEveryOrganization(behavior, grants, args) {
3275
+ const organizations = [];
3276
+ for (let offset = 0; offset < grants.length; offset += MAX_CONCURRENT_ORGANIZATIONS) {
3277
+ const batch = grants.slice(offset, offset + MAX_CONCURRENT_ORGANIZATIONS);
3278
+ const sections = await Promise.all(
3279
+ batch.map((grant) => listOneOrganization(behavior, grant, args))
3280
+ );
3281
+ organizations.push(...sections);
3282
+ }
3283
+ return { organizations };
3284
+ }
3285
+ function registerCrossOrganizationTools(server, grants) {
3286
+ if (grants.length < 2) return;
3287
+ for (const behavior of CROSS_ORGANIZATION_LIST_BEHAVIORS) {
3288
+ if (!grants.some((grant) => grant.allowedTools.has(behavior.baseToolName))) continue;
3289
+ const baseCapability = getMcpCapability(behavior.baseToolName);
3290
+ if (!baseCapability || baseCapability.effect !== "read") {
3291
+ throw new Error(
3292
+ `Cross-organization MCP tool ${behavior.toolName} must map to an audited read capability.`
3293
+ );
3294
+ }
3295
+ const outputSchema = crossOrganizationListOutputSchema(behavior.itemSchema);
3296
+ server.registerTool(
3297
+ behavior.toolName,
3298
+ {
3299
+ title: behavior.title,
3300
+ description: behavior.description,
3301
+ inputSchema: crossOrganizationListInputSchema,
3302
+ outputSchema,
3303
+ annotations: {
3304
+ ...baseCapability.annotations,
3305
+ readOnlyHint: true,
3306
+ destructiveHint: false
3307
+ }
3308
+ },
3309
+ async (args) => {
3310
+ const startedAt = performance.now();
3311
+ const raw = await listEveryOrganization(behavior, grants, args);
3312
+ const parsed = outputSchema.safeParse(raw);
3313
+ if (!parsed.success) {
3314
+ process.stderr.write(
3315
+ `${JSON.stringify({
3316
+ tool: behavior.toolName,
3317
+ output_schema_violation: parsed.error.message
3318
+ })}
3319
+ `
3320
+ );
3321
+ return {
3322
+ isError: true,
3323
+ content: [
3324
+ {
3325
+ type: "text",
3326
+ text: "ShipMail could not validate the cross-organization list result."
3327
+ }
3328
+ ]
3329
+ };
3330
+ }
3331
+ const failedOrganizations = parsed.data.organizations.filter(
3332
+ (section) => section.status === "error"
3333
+ ).length;
3334
+ process.stderr.write(
3335
+ `${JSON.stringify({
3336
+ tool: behavior.toolName,
3337
+ duration_ms: Math.round(performance.now() - startedAt),
3338
+ organizations: grants.length,
3339
+ failed_organizations: failedOrganizations,
3340
+ status: failedOrganizations === 0 ? "ok" : failedOrganizations === grants.length ? "failed" : "partial"
3341
+ })}
3342
+ `
3343
+ );
3344
+ return jsonResult(parsed.data);
3345
+ }
3346
+ );
3347
+ }
3348
+ }
3349
+
2827
3350
  // src/prompts.ts
3351
+ import "@modelcontextprotocol/sdk/server/mcp.js";
3352
+ import { WEBHOOK_EVENT_TYPES as WEBHOOK_EVENT_TYPES2 } from "shipmail";
3353
+ import { z as z3 } from "zod/v4";
2828
3354
  var MAILBOX_LOCAL_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9._-]{0,62}[a-zA-Z0-9])?$/;
2829
3355
  var MAILBOX_ADDRESS_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9._-]{0,62}[a-zA-Z0-9])?@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
2830
3356
  var TONE_REGEX = /^[A-Za-z][A-Za-z\s-]{0,49}$/;
2831
3357
  var LIMIT_REGEX = /^[1-9][0-9]?$|^100$/;
2832
- var mailboxAddressArg = z2.string().max(254).regex(MAILBOX_ADDRESS_REGEX, "mailbox_address must be a valid email address.").optional();
2833
- var mailboxLocalArg = z2.string().max(64).regex(MAILBOX_LOCAL_REGEX, "Mailbox local-part contains invalid characters.").optional();
3358
+ var mailboxAddressArg = z3.string().max(254).regex(MAILBOX_ADDRESS_REGEX, "mailbox_address must be a valid email address.").optional();
3359
+ var mailboxLocalArg = z3.string().max(64).regex(MAILBOX_LOCAL_REGEX, "Mailbox local-part contains invalid characters.").optional();
2834
3360
  var domainNameArg = domainNameSchema.optional();
2835
- var toneArg = z2.string().regex(TONE_REGEX, "tone must be 1-50 letters/spaces/hyphens.").optional();
2836
- var limitArg = z2.string().regex(LIMIT_REGEX, "limit must be an integer 1-100.").optional();
2837
- var urlArg = z2.url().max(2048).refine((value) => isPublicHttpsUrl(value), {
3361
+ var toneArg = z3.string().regex(TONE_REGEX, "tone must be 1-50 letters/spaces/hyphens.").optional();
3362
+ var limitArg = z3.string().regex(LIMIT_REGEX, "limit must be an integer 1-100.").optional();
3363
+ var urlArg = z3.url().max(2048).refine((value) => isPublicHttpsUrl(value), {
2838
3364
  message: "url must use https on a public host."
2839
3365
  }).optional();
2840
- var eventsArg = z2.string().max(500).refine(
3366
+ var eventsArg = z3.string().max(500).refine(
2841
3367
  (value) => {
2842
3368
  const parts = value.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
2843
3369
  if (parts.length === 0) return false;
@@ -3190,145 +3716,6 @@ function toInboxMessageSummaries(messages) {
3190
3716
  };
3191
3717
  }
3192
3718
 
3193
- // src/result.ts
3194
- import { ShipMailError, ValidationError } from "shipmail";
3195
-
3196
- // src/sanitize.ts
3197
- var MAX_STRING_LENGTH = 16384;
3198
- var TRUNCATION_MARKER = "\n\u2026[truncated]";
3199
- var DANGEROUS_CHARS_REGEX = (
3200
- // eslint-disable-next-line no-control-regex
3201
- /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F؜‎‏‪-‮⁦-⁩]/gu
3202
- );
3203
- function sanitizeString(value, maxLength = MAX_STRING_LENGTH) {
3204
- const stripped = value.replace(DANGEROUS_CHARS_REGEX, "");
3205
- if (stripped.length <= maxLength) return stripped;
3206
- const keep = Math.max(0, maxLength - TRUNCATION_MARKER.length);
3207
- return stripped.slice(0, keep) + TRUNCATION_MARKER;
3208
- }
3209
- function isPlainObject(value) {
3210
- return typeof value === "object" && value !== null && !Array.isArray(value);
3211
- }
3212
- function sanitizeValue(value, maxStringLength = MAX_STRING_LENGTH) {
3213
- if (typeof value === "string") {
3214
- return sanitizeString(value, maxStringLength);
3215
- }
3216
- if (Array.isArray(value)) {
3217
- return value.map((item) => sanitizeValue(item, maxStringLength));
3218
- }
3219
- if (isPlainObject(value)) {
3220
- const out = {};
3221
- for (const [key, item] of Object.entries(value)) {
3222
- out[key] = sanitizeValue(item, maxStringLength);
3223
- }
3224
- return out;
3225
- }
3226
- return value;
3227
- }
3228
- function sanitizeRecord(value, maxStringLength = MAX_STRING_LENGTH) {
3229
- const out = {};
3230
- for (const [key, item] of Object.entries(value)) {
3231
- out[key] = sanitizeValue(item, maxStringLength);
3232
- }
3233
- return out;
3234
- }
3235
-
3236
- // src/result.ts
3237
- var MAX_ERROR_MESSAGE_LENGTH = 500;
3238
- var SAFE_ERROR_TYPES = /* @__PURE__ */ new Set([
3239
- "validation_error",
3240
- "not_found",
3241
- "conflict",
3242
- "rate_limit_error",
3243
- "authentication_error",
3244
- "authorization_error",
3245
- "quota_exceeded"
3246
- ]);
3247
- var GENERIC_INTERNAL_MESSAGE = "Internal MCP error. The original message is logged on the MCP server stderr.";
3248
- var MCP_RATE_LIMIT_MARKER = "[mcp.rate_limit]";
3249
- var MCP_SCHEMA_VIOLATION_MARKER = "[mcp.schema_violation]";
3250
- var SAFE_MARKERS = [MCP_RATE_LIMIT_MARKER, MCP_SCHEMA_VIOLATION_MARKER];
3251
- function jsonResult(structuredContent) {
3252
- const sanitized = sanitizeRecord(structuredContent);
3253
- return {
3254
- content: [
3255
- {
3256
- type: "text",
3257
- text: JSON.stringify(sanitized, null, 2)
3258
- }
3259
- ],
3260
- structuredContent: sanitized
3261
- };
3262
- }
3263
- function formatShipMailError(error) {
3264
- const parts = [];
3265
- const isSafeMessage = error.type !== void 0 && SAFE_ERROR_TYPES.has(error.type);
3266
- if (isSafeMessage) {
3267
- parts.push(sanitizeString(error.message, MAX_ERROR_MESSAGE_LENGTH));
3268
- } else {
3269
- parts.push("ShipMail request failed. Contact ShipMail support with the request_id below.");
3270
- }
3271
- if (error.type) parts.push(`type=${error.type}`);
3272
- if (error.status !== void 0) parts.push(`status=${error.status}`);
3273
- if (error.requestId) parts.push(`request_id=${error.requestId}`);
3274
- if (isSafeMessage && error instanceof ValidationError && error.details?.length) {
3275
- const fields = error.details.map((detail) => sanitizeString(detail.field, 100)).filter((field) => field.length > 0).slice(0, 25).join(", ");
3276
- if (fields.length > 0) parts.push(`fields=${fields}`);
3277
- }
3278
- return parts.join(" | ");
3279
- }
3280
- function safeMarkerStrip(message) {
3281
- for (const marker of SAFE_MARKERS) {
3282
- if (message.startsWith(marker)) {
3283
- return message.slice(marker.length).trim();
3284
- }
3285
- }
3286
- return null;
3287
- }
3288
- function errorResult(error) {
3289
- let text;
3290
- if (error instanceof ShipMailError) {
3291
- text = formatShipMailError(error);
3292
- } else if (error instanceof Error && safeMarkerStrip(error.message) !== null) {
3293
- text = sanitizeString(safeMarkerStrip(error.message) ?? "", MAX_ERROR_MESSAGE_LENGTH);
3294
- } else {
3295
- if (error instanceof Error) {
3296
- process.stderr.write(
3297
- `${JSON.stringify({
3298
- tool: "mcp_internal_error",
3299
- name: error.name,
3300
- message: sanitizeString(error.message, MAX_ERROR_MESSAGE_LENGTH)
3301
- })}
3302
- `
3303
- );
3304
- } else {
3305
- process.stderr.write(
3306
- `${JSON.stringify({
3307
- tool: "mcp_internal_error",
3308
- message: sanitizeString(String(error), MAX_ERROR_MESSAGE_LENGTH)
3309
- })}
3310
- `
3311
- );
3312
- }
3313
- text = GENERIC_INTERNAL_MESSAGE;
3314
- }
3315
- return {
3316
- content: [{ type: "text", text }],
3317
- isError: true
3318
- };
3319
- }
3320
- function asTextResource(uri, value) {
3321
- return {
3322
- contents: [
3323
- {
3324
- uri,
3325
- mimeType: "application/json",
3326
- text: JSON.stringify(sanitizeValue(value), null, 2)
3327
- }
3328
- ]
3329
- };
3330
- }
3331
-
3332
3719
  // src/resources.ts
3333
3720
  var JSON_MIME = "application/json";
3334
3721
  function resourceConfig(title, description) {
@@ -3448,6 +3835,15 @@ function registerResources(server, client, componentConnectDomain2 = "https://sh
3448
3835
  return asTextResource(uri.toString(), await client.mailboxes.listIdentities(id));
3449
3836
  }
3450
3837
  );
3838
+ server.registerResource(
3839
+ "shipmail_mailbox_rules",
3840
+ new ResourceTemplate("shipmail://mailboxes/{id}/rules", { list: void 0 }),
3841
+ resourceConfig("ShipMail Mailbox Rules", "Server-side inbox rules and target folders."),
3842
+ async (uri, variables) => {
3843
+ const id = readId(variables);
3844
+ return asTextResource(uri.toString(), await client.mailboxes.getRules(id));
3845
+ }
3846
+ );
3451
3847
  server.registerResource(
3452
3848
  "shipmail_mailbox_inbox_messages",
3453
3849
  new ResourceTemplate("shipmail://mailboxes/{id}/inbox/messages", { list: void 0 }),
@@ -3510,11 +3906,12 @@ function registerResources(server, client, componentConnectDomain2 = "https://sh
3510
3906
 
3511
3907
  // src/tools.ts
3512
3908
  import { randomUUID } from "crypto";
3513
- import { performance } from "perf_hooks";
3909
+ import { performance as performance2 } from "perf_hooks";
3514
3910
  import "@modelcontextprotocol/sdk/server/mcp.js";
3515
3911
  import {
3516
- ShipMailError as ShipMailError2
3912
+ ShipMailError as ShipMailError3
3517
3913
  } from "shipmail";
3914
+ import { z as z4 } from "zod/v4";
3518
3915
  var SESSION_LIMITS = {
3519
3916
  shipmail_inject_sandbox_inbound: 20,
3520
3917
  shipmail_compose_message_with_file: 10,
@@ -3556,6 +3953,7 @@ var SESSION_LIMITS = {
3556
3953
  shipmail_reset_mailbox_password: 10,
3557
3954
  shipmail_create_mailbox_forwarding: 10,
3558
3955
  shipmail_delete_mailbox_forwarding: 10,
3956
+ shipmail_set_mailbox_rules: 20,
3559
3957
  shipmail_set_auto_reply: 20,
3560
3958
  shipmail_set_spam_filter: 20,
3561
3959
  shipmail_update_inbox_message: 50,
@@ -3626,7 +4024,7 @@ function logToolCall(name, durationMs, error) {
3626
4024
  tool: name,
3627
4025
  duration_ms: Math.round(durationMs)
3628
4026
  };
3629
- if (error instanceof ShipMailError2) {
4027
+ if (error instanceof ShipMailError3) {
3630
4028
  entry["error_type"] = error.type ?? "unknown";
3631
4029
  if (DEBUG_ENABLED) {
3632
4030
  if (error.requestId) entry["request_id"] = error.requestId;
@@ -3640,7 +4038,25 @@ function logToolCall(name, durationMs, error) {
3640
4038
  process.stderr.write(`${JSON.stringify(entry)}
3641
4039
  `);
3642
4040
  }
3643
- function registerTools(rawServer, client, allowedTools) {
4041
+ function organizationField(organizationIds) {
4042
+ return z4.string().optional().describe(
4043
+ `Organization to act in. This connection covers ${organizationIds.length} organizations: ${organizationIds.join(", ")}. Omit only when the target is unambiguous.`
4044
+ );
4045
+ }
4046
+ function withOrganizationParam(inputSchema, organizationIds) {
4047
+ if (organizationIds.length < 2) return inputSchema;
4048
+ if (inputSchema === void 0 || typeof inputSchema !== "object" || inputSchema === null) {
4049
+ return inputSchema;
4050
+ }
4051
+ if ("_def" in inputSchema || "~standard" in inputSchema) {
4052
+ const schema = inputSchema;
4053
+ if (typeof schema.extend !== "function") return inputSchema;
4054
+ return schema.extend({ organization_id: organizationField(organizationIds) });
4055
+ }
4056
+ return { ...inputSchema, organization_id: organizationField(organizationIds) };
4057
+ }
4058
+ function registerTools(rawServer, client, allowedTools, grantedOrganizations = []) {
4059
+ const organizationIds = grantedOrganizations.map((entry) => entry.id);
3644
4060
  const knownTools = [];
3645
4061
  const enabledTools = [];
3646
4062
  const callCounts = /* @__PURE__ */ new Map();
@@ -3655,6 +4071,7 @@ function registerTools(rawServer, client, allowedTools) {
3655
4071
  name,
3656
4072
  {
3657
4073
  ...config,
4074
+ ...config.inputSchema === void 0 ? {} : { inputSchema: withOrganizationParam(config.inputSchema, organizationIds) },
3658
4075
  annotations: {
3659
4076
  ...config.annotations,
3660
4077
  ...capability.annotations
@@ -3688,7 +4105,7 @@ function registerTools(rawServer, client, allowedTools) {
3688
4105
  callCounts.set(name, used + 1);
3689
4106
  }
3690
4107
  async function runTool(name, outputSchema, body) {
3691
- const start = performance.now();
4108
+ const start = performance2.now();
3692
4109
  try {
3693
4110
  checkRateLimit(name);
3694
4111
  const raw = await body();
@@ -3699,7 +4116,7 @@ function registerTools(rawServer, client, allowedTools) {
3699
4116
  ).join("; ");
3700
4117
  throw new OutputSchemaViolation(name, issues);
3701
4118
  }
3702
- logToolCall(name, performance.now() - start);
4119
+ logToolCall(name, performance2.now() - start);
3703
4120
  return jsonResult(parsed.data);
3704
4121
  } catch (error) {
3705
4122
  if (error instanceof OutputSchemaViolation) {
@@ -3708,12 +4125,12 @@ function registerTools(rawServer, client, allowedTools) {
3708
4125
  `
3709
4126
  );
3710
4127
  }
3711
- logToolCall(name, performance.now() - start, error instanceof Error ? error : void 0);
4128
+ logToolCall(name, performance2.now() - start, error instanceof Error ? error : void 0);
3712
4129
  return errorResult(error);
3713
4130
  }
3714
4131
  }
3715
4132
  async function runToolWithMeta(name, outputSchema, body) {
3716
- const start = performance.now();
4133
+ const start = performance2.now();
3717
4134
  try {
3718
4135
  checkRateLimit(name);
3719
4136
  const raw = await body();
@@ -3724,7 +4141,7 @@ function registerTools(rawServer, client, allowedTools) {
3724
4141
  ).join("; ");
3725
4142
  throw new OutputSchemaViolation(name, issues);
3726
4143
  }
3727
- logToolCall(name, performance.now() - start);
4144
+ logToolCall(name, performance2.now() - start);
3728
4145
  return { ...jsonResult(parsed.data), _meta: { ...raw.meta } };
3729
4146
  } catch (error) {
3730
4147
  if (error instanceof OutputSchemaViolation) {
@@ -3733,7 +4150,7 @@ function registerTools(rawServer, client, allowedTools) {
3733
4150
  `
3734
4151
  );
3735
4152
  }
3736
- logToolCall(name, performance.now() - start, error instanceof Error ? error : void 0);
4153
+ logToolCall(name, performance2.now() - start, error instanceof Error ? error : void 0);
3737
4154
  return errorResult(error);
3738
4155
  }
3739
4156
  }
@@ -3747,7 +4164,16 @@ function registerTools(rawServer, client, allowedTools) {
3747
4164
  annotations: { readOnlyHint: true, openWorldHint: false }
3748
4165
  },
3749
4166
  async () => runTool("shipmail_status", statusOutputSchema, async () => ({
3750
- status: await client.status.get()
4167
+ status: await client.status.get(),
4168
+ // Reported here rather than from a tool of its own: status is always available on every
4169
+ // profile, and it is what a caller checks before starting work. Without it the router's
4170
+ // "pass organization_id" error names ids the caller cannot resolve to anything.
4171
+ ...grantedOrganizations.length > 1 ? {
4172
+ organizations: grantedOrganizations.map((entry) => ({
4173
+ id: entry.id,
4174
+ name: entry.name
4175
+ }))
4176
+ } : {}
3751
4177
  }))
3752
4178
  );
3753
4179
  });
@@ -4262,7 +4688,7 @@ function registerTools(rawServer, client, allowedTools) {
4262
4688
  "shipmail_list_mailbox_folders",
4263
4689
  {
4264
4690
  title: "List Mailbox Folders",
4265
- description: "List system and custom folders for a mailbox, including unread counts and folder IDs for Assistant automations.",
4691
+ description: "List system and custom folders for a mailbox, including unread counts and folder IDs for inbox rules and Assistant automations.",
4266
4692
  inputSchema: getByIdInputSchema,
4267
4693
  outputSchema: mailboxFoldersOutputSchema,
4268
4694
  annotations: { readOnlyHint: true, openWorldHint: false }
@@ -4272,6 +4698,45 @@ function registerTools(rawServer, client, allowedTools) {
4272
4698
  }))
4273
4699
  );
4274
4700
  });
4701
+ registerIfAllowed("shipmail_get_mailbox_rules", () => {
4702
+ server.registerTool(
4703
+ "shipmail_get_mailbox_rules",
4704
+ {
4705
+ title: "Get Mailbox Rules",
4706
+ description: "List deterministic server-side inbox rules and the available destination folders for a mailbox.",
4707
+ inputSchema: getByIdInputSchema,
4708
+ outputSchema: mailboxRulesOutputSchema,
4709
+ annotations: { readOnlyHint: true, openWorldHint: false }
4710
+ },
4711
+ async ({ id }) => runTool("shipmail_get_mailbox_rules", mailboxRulesOutputSchema, async () => ({
4712
+ rules: await client.mailboxes.getRules(id)
4713
+ }))
4714
+ );
4715
+ });
4716
+ registerIfAllowed("shipmail_set_mailbox_rules", () => {
4717
+ server.registerTool(
4718
+ "shipmail_set_mailbox_rules",
4719
+ {
4720
+ title: "Set Mailbox Rules",
4721
+ description: "Replace every deterministic server-side inbox rule for a mailbox. Call shipmail_get_mailbox_rules first and preserve any rules that should remain.",
4722
+ inputSchema: updateMailboxRulesInputSchema,
4723
+ outputSchema: mailboxRulesOutputSchema,
4724
+ annotations: {
4725
+ readOnlyHint: false,
4726
+ destructiveHint: true,
4727
+ idempotentHint: true,
4728
+ openWorldHint: false
4729
+ }
4730
+ },
4731
+ async (args) => runTool("shipmail_set_mailbox_rules", mailboxRulesOutputSchema, async () => ({
4732
+ rules: await client.mailboxes.updateRules(
4733
+ args.id,
4734
+ { rules: args.rules },
4735
+ mutationOptions(args)
4736
+ )
4737
+ }))
4738
+ );
4739
+ });
4275
4740
  registerIfAllowed("shipmail_create_mailbox_folder", () => {
4276
4741
  server.registerTool(
4277
4742
  "shipmail_create_mailbox_folder",
@@ -4301,7 +4766,7 @@ function registerTools(rawServer, client, allowedTools) {
4301
4766
  "shipmail_update_mailbox_folder",
4302
4767
  {
4303
4768
  title: "Update Mailbox Folder",
4304
- description: "Rename a custom mailbox folder. System folders cannot be renamed; Assistant automations keep the stable folder ID.",
4769
+ description: "Rename a custom mailbox folder. System folders cannot be renamed. Inbox rules and Assistant automations keep the stable folder ID.",
4305
4770
  inputSchema: updateMailboxFolderInputSchema,
4306
4771
  outputSchema: mailboxFolderOutputSchema,
4307
4772
  annotations: {
@@ -4326,7 +4791,7 @@ function registerTools(rawServer, client, allowedTools) {
4326
4791
  "shipmail_delete_mailbox_folder",
4327
4792
  {
4328
4793
  title: "Delete Mailbox Folder",
4329
- description: "Delete a custom mailbox folder after moving its messages to Trash. Folders referenced by Assistant automations must be removed from those automations first.",
4794
+ description: "Delete a custom mailbox folder after moving its messages to Trash. Remove references from inbox rules and Assistant automations first.",
4330
4795
  inputSchema: deleteMailboxFolderInputSchema,
4331
4796
  outputSchema: acknowledgmentOutputSchema,
4332
4797
  annotations: {
@@ -6438,7 +6903,7 @@ function registerTools(rawServer, client, allowedTools) {
6438
6903
  }
6439
6904
 
6440
6905
  // src/version.ts
6441
- var VERSION = "0.5.4";
6906
+ var VERSION = "0.5.7";
6442
6907
 
6443
6908
  // src/server.ts
6444
6909
  var INSTRUCTIONS = `ShipMail MCP exposes the business email and calendar tools authorized by the connection's current ShipMail permissions.
@@ -6463,7 +6928,7 @@ function buildDefaultHeaders() {
6463
6928
  function componentConnectDomain(baseUrl) {
6464
6929
  return new URL(baseUrl ?? "https://shipmail.to/api/v1").origin;
6465
6930
  }
6466
- function createShipMailMcpServer(config, allowedTools) {
6931
+ function createShipMailMcpServer(config, allowedTools, organizationGrants = []) {
6467
6932
  const defaultHeaders = buildDefaultHeaders();
6468
6933
  const client = new ShipMailClient({
6469
6934
  apiKey: config.apiKey,
@@ -6471,7 +6936,7 @@ function createShipMailMcpServer(config, allowedTools) {
6471
6936
  ...config.organizationId ? { organizationId: config.organizationId } : {},
6472
6937
  defaultHeaders
6473
6938
  });
6474
- const server = new McpServer4(
6939
+ const server = new McpServer5(
6475
6940
  {
6476
6941
  name: "shipmail",
6477
6942
  version: VERSION
@@ -6480,7 +6945,26 @@ function createShipMailMcpServer(config, allowedTools) {
6480
6945
  instructions: INSTRUCTIONS
6481
6946
  }
6482
6947
  );
6483
- registerTools(server, client, allowedTools);
6948
+ const grantedOrganizations = organizationGrants.map(({ id, name }) => ({ id, name }));
6949
+ registerTools(server, client, allowedTools, grantedOrganizations);
6950
+ const crossOrganizationGrants = organizationGrants.map(
6951
+ (grant) => ({
6952
+ id: grant.id,
6953
+ name: grant.name,
6954
+ allowedTools: grant.allowedTools,
6955
+ // Cross-organization tools deliberately make exactly one bounded REST request per grant.
6956
+ // Retries would silently amplify one MCP call further, and a short timeout lets the tool
6957
+ // report a failed section instead of losing every organization's result to the route limit.
6958
+ client: new ShipMailClient({
6959
+ apiKey: grant.apiKey,
6960
+ ...config.baseUrl ? { baseUrl: config.baseUrl } : {},
6961
+ maxRetries: 0,
6962
+ timeout: 15e3,
6963
+ defaultHeaders
6964
+ })
6965
+ })
6966
+ );
6967
+ registerCrossOrganizationTools(server, crossOrganizationGrants);
6484
6968
  registerResources(server, client, componentConnectDomain(config.baseUrl));
6485
6969
  registerPrompts(server);
6486
6970
  return server;