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 +14 -1
- package/build/index.js +55 -12
- package/docs/IMAP-SETUP.md +1 -1
- package/package.json +1 -1
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
|
-
|
|
82681
|
-
|
|
82682
|
-
|
|
82683
|
-
|
|
82684
|
-
|
|
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
|
|
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
|
-
|
|
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();
|
package/docs/IMAP-SETUP.md
CHANGED
|
@@ -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`) —
|
|
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