apple-mail-mcp 2.10.8 → 2.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -486,7 +486,7 @@ for an explicitly-named IMAP account, never on an omitted account.
486
486
  | `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account |
487
487
  | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `30000` | Idle timeout (ms) before a pooled IMAP connection is closed (`0` = never close) |
488
488
  | `APPLE_MAIL_MCP_STATS_BUDGET_MS` | No | `25000` | Per-account wall-clock budget for `get-mail-stats` (minimum `1000`). Raise it for very large accounts |
489
- | `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | No | `50000` | Overall wall-clock deadline for one `get-mail-stats` call (minimum `2000`), covering account enumeration **and** every per-account read. Keep it below your client's request timeout |
489
+ | `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | No | `50000` | Overall wall-clock deadline for one `get-mail-stats` call (minimum `2000`), measured from when the request arrived and covering time queued behind other tool calls, account enumeration **and** every per-account read. Keep it below your client's request timeout |
490
490
 
491
491
  **Multiple IMAP accounts (C2):** set `APPLE_MAIL_MCP_IMAP_ACCOUNTS` to a JSON array, e.g.
492
492
  `[{"account":"Work","user":"me@co.com","host":"imap.co.com","keychainService":"imap.co.com"}]`.
@@ -1173,6 +1173,19 @@ degrading. Keep the deadline below your MCP client's request timeout — whateve
1173
1173
  cannot be read inside it is named in `failedAccounts`, so you always get a
1174
1174
  partial answer rather than a dead call.
1175
1175
 
1176
+ **Concurrent `get-mail-stats` calls do not run concurrently.** Tool calls are
1177
+ serialized so they cannot race into Mail.app's single-threaded AppleScript
1178
+ dispatch, so each call waits for the ones ahead of it and per-call latency grows
1179
+ with queue depth — N concurrent calls take about N × the single-call cost. Since
1180
+ this is the most expensive read tool, that is very visible here: measured on 3
1181
+ IMAP accounts, three concurrent calls returned at 5.5s / 10.3s / 15.6s against a
1182
+ ~5.2s solo cost. The deadline is measured from when the request **arrived**, so
1183
+ that wait is spent from the same budget as the work: a call that waited ≥1s
1184
+ reports `queueWaitMs`, and one that arrives with its deadline already spent
1185
+ returns straight away naming the queue rather than starting work whose answer
1186
+ would land after your client has given up. Issue these calls one at a time, and
1187
+ prefer `get-unread-count` when a single number will do.
1188
+
1176
1189
  ---
1177
1190
 
1178
1191
  #### `get-sync-status`
package/build/index.js CHANGED
@@ -82606,6 +82606,9 @@ async function imapThread(id, deps = {}, limit = 50) {
82606
82606
  );
82607
82607
  }
82608
82608
 
82609
+ // src/tools/respond.ts
82610
+ import { AsyncLocalStorage } from "node:async_hooks";
82611
+
82609
82612
  // src/utils/serialize.ts
82610
82613
  function createSerialGate(settleMs = 50) {
82611
82614
  let tail = Promise.resolve();
@@ -82674,15 +82677,23 @@ function partialCoverageBlock(diagnostics) {
82674
82677
  ${notes.map((n) => ` - ${n}`).join("\n")}`;
82675
82678
  }
82676
82679
  var serializeAppleScript = createSerialGate();
82680
+ var callTiming = new AsyncLocalStorage();
82681
+ function currentCallTiming() {
82682
+ return callTiming.getStore();
82683
+ }
82677
82684
  function withErrorHandling(handler, errorPrefix) {
82678
82685
  return async (params) => {
82686
+ const arrivedAt = Date.now();
82679
82687
  return serializeAppleScript(async () => {
82680
- try {
82681
- return await handler(params);
82682
- } catch (error2) {
82683
- const message = error2 instanceof Error ? error2.message : "Unknown error";
82684
- return errorResponse(`${errorPrefix}: ${message}`);
82685
- }
82688
+ const timing = { arrivedAt, queueWaitMs: Date.now() - arrivedAt };
82689
+ return callTiming.run(timing, async () => {
82690
+ try {
82691
+ return await handler(params);
82692
+ } catch (error2) {
82693
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
82694
+ return errorResponse(`${errorPrefix}: ${message}`);
82695
+ }
82696
+ });
82686
82697
  });
82687
82698
  };
82688
82699
  }
@@ -82777,6 +82788,7 @@ function planCountSources(accounts, configs) {
82777
82788
  usedConfigs.add(match);
82778
82789
  sources.push({ kind: "imap", config: match, label: account.name });
82779
82790
  } else {
82791
+ if (account.enabled === false) continue;
82780
82792
  sources.push({ kind: "applescript", account, label: account.name });
82781
82793
  }
82782
82794
  }
@@ -85154,7 +85166,11 @@ registerTool(
85154
85166
  // tolerated by the permissive advertisement (#135).
85155
85167
  perMailbox: external_exports.array(external_exports.object({}).passthrough()).optional(),
85156
85168
  partial: external_exports.boolean().optional(),
85157
- failedAccounts: external_exports.array(external_exports.string()).optional()
85169
+ failedAccounts: external_exports.array(external_exports.string()).optional(),
85170
+ // Present only when this call waited behind other tool calls. Without it,
85171
+ // queue wait is invisible from the outside and a caller timing the call
85172
+ // sees a duration no per-account budget explains (#135).
85173
+ queueWaitMs: external_exports.number().optional()
85158
85174
  }
85159
85175
  },
85160
85176
  withErrorHandling(async ({ account }) => {
@@ -85163,8 +85179,15 @@ registerTool(
85163
85179
  2e3,
85164
85180
  Number(process.env.APPLE_MAIL_MCP_STATS_DEADLINE_MS ?? 5e4)
85165
85181
  );
85166
- const startedAt = Date.now();
85182
+ const timing = currentCallTiming();
85183
+ const queueWaitMs = timing?.queueWaitMs ?? 0;
85184
+ const startedAt = timing?.arrivedAt ?? Date.now();
85167
85185
  const remainingMs = () => Math.max(0, deadlineMs - (Date.now() - startedAt));
85186
+ if (queueWaitMs > 0 && remainingMs() < 1e3) {
85187
+ return errorResponse(
85188
+ `Could not read mail statistics: the ${deadlineMs}ms overall deadline was spent waiting ${queueWaitMs}ms in the tool-call queue before this call could start. Tool calls are serialized so they cannot race into Mail.app, so concurrent get-mail-stats calls each wait for the ones ahead of them \u2014 and it is the most expensive read tool (one IMAP STATUS per mailbox, and Gmail lists every label). Issue one at a time, prefer "get-unread-count" when a single number will do, or raise APPLE_MAIL_MCP_STATS_DEADLINE_MS.`
85189
+ );
85190
+ }
85168
85191
  const withBudget = async (work, label) => {
85169
85192
  const ms = Math.min(budgetMs, remainingMs());
85170
85193
  let timer;
@@ -85185,7 +85208,9 @@ registerTool(
85185
85208
  s = await withBudget(imapMailStats({ account }), `IMAP mail-stats for "${account}"`);
85186
85209
  } catch (e) {
85187
85210
  return errorResponse(
85188
- `Could not read mail statistics for "${account}": ${String(e)}. Gathering stats costs one IMAP STATUS per mailbox, so a very large account can exceed the ${budgetMs}ms budget \u2014 raise APPLE_MAIL_MCP_STATS_BUDGET_MS, or run the "doctor" tool if the connection itself is the problem.`
85211
+ `Could not read mail statistics for "${account}": ${String(e)}. Gathering stats costs one IMAP STATUS per mailbox, so a very large account can exceed the ${budgetMs}ms budget \u2014 raise APPLE_MAIL_MCP_STATS_BUDGET_MS, or run the "doctor" tool if the connection itself is the problem.` + // Don't send someone tuning a budget when the queue is what ran the
85212
+ // clock down: the remaining deadline caps the budget (#135).
85213
+ (queueWaitMs >= 1e3 ? ` Note: this call waited ${queueWaitMs}ms behind other tool calls before starting, which counts against the ${deadlineMs}ms overall deadline and so caps the effective budget \u2014 issue get-mail-stats calls one at a time.` : ``)
85189
85214
  );
85190
85215
  }
85191
85216
  const lines2 = [
@@ -85199,7 +85224,17 @@ registerTool(
85199
85224
  ` Last 7 days: ${s.recent.last7d}`,
85200
85225
  ` Last 30 days: ${s.recent.last30d}`
85201
85226
  ];
85202
- return successResponse(lines2.join("\n"), { account, ...s });
85227
+ if (queueWaitMs >= 1e3) {
85228
+ lines2.push(
85229
+ ``,
85230
+ `\u23F3 Waited ${(queueWaitMs / 1e3).toFixed(1)}s in the tool-call queue before this call started (calls are serialized so they cannot race into Mail.app), so the time you measured is queue wait plus work.`
85231
+ );
85232
+ }
85233
+ return successResponse(lines2.join("\n"), {
85234
+ account,
85235
+ ...s,
85236
+ ...queueWaitMs >= 1e3 ? { queueWaitMs } : {}
85237
+ });
85203
85238
  }
85204
85239
  if (account === void 0 && shouldUseImap(account)) {
85205
85240
  let totalMessages = 0;
@@ -85300,7 +85335,15 @@ registerTool(
85300
85335
  if (failedAccounts.length > 0) {
85301
85336
  lines2.push(
85302
85337
  ``,
85303
- `\u26A0\uFE0F PARTIAL: ${failedAccounts.length} account(s) could not be read (${failedAccounts.join(", ")}), so the real totals are higher. They either failed, exceeded the ${budgetMs}ms per-account budget, or ran out of the ${deadlineMs}ms overall deadline \u2014 raise APPLE_MAIL_MCP_STATS_BUDGET_MS and/or APPLE_MAIL_MCP_STATS_DEADLINE_MS if an account is simply large, or run the "doctor" tool to check the connection.`
85338
+ `\u26A0\uFE0F PARTIAL: ${failedAccounts.length} account(s) could not be read (${failedAccounts.join(", ")}), so the real totals are higher. They either failed, exceeded the ${budgetMs}ms per-account budget, or ran out of the ${deadlineMs}ms overall deadline \u2014 raise APPLE_MAIL_MCP_STATS_BUDGET_MS and/or APPLE_MAIL_MCP_STATS_DEADLINE_MS if an account is simply large, or run the "doctor" tool to check the connection.` + // Attribute the shortfall to the queue when the queue is what ate the
85339
+ // deadline, instead of leaving it to look like a slow account (#135).
85340
+ (queueWaitMs >= 1e3 ? ` Note: ${queueWaitMs}ms of that deadline was spent queued behind other tool calls, not reading mail \u2014 issuing get-mail-stats calls one at a time will recover it.` : ``)
85341
+ );
85342
+ }
85343
+ if (queueWaitMs >= 1e3) {
85344
+ lines2.push(
85345
+ ``,
85346
+ `\u23F3 Waited ${(queueWaitMs / 1e3).toFixed(1)}s in the tool-call queue before this call started (calls are serialized so they cannot race into Mail.app), so the time you measured is queue wait plus work.`
85304
85347
  );
85305
85348
  }
85306
85349
  return successResponse(lines2.join("\n"), {
@@ -85308,7 +85351,8 @@ registerTool(
85308
85351
  totalUnread,
85309
85352
  accounts: perAccount,
85310
85353
  recent,
85311
- ...failedAccounts.length > 0 ? { partial: true, failedAccounts } : {}
85354
+ ...failedAccounts.length > 0 ? { partial: true, failedAccounts } : {},
85355
+ ...queueWaitMs >= 1e3 ? { queueWaitMs } : {}
85312
85356
  });
85313
85357
  }
85314
85358
  const stats = mailManager.getMailStats();
@@ -358,7 +358,7 @@ GUI is ignoring.
358
358
  | `APPLE_MAIL_MCP_IMAP_IDLE` | `1` to enable IMAP IDLE new-mail push. |
359
359
  | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | Pooled-connection idle timeout in ms (default `30000`; `0` = never close). |
360
360
  | `APPLE_MAIL_MCP_STATS_BUDGET_MS` | Per-account wall-clock budget for `get-mail-stats` in ms (default `25000`, minimum `1000`). |
361
- | `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | Overall wall-clock deadline for one `get-mail-stats` call in ms (default `50000`, minimum `2000`) — covers the Mail.app account enumeration as well as every per-account read. Keep it below your MCP client's request timeout. |
361
+ | `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | Overall wall-clock deadline for one `get-mail-stats` call in ms (default `50000`, minimum `2000`) — measured from when the request arrived, and covering time spent queued behind other tool calls as well as the Mail.app account enumeration and every per-account read. Keep it below your MCP client's request timeout. |
362
362
  | `APPLE_MAIL_MCP_SMTP_HOST` | SMTP host; setting it enables `transport:"smtp"`. |
363
363
  | `APPLE_MAIL_MCP_SMTP_PORT` | SMTP port (`465` if secure, else `587`). |
364
364
  | `APPLE_MAIL_MCP_SMTP_SECURE` | `true` for implicit TLS (465); else STARTTLS. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.10.8",
3
+ "version": "2.10.10",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",