apple-mail-mcp 2.10.8 → 2.10.9

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
  }
@@ -85154,7 +85165,11 @@ registerTool(
85154
85165
  // tolerated by the permissive advertisement (#135).
85155
85166
  perMailbox: external_exports.array(external_exports.object({}).passthrough()).optional(),
85156
85167
  partial: external_exports.boolean().optional(),
85157
- failedAccounts: external_exports.array(external_exports.string()).optional()
85168
+ failedAccounts: external_exports.array(external_exports.string()).optional(),
85169
+ // Present only when this call waited behind other tool calls. Without it,
85170
+ // queue wait is invisible from the outside and a caller timing the call
85171
+ // sees a duration no per-account budget explains (#135).
85172
+ queueWaitMs: external_exports.number().optional()
85158
85173
  }
85159
85174
  },
85160
85175
  withErrorHandling(async ({ account }) => {
@@ -85163,8 +85178,15 @@ registerTool(
85163
85178
  2e3,
85164
85179
  Number(process.env.APPLE_MAIL_MCP_STATS_DEADLINE_MS ?? 5e4)
85165
85180
  );
85166
- const startedAt = Date.now();
85181
+ const timing = currentCallTiming();
85182
+ const queueWaitMs = timing?.queueWaitMs ?? 0;
85183
+ const startedAt = timing?.arrivedAt ?? Date.now();
85167
85184
  const remainingMs = () => Math.max(0, deadlineMs - (Date.now() - startedAt));
85185
+ if (queueWaitMs > 0 && remainingMs() < 1e3) {
85186
+ return errorResponse(
85187
+ `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.`
85188
+ );
85189
+ }
85168
85190
  const withBudget = async (work, label) => {
85169
85191
  const ms = Math.min(budgetMs, remainingMs());
85170
85192
  let timer;
@@ -85185,7 +85207,9 @@ registerTool(
85185
85207
  s = await withBudget(imapMailStats({ account }), `IMAP mail-stats for "${account}"`);
85186
85208
  } catch (e) {
85187
85209
  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.`
85210
+ `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
85211
+ // clock down: the remaining deadline caps the budget (#135).
85212
+ (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
85213
  );
85190
85214
  }
85191
85215
  const lines2 = [
@@ -85199,7 +85223,17 @@ registerTool(
85199
85223
  ` Last 7 days: ${s.recent.last7d}`,
85200
85224
  ` Last 30 days: ${s.recent.last30d}`
85201
85225
  ];
85202
- return successResponse(lines2.join("\n"), { account, ...s });
85226
+ if (queueWaitMs >= 1e3) {
85227
+ lines2.push(
85228
+ ``,
85229
+ `\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.`
85230
+ );
85231
+ }
85232
+ return successResponse(lines2.join("\n"), {
85233
+ account,
85234
+ ...s,
85235
+ ...queueWaitMs >= 1e3 ? { queueWaitMs } : {}
85236
+ });
85203
85237
  }
85204
85238
  if (account === void 0 && shouldUseImap(account)) {
85205
85239
  let totalMessages = 0;
@@ -85300,7 +85334,15 @@ registerTool(
85300
85334
  if (failedAccounts.length > 0) {
85301
85335
  lines2.push(
85302
85336
  ``,
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.`
85337
+ `\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
85338
+ // deadline, instead of leaving it to look like a slow account (#135).
85339
+ (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.` : ``)
85340
+ );
85341
+ }
85342
+ if (queueWaitMs >= 1e3) {
85343
+ lines2.push(
85344
+ ``,
85345
+ `\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
85346
  );
85305
85347
  }
85306
85348
  return successResponse(lines2.join("\n"), {
@@ -85308,7 +85350,8 @@ registerTool(
85308
85350
  totalUnread,
85309
85351
  accounts: perAccount,
85310
85352
  recent,
85311
- ...failedAccounts.length > 0 ? { partial: true, failedAccounts } : {}
85353
+ ...failedAccounts.length > 0 ? { partial: true, failedAccounts } : {},
85354
+ ...queueWaitMs >= 1e3 ? { queueWaitMs } : {}
85312
85355
  });
85313
85356
  }
85314
85357
  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.9",
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",