apple-mail-mcp 2.16.1 → 2.17.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.
- package/README.md +15 -4
- package/build/index.js +241 -103
- package/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md +32 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -937,10 +937,21 @@ List all mailboxes for an account.
|
|
|
937
937
|
|-----------|------|----------|-------------|
|
|
938
938
|
| `account` | string | No | Account to list from, or `"On My Mac"` for the local store |
|
|
939
939
|
|
|
940
|
-
**Returns:** List of mailbox
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
940
|
+
**Returns:** List of mailbox **paths** (account-relative, e.g. `Archive/Inbox` for
|
|
941
|
+
a nested mailbox — a top-level `Inbox` stays `Inbox`) with message and unread
|
|
942
|
+
counts. A source that could not be read is **named** (`partial: true` +
|
|
943
|
+
`failedAccounts`) rather than dropped, and a listing Mail refused outright
|
|
944
|
+
returns an error naming the accounts that do exist — never an empty list.
|
|
945
|
+
|
|
946
|
+
**Nested mailboxes and Gmail labels.** Every `mailbox` parameter across this
|
|
947
|
+
server (search-messages, list-messages, get-unread-count, move-message,
|
|
948
|
+
delete-mailbox, rename-mailbox, create-rule's `moveTo`) accepts either the full
|
|
949
|
+
path or a leaf name that is unique across the account — the same rule
|
|
950
|
+
move-message has always used. A leaf name that matches more than one mailbox
|
|
951
|
+
(e.g. a top-level `Inbox` and an `Archive/Inbox` on an Exchange account) is
|
|
952
|
+
refused rather than guessed; pass the full path to disambiguate. This also
|
|
953
|
+
means Gmail's nested special mailboxes now report their real path, e.g.
|
|
954
|
+
`[Gmail]/All Mail` rather than `All Mail` — a visible change from before 2.17.0.
|
|
944
955
|
|
|
945
956
|
**Mail's local "On My Mac" mailboxes** are not children of any account — they
|
|
946
957
|
hang off the application — so they are reported under the synthetic account label
|
package/build/index.js
CHANGED
|
@@ -79291,6 +79291,41 @@ function buildAppLevelScript(command) {
|
|
|
79291
79291
|
end tell
|
|
79292
79292
|
`;
|
|
79293
79293
|
}
|
|
79294
|
+
function mailboxPathFragment(mailboxVar, outputVar) {
|
|
79295
|
+
return `
|
|
79296
|
+
set ${outputVar} to name of ${mailboxVar}
|
|
79297
|
+
set _pathParent to missing value
|
|
79298
|
+
try
|
|
79299
|
+
set _pathParent to container of ${mailboxVar}
|
|
79300
|
+
end try
|
|
79301
|
+
repeat while _pathParent is not missing value
|
|
79302
|
+
set _parentClass to missing value
|
|
79303
|
+
try
|
|
79304
|
+
set _parentClass to class of _pathParent
|
|
79305
|
+
end try
|
|
79306
|
+
if _parentClass is not mailbox and _parentClass is not container then exit repeat
|
|
79307
|
+
set ${outputVar} to (name of _pathParent) & "/" & ${outputVar}
|
|
79308
|
+
set _pathNext to missing value
|
|
79309
|
+
try
|
|
79310
|
+
set _pathNext to container of _pathParent
|
|
79311
|
+
end try
|
|
79312
|
+
set _pathParent to _pathNext
|
|
79313
|
+
end repeat`;
|
|
79314
|
+
}
|
|
79315
|
+
function mailboxLookupFragment(collExpr, path, outputVar) {
|
|
79316
|
+
return `
|
|
79317
|
+
set ${outputVar} to missing value
|
|
79318
|
+
repeat with _mbc in (${collExpr})
|
|
79319
|
+
set _mbcPath to ""
|
|
79320
|
+
${mailboxPathFragment("_mbc", "_mbcPath")}
|
|
79321
|
+
ignoring case
|
|
79322
|
+
if _mbcPath is "${escapeForAppleScript(path)}" then
|
|
79323
|
+
set ${outputVar} to _mbc
|
|
79324
|
+
exit repeat
|
|
79325
|
+
end if
|
|
79326
|
+
end ignoring
|
|
79327
|
+
end repeat`;
|
|
79328
|
+
}
|
|
79294
79329
|
var MAILBOX_ALIASES = {
|
|
79295
79330
|
inbox: ["INBOX", "Inbox", "inbox"],
|
|
79296
79331
|
sent: ["Sent", "Sent Items", "Sent Messages", "SENT", "sent"],
|
|
@@ -79299,13 +79334,39 @@ var MAILBOX_ALIASES = {
|
|
|
79299
79334
|
junk: ["Junk", "Junk Email", "Spam", "JUNK", "junk"],
|
|
79300
79335
|
archive: ["Archive", "ARCHIVE", "archive", "All Mail"]
|
|
79301
79336
|
};
|
|
79337
|
+
function mailboxLeaf(path) {
|
|
79338
|
+
return path.split("/").at(-1) ?? path;
|
|
79339
|
+
}
|
|
79340
|
+
function resolveAppleMailboxPath(mailbox, actualPaths) {
|
|
79341
|
+
if (actualPaths.length === 0) return mailbox;
|
|
79342
|
+
const candidates = [mailbox, ...MAILBOX_ALIASES[mailbox.toLowerCase()] ?? []];
|
|
79343
|
+
for (const candidate of candidates) {
|
|
79344
|
+
const exact = actualPaths.find((path) => path === candidate);
|
|
79345
|
+
if (exact) return exact;
|
|
79346
|
+
const folded = actualPaths.find((path) => path.toLowerCase() === candidate.toLowerCase());
|
|
79347
|
+
if (folded) return folded;
|
|
79348
|
+
}
|
|
79349
|
+
for (const candidate of candidates) {
|
|
79350
|
+
const leafMatches = actualPaths.filter(
|
|
79351
|
+
(path) => mailboxLeaf(path).toLowerCase() === candidate.toLowerCase()
|
|
79352
|
+
);
|
|
79353
|
+
if (leafMatches.length === 1) return leafMatches[0];
|
|
79354
|
+
if (leafMatches.length > 1) {
|
|
79355
|
+
const paths = [...leafMatches].sort().map((path) => `"${path}"`).join(" and ");
|
|
79356
|
+
throw new Error(
|
|
79357
|
+
`Mailbox "${mailbox}" is ambiguous \u2014 it matches ${paths}. Pass the full path.`
|
|
79358
|
+
);
|
|
79359
|
+
}
|
|
79360
|
+
}
|
|
79361
|
+
return mailbox;
|
|
79362
|
+
}
|
|
79302
79363
|
var GMAIL_INBOX_MAILBOXES = ["All Mail", "Important"];
|
|
79303
79364
|
var INBOX_SCOPE_NAMES = /* @__PURE__ */ new Set(["inbox"]);
|
|
79304
79365
|
function isInboxScope(mailbox) {
|
|
79305
79366
|
return INBOX_SCOPE_NAMES.has(mailbox.trim().toLowerCase());
|
|
79306
79367
|
}
|
|
79307
79368
|
function gmailReceivingMailboxes(mailboxNames) {
|
|
79308
|
-
const lower = mailboxNames.map((n) => n.toLowerCase());
|
|
79369
|
+
const lower = mailboxNames.map((n) => mailboxLeaf(n).toLowerCase());
|
|
79309
79370
|
if (!lower.includes("all mail")) return null;
|
|
79310
79371
|
const present = GMAIL_INBOX_MAILBOXES.filter((want) => lower.includes(want.toLowerCase()));
|
|
79311
79372
|
return present.length > 0 ? present : null;
|
|
@@ -79429,7 +79490,11 @@ var AppleMailManager = class {
|
|
|
79429
79490
|
if (count of _acctM) is 1 then
|
|
79430
79491
|
set _mbM to {}
|
|
79431
79492
|
repeat with _m in (mailboxes of (item 1 of _acctM))
|
|
79432
|
-
|
|
79493
|
+
set _mPath to ""
|
|
79494
|
+
${mailboxPathFragment("_m", "_mPath")}
|
|
79495
|
+
ignoring case
|
|
79496
|
+
if _mPath is "${escapeForAppleScript(resolved)}" then set end of _mbM to _m
|
|
79497
|
+
end ignoring
|
|
79433
79498
|
end repeat
|
|
79434
79499
|
if (count of _mbM) is 1 then set _tmb to item 1 of _mbM
|
|
79435
79500
|
end if`;
|
|
@@ -79539,11 +79604,16 @@ ${indent}set _out to _out & (_idx as string) & "${FIELD_SEP}error:" & _zErr & "$
|
|
|
79539
79604
|
* are read from Mail at runtime (`_uacct`, `mailbox of _msg`) instead of being
|
|
79540
79605
|
* interpolated as literals from here — so they get the same delimiter
|
|
79541
79606
|
* stripping the literal paths get in TypeScript.
|
|
79607
|
+
*
|
|
79608
|
+
* Emits the canonical container-walked path, not the leaf — otherwise
|
|
79609
|
+
* `Inbox` and `Archive/Inbox` collapse into the same RECON record, and the
|
|
79610
|
+
* forensics comparison that reads it back (`sameMailbox` in
|
|
79611
|
+
* `recordSingleForensics`/the batch path) can be handed an ambiguous leaf.
|
|
79542
79612
|
*/
|
|
79543
79613
|
reconEmitFromMessage(beforeVar, afterVar, posExpr = '""', indent = " ") {
|
|
79544
79614
|
return `set _umbName to ""
|
|
79545
79615
|
${indent}try
|
|
79546
|
-
${
|
|
79616
|
+
${mailboxPathFragment("_umb", "_umbName")}
|
|
79547
79617
|
${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragment("_umbName", indent)}${this.reconEmit("_uacct", "_umbName", beforeVar, afterVar, posExpr)}`;
|
|
79548
79618
|
}
|
|
79549
79619
|
/**
|
|
@@ -80018,8 +80088,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80018
80088
|
return accounts;
|
|
80019
80089
|
}
|
|
80020
80090
|
/**
|
|
80021
|
-
* Returns cached mailbox
|
|
80022
|
-
* This caches only the
|
|
80091
|
+
* Returns cached canonical mailbox paths for an account, or fetches fresh.
|
|
80092
|
+
* This caches only the path list used by resolveMailbox(), not the
|
|
80023
80093
|
* full Mailbox objects with counts (which change frequently).
|
|
80024
80094
|
*/
|
|
80025
80095
|
getCachedMailboxNames(account) {
|
|
@@ -80221,31 +80291,22 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80221
80291
|
* @returns Actual mailbox name, or original if not found
|
|
80222
80292
|
*/
|
|
80223
80293
|
resolveMailbox(mailbox, account) {
|
|
80224
|
-
|
|
80225
|
-
|
|
80226
|
-
|
|
80227
|
-
|
|
80228
|
-
|
|
80294
|
+
return resolveAppleMailboxPath(mailbox, this.getCachedMailboxNames(account));
|
|
80295
|
+
}
|
|
80296
|
+
/**
|
|
80297
|
+
* Non-throwing `resolveMailbox`, for callers comparing mailbox names AFTER
|
|
80298
|
+
* a destructive op already ran (the forensics `sameMailbox` closures). An
|
|
80299
|
+
* ambiguous leaf there must not raise — the op already happened, and a
|
|
80300
|
+
* thrown error would misreport a successful move as a failure while the
|
|
80301
|
+
* message sits safely in its new mailbox. Falls back to the unresolved
|
|
80302
|
+
* input, which only degrades the self-move comparison, never the mutation.
|
|
80303
|
+
*/
|
|
80304
|
+
resolveMailboxSafe(mailbox, account) {
|
|
80305
|
+
try {
|
|
80306
|
+
return this.resolveMailbox(mailbox, account);
|
|
80307
|
+
} catch {
|
|
80229
80308
|
return mailbox;
|
|
80230
80309
|
}
|
|
80231
|
-
const lowerMailbox = mailbox.toLowerCase();
|
|
80232
|
-
const caseMatch = actualMailboxes.find((mb) => mb.toLowerCase() === lowerMailbox);
|
|
80233
|
-
if (caseMatch) {
|
|
80234
|
-
return caseMatch;
|
|
80235
|
-
}
|
|
80236
|
-
const aliases = MAILBOX_ALIASES[lowerMailbox];
|
|
80237
|
-
if (aliases) {
|
|
80238
|
-
for (const alias of aliases) {
|
|
80239
|
-
if (actualMailboxes.includes(alias)) {
|
|
80240
|
-
return alias;
|
|
80241
|
-
}
|
|
80242
|
-
const aliasMatch = actualMailboxes.find((mb) => mb.toLowerCase() === alias.toLowerCase());
|
|
80243
|
-
if (aliasMatch) {
|
|
80244
|
-
return aliasMatch;
|
|
80245
|
-
}
|
|
80246
|
-
}
|
|
80247
|
-
}
|
|
80248
|
-
return mailbox;
|
|
80249
80310
|
}
|
|
80250
80311
|
// ===========================================================================
|
|
80251
80312
|
// Message Operations
|
|
@@ -80304,20 +80365,26 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80304
80365
|
for (const acct of accounts) {
|
|
80305
80366
|
if (allMessages.length >= limit) break;
|
|
80306
80367
|
const remaining = limit - allMessages.length;
|
|
80307
|
-
|
|
80308
|
-
|
|
80309
|
-
|
|
80310
|
-
|
|
80311
|
-
|
|
80312
|
-
|
|
80313
|
-
|
|
80314
|
-
|
|
80315
|
-
|
|
80316
|
-
|
|
80317
|
-
|
|
80318
|
-
|
|
80319
|
-
|
|
80320
|
-
|
|
80368
|
+
try {
|
|
80369
|
+
const res = this.searchMessagesWithDiagnostics(
|
|
80370
|
+
query,
|
|
80371
|
+
mailbox,
|
|
80372
|
+
acct.name,
|
|
80373
|
+
remaining,
|
|
80374
|
+
dateFrom,
|
|
80375
|
+
dateTo,
|
|
80376
|
+
from,
|
|
80377
|
+
subject,
|
|
80378
|
+
isRead,
|
|
80379
|
+
isFlagged
|
|
80380
|
+
);
|
|
80381
|
+
allMessages.push(...res.messages);
|
|
80382
|
+
mergeSearchDiagnostics(diagnostics, res.diagnostics);
|
|
80383
|
+
} catch (err) {
|
|
80384
|
+
diagnostics.partial = true;
|
|
80385
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
80386
|
+
diagnostics.notSearchedMailboxes.push(`${acct.name} / ${mailbox ?? "*"}: ${message}`);
|
|
80387
|
+
}
|
|
80321
80388
|
}
|
|
80322
80389
|
return { messages: allMessages.slice(0, limit), diagnostics };
|
|
80323
80390
|
}
|
|
@@ -80343,10 +80410,14 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80343
80410
|
}
|
|
80344
80411
|
const scanThreshold = getMailboxScanThreshold();
|
|
80345
80412
|
let searchCommand;
|
|
80413
|
+
let resultMailbox = mailbox || "INBOX";
|
|
80414
|
+
let rowsIncludeMailbox = !mailbox;
|
|
80346
80415
|
if (mailbox) {
|
|
80347
80416
|
const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
|
|
80417
|
+
resultMailbox = targetMailbox;
|
|
80348
80418
|
const gmailInbox = isInboxScope(mailbox) ? gmailReceivingMailboxes(this.getCachedMailboxNames(targetAccount)) : null;
|
|
80349
80419
|
if (gmailInbox) {
|
|
80420
|
+
rowsIncludeMailbox = true;
|
|
80350
80421
|
const nameList = appleScriptLowerNameList(gmailInbox);
|
|
80351
80422
|
searchCommand = `
|
|
80352
80423
|
${dateSetup}set outputText to ""
|
|
@@ -80359,12 +80430,12 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80359
80430
|
if msgCount >= ${limit} then exit repeat
|
|
80360
80431
|
set mbName to ""
|
|
80361
80432
|
try
|
|
80362
|
-
|
|
80433
|
+
${mailboxPathFragment("mb", "mbName")}
|
|
80363
80434
|
end try
|
|
80364
80435
|
ignoring case
|
|
80365
|
-
if _wantNames contains
|
|
80436
|
+
if _wantNames contains (name of mb) then
|
|
80366
80437
|
try
|
|
80367
|
-
${buildMessageRowLoop({ collection: `messages of mb ${searchCondition}`, limit, dedup: true, dateFilter })}
|
|
80438
|
+
${buildMessageRowLoop({ collection: `messages of mb ${searchCondition}`, limit, dedup: true, dateFilter, trailing: ` & "${FIELD_SEP}" & mbName` })}
|
|
80368
80439
|
on error _errMsg number _errNum
|
|
80369
80440
|
set _timedOut to true
|
|
80370
80441
|
set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
|
|
@@ -80379,14 +80450,19 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80379
80450
|
${dateSetup}set outputText to ""
|
|
80380
80451
|
set _timedOut to false
|
|
80381
80452
|
set _notSearched to ""
|
|
80382
|
-
|
|
80453
|
+
${mailboxLookupFragment(mbIter, targetMailbox, "theMailbox")}
|
|
80383
80454
|
set msgCount to 0
|
|
80384
|
-
|
|
80385
|
-
${buildMessageRowLoop({ collection: `messages of theMailbox ${searchCondition}`, limit, dateFilter })}
|
|
80386
|
-
on error _errMsg number _errNum
|
|
80455
|
+
if theMailbox is missing value then
|
|
80387
80456
|
set _timedOut to true
|
|
80388
80457
|
set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
|
|
80389
|
-
|
|
80458
|
+
else
|
|
80459
|
+
try
|
|
80460
|
+
${buildMessageRowLoop({ collection: `messages of theMailbox ${searchCondition}`, limit, dateFilter })}
|
|
80461
|
+
on error _errMsg number _errNum
|
|
80462
|
+
set _timedOut to true
|
|
80463
|
+
set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
|
|
80464
|
+
end try
|
|
80465
|
+
end if
|
|
80390
80466
|
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=${DIAG_FIELD_SEP}notSearched=" & _notSearched
|
|
80391
80467
|
`;
|
|
80392
80468
|
}
|
|
@@ -80404,7 +80480,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80404
80480
|
if msgCount >= ${limit} then exit repeat
|
|
80405
80481
|
set mbName to ""
|
|
80406
80482
|
try
|
|
80407
|
-
|
|
80483
|
+
${mailboxPathFragment("mb", "mbName")}
|
|
80408
80484
|
end try
|
|
80409
80485
|
if ((current date) - _startedAt) > ${SEARCH_ACCOUNT_BUDGET_SECONDS} then
|
|
80410
80486
|
set _timedOut to true
|
|
@@ -80444,15 +80520,15 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80444
80520
|
}
|
|
80445
80521
|
};
|
|
80446
80522
|
}
|
|
80447
|
-
return this.parseSearchResult(result.output,
|
|
80523
|
+
return this.parseSearchResult(result.output, resultMailbox, targetAccount, rowsIncludeMailbox);
|
|
80448
80524
|
}
|
|
80449
80525
|
/**
|
|
80450
80526
|
* Split a per-account search payload into its message list and the DIAG
|
|
80451
80527
|
* trailer, parse both, and return a SearchResult. See searchMessagesWithDiagnostics.
|
|
80452
80528
|
*/
|
|
80453
|
-
parseSearchResult(output, mailbox, account) {
|
|
80529
|
+
parseSearchResult(output, mailbox, account, rowsIncludeMailbox = false) {
|
|
80454
80530
|
const { payload, diagnostics } = splitSearchDiagnostics(output, account);
|
|
80455
|
-
const messages = payload.trim() ? this.parseMessageList(payload, mailbox, account) : [];
|
|
80531
|
+
const messages = payload.trim() ? this.parseMessageList(payload, mailbox, account, rowsIncludeMailbox) : [];
|
|
80456
80532
|
return { messages, diagnostics };
|
|
80457
80533
|
}
|
|
80458
80534
|
/**
|
|
@@ -80484,7 +80560,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80484
80560
|
set msgFlagged to flagged status of msg as string
|
|
80485
80561
|
set msgJunk to junk mail status of msg as string
|
|
80486
80562
|
set msgDeleted to deleted status of msg as string
|
|
80487
|
-
set msgMailbox to
|
|
80563
|
+
set msgMailbox to ""
|
|
80564
|
+
${mailboxPathFragment("mb", "msgMailbox")}
|
|
80488
80565
|
set msgAccount to name of acct
|
|
80489
80566
|
set hasAtt to "false"
|
|
80490
80567
|
try
|
|
@@ -80545,7 +80622,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80545
80622
|
set targetMb to missing value
|
|
80546
80623
|
ignoring case
|
|
80547
80624
|
repeat with mb in _mbs
|
|
80548
|
-
|
|
80625
|
+
set _mbPath to ""
|
|
80626
|
+
${mailboxPathFragment("mb", "_mbPath")}
|
|
80627
|
+
if _mbPath is "${escapeForAppleScript(resolved)}" then
|
|
80549
80628
|
set targetMb to mb
|
|
80550
80629
|
exit repeat
|
|
80551
80630
|
end if
|
|
@@ -80554,7 +80633,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80554
80633
|
set targetMb to missing value
|
|
80555
80634
|
ignoring case
|
|
80556
80635
|
repeat with mb in mailboxes of acct
|
|
80557
|
-
|
|
80636
|
+
set _mbPath to ""
|
|
80637
|
+
${mailboxPathFragment("mb", "_mbPath")}
|
|
80638
|
+
if _mbPath is "${escapeForAppleScript(resolved)}" then
|
|
80558
80639
|
set targetMb to mb
|
|
80559
80640
|
exit repeat
|
|
80560
80641
|
end if
|
|
@@ -80799,9 +80880,15 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80799
80880
|
for (const acct of accounts) {
|
|
80800
80881
|
if (allMessages.length >= limit) break;
|
|
80801
80882
|
const remaining = limit - allMessages.length;
|
|
80802
|
-
|
|
80803
|
-
|
|
80804
|
-
|
|
80883
|
+
try {
|
|
80884
|
+
const res = this.listMessagesWithDiagnostics(mailbox, acct.name, remaining, from, offset);
|
|
80885
|
+
allMessages.push(...res.messages);
|
|
80886
|
+
mergeSearchDiagnostics(diagnostics, res.diagnostics);
|
|
80887
|
+
} catch (err) {
|
|
80888
|
+
diagnostics.partial = true;
|
|
80889
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
80890
|
+
diagnostics.notSearchedMailboxes.push(`${acct.name} / ${mailbox ?? "*"}: ${message}`);
|
|
80891
|
+
}
|
|
80805
80892
|
}
|
|
80806
80893
|
return { messages: allMessages.slice(0, limit), diagnostics };
|
|
80807
80894
|
}
|
|
@@ -80812,10 +80899,14 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80812
80899
|
const scanThreshold = getMailboxScanThreshold();
|
|
80813
80900
|
const mbIter = local ? "_mbs" : "mailboxes";
|
|
80814
80901
|
let listCommand;
|
|
80902
|
+
let resultMailbox = mailbox || "INBOX";
|
|
80903
|
+
let rowsIncludeMailbox = !mailbox;
|
|
80815
80904
|
if (mailbox) {
|
|
80816
80905
|
const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
|
|
80906
|
+
resultMailbox = targetMailbox;
|
|
80817
80907
|
const gmailInbox = isInboxScope(mailbox) ? gmailReceivingMailboxes(this.getCachedMailboxNames(targetAccount)) : null;
|
|
80818
80908
|
if (gmailInbox) {
|
|
80909
|
+
rowsIncludeMailbox = true;
|
|
80819
80910
|
const nameList = appleScriptLowerNameList(gmailInbox);
|
|
80820
80911
|
listCommand = `
|
|
80821
80912
|
set outputText to ""
|
|
@@ -80829,10 +80920,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80829
80920
|
if msgCount >= ${limit} then exit repeat
|
|
80830
80921
|
set mbName to ""
|
|
80831
80922
|
try
|
|
80832
|
-
|
|
80923
|
+
${mailboxPathFragment("mb", "mbName")}
|
|
80833
80924
|
end try
|
|
80834
80925
|
ignoring case
|
|
80835
|
-
if _wantNames contains
|
|
80926
|
+
if _wantNames contains (name of mb) then
|
|
80836
80927
|
try
|
|
80837
80928
|
${buildMessageRowLoop({ collection: `messages of mb ${fromFilter}`, limit, offset, dedup: true, withAttachments: true, trailing: ` & "${FIELD_SEP}" & mbName` })}
|
|
80838
80929
|
on error _errMsg number _errNum
|
|
@@ -80849,15 +80940,20 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80849
80940
|
set outputText to ""
|
|
80850
80941
|
set _timedOut to false
|
|
80851
80942
|
set _notSearched to ""
|
|
80852
|
-
|
|
80943
|
+
${mailboxLookupFragment(mbIter, targetMailbox, "theMailbox")}
|
|
80853
80944
|
set msgCount to 0
|
|
80854
80945
|
set skipped to 0
|
|
80855
|
-
|
|
80856
|
-
${buildMessageRowLoop({ collection: `messages of theMailbox ${fromFilter}`, limit, offset, withAttachments: true })}
|
|
80857
|
-
on error _errMsg number _errNum
|
|
80946
|
+
if theMailbox is missing value then
|
|
80858
80947
|
set _timedOut to true
|
|
80859
80948
|
set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
|
|
80860
|
-
|
|
80949
|
+
else
|
|
80950
|
+
try
|
|
80951
|
+
${buildMessageRowLoop({ collection: `messages of theMailbox ${fromFilter}`, limit, offset, withAttachments: true })}
|
|
80952
|
+
on error _errMsg number _errNum
|
|
80953
|
+
set _timedOut to true
|
|
80954
|
+
set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
|
|
80955
|
+
end try
|
|
80956
|
+
end if
|
|
80861
80957
|
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=${DIAG_FIELD_SEP}notSearched=" & _notSearched
|
|
80862
80958
|
`;
|
|
80863
80959
|
}
|
|
@@ -80876,7 +80972,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80876
80972
|
if msgCount >= ${limit} then exit repeat
|
|
80877
80973
|
set mbName to ""
|
|
80878
80974
|
try
|
|
80879
|
-
|
|
80975
|
+
${mailboxPathFragment("mb", "mbName")}
|
|
80880
80976
|
end try
|
|
80881
80977
|
if ((current date) - _startedAt) > ${SEARCH_ACCOUNT_BUDGET_SECONDS} then
|
|
80882
80978
|
set _timedOut to true
|
|
@@ -80916,7 +81012,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80916
81012
|
}
|
|
80917
81013
|
};
|
|
80918
81014
|
}
|
|
80919
|
-
return this.parseSearchResult(result.output,
|
|
81015
|
+
return this.parseSearchResult(result.output, resultMailbox, targetAccount, rowsIncludeMailbox);
|
|
80920
81016
|
}
|
|
80921
81017
|
/**
|
|
80922
81018
|
* Parse message list output from AppleScript.
|
|
@@ -80929,7 +81025,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80929
81025
|
* false-negative for MIME-embedded attachments (a known AppleScript
|
|
80930
81026
|
* limitation). Use getMessage or list-attachments for authoritative info.
|
|
80931
81027
|
*/
|
|
80932
|
-
parseMessageList(output, mailbox, account) {
|
|
81028
|
+
parseMessageList(output, mailbox, account, rowsIncludeMailbox = false) {
|
|
80933
81029
|
const items = output.split(RECORD_SEP);
|
|
80934
81030
|
const messages = [];
|
|
80935
81031
|
for (const item of items) {
|
|
@@ -80937,9 +81033,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80937
81033
|
if (parts.length < 6) continue;
|
|
80938
81034
|
let msgMailbox = mailbox;
|
|
80939
81035
|
let hasAttachments = false;
|
|
80940
|
-
if (parts.length >=
|
|
81036
|
+
if (rowsIncludeMailbox && parts.length >= 7) {
|
|
80941
81037
|
msgMailbox = parts[6];
|
|
80942
|
-
hasAttachments = parts[7] === "true";
|
|
81038
|
+
hasAttachments = parts.length >= 8 ? parts[7] === "true" : false;
|
|
80943
81039
|
} else if (parts.length === 7) {
|
|
80944
81040
|
hasAttachments = parts[6] === "true";
|
|
80945
81041
|
}
|
|
@@ -81367,7 +81463,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
81367
81463
|
const valid = [{ id, num: Number(id) }];
|
|
81368
81464
|
const parsed = this.parseForensicStream(output, valid);
|
|
81369
81465
|
const succeeded = parsed.okPositions.has(1);
|
|
81370
|
-
const sameMailbox = (account, mailbox) => destination !== void 0 && destination.account === account && this.
|
|
81466
|
+
const sameMailbox = (account, mailbox) => destination !== void 0 && destination.account === account && this.resolveMailboxSafe(destination.mailbox, destination.account) === this.resolveMailboxSafe(mailbox, account);
|
|
81371
81467
|
const home = parsed.recons[0];
|
|
81372
81468
|
this.lastForensics = this.buildForensicReport(
|
|
81373
81469
|
parsed,
|
|
@@ -81540,9 +81636,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
81540
81636
|
* Move a message to a destination mailbox, with full nested-mailbox support.
|
|
81541
81637
|
*
|
|
81542
81638
|
* Resolving the destination as `mailbox "X" of account "Y"` only finds
|
|
81543
|
-
* top-level mailboxes
|
|
81544
|
-
*
|
|
81545
|
-
*
|
|
81639
|
+
* top-level mailboxes on some stores. Instead we walk the account's recursive
|
|
81640
|
+
* mailbox collection, reconstruct each container path, and match by path.
|
|
81641
|
+
* Resolution is:
|
|
81546
81642
|
* - account-scoped (won't move to a same-named mailbox in another account)
|
|
81547
81643
|
* - ambiguity-aware: if the name matches more than one mailbox in the
|
|
81548
81644
|
* account we refuse to guess and return an error — silently moving mail to
|
|
@@ -81613,15 +81709,17 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
81613
81709
|
return "1${FIELD_SEP}ok" & _pre & "${RECORD_SEP}" & _out`;
|
|
81614
81710
|
const script = buildAppLevelScript(`
|
|
81615
81711
|
try
|
|
81616
|
-
-- \`mailboxes of account\`
|
|
81617
|
-
--
|
|
81618
|
-
--
|
|
81619
|
-
-- we match against this flat list by exact name and use the reference directly
|
|
81620
|
-
-- (addressing \`mailbox "X" of account "Y"\` only finds some top-level mailboxes).
|
|
81712
|
+
-- \`mailboxes of account\` recursively includes nested mailboxes, but
|
|
81713
|
+
-- \`name of mb\` is only the leaf. Reconstruct each path from its
|
|
81714
|
+
-- container chain so Inbox and Archive/Inbox remain distinct.
|
|
81621
81715
|
set destName to "${safeMailbox}"
|
|
81622
81716
|
set destMatches to {}
|
|
81623
81717
|
repeat with mb in (mailboxes of account "${safeAccount}")
|
|
81624
|
-
|
|
81718
|
+
set _destPath to ""
|
|
81719
|
+
${mailboxPathFragment("mb", "_destPath")}
|
|
81720
|
+
ignoring case
|
|
81721
|
+
if _destPath is destName then set end of destMatches to mb
|
|
81722
|
+
end ignoring
|
|
81625
81723
|
end repeat
|
|
81626
81724
|
if (count of destMatches) is 0 then return "error:Destination mailbox \\"" & destName & "\\" not found in account \\"${safeAccount}\\""
|
|
81627
81725
|
if (count of destMatches) > 1 then return "error:Destination mailbox \\"" & destName & "\\" is ambiguous (" & (count of destMatches) & " matches) in account \\"${safeAccount}\\"; disambiguate or move by full path"
|
|
@@ -81927,7 +82025,7 @@ ${this.errorEmit(" ")}
|
|
|
81927
82025
|
}
|
|
81928
82026
|
const { okPositions } = parsed;
|
|
81929
82027
|
const dest = forensics?.destination;
|
|
81930
|
-
const sameMailbox = (account, mailbox) => dest !== void 0 && dest.account === account && this.
|
|
82028
|
+
const sameMailbox = (account, mailbox) => dest !== void 0 && dest.account === account && this.resolveMailboxSafe(dest.mailbox, dest.account) === this.resolveMailboxSafe(mailbox, account);
|
|
81931
82029
|
const expectedFor = (account, mailbox, pos) => {
|
|
81932
82030
|
if (sameMailbox(account, mailbox)) return null;
|
|
81933
82031
|
if (pos !== null) return okPositions.has(pos) ? 1 : 0;
|
|
@@ -81973,7 +82071,11 @@ ${this.errorEmit(" ")}
|
|
|
81973
82071
|
set destName to "${safeMailbox}"
|
|
81974
82072
|
set destMatches to {}
|
|
81975
82073
|
repeat with _dmb in (mailboxes of account "${safeAccount}")
|
|
81976
|
-
|
|
82074
|
+
set _destPath to ""
|
|
82075
|
+
${mailboxPathFragment("_dmb", "_destPath")}
|
|
82076
|
+
ignoring case
|
|
82077
|
+
if _destPath is destName then set end of destMatches to _dmb
|
|
82078
|
+
end ignoring
|
|
81977
82079
|
end repeat
|
|
81978
82080
|
if (count of destMatches) is 0 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" not found in account \\"${safeAccount}\\""
|
|
81979
82081
|
if (count of destMatches) > 1 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" is ambiguous (" & (count of destMatches) & " matches) in account \\"${safeAccount}\\"; move by full path"
|
|
@@ -82206,10 +82308,11 @@ ${this.errorEmit(" ")}
|
|
|
82206
82308
|
const listCommand = (iterExpr) => `
|
|
82207
82309
|
set mailboxList to {}
|
|
82208
82310
|
repeat with mb in ${iterExpr}
|
|
82209
|
-
set
|
|
82311
|
+
set mbPath to ""
|
|
82312
|
+
${mailboxPathFragment("mb", "mbPath")}
|
|
82210
82313
|
set mbUnread to unread count of mb
|
|
82211
82314
|
set mbCount to count of messages of mb
|
|
82212
|
-
set end of mailboxList to
|
|
82315
|
+
set end of mailboxList to mbPath & "${FIELD_SEP}" & mbUnread & "${FIELD_SEP}" & mbCount
|
|
82213
82316
|
end repeat
|
|
82214
82317
|
set AppleScript's text item delimiters to "${RECORD_SEP}"
|
|
82215
82318
|
return mailboxList as text
|
|
@@ -82257,8 +82360,10 @@ ${this.errorEmit(" ")}
|
|
|
82257
82360
|
getUnreadCount(mailbox, account) {
|
|
82258
82361
|
const targetAccount = this.resolveAccount(account);
|
|
82259
82362
|
const targetMailbox = this.resolveMailbox(mailbox || "INBOX", targetAccount);
|
|
82260
|
-
const
|
|
82261
|
-
|
|
82363
|
+
const command = `
|
|
82364
|
+
${mailboxLookupFragment("mailboxes", targetMailbox, "theMailbox")}
|
|
82365
|
+
if theMailbox is missing value then error "Mailbox \\"${escapeForAppleScript(targetMailbox)}\\" not found"
|
|
82366
|
+
return unread count of theMailbox`;
|
|
82262
82367
|
const script = buildAccountScopedScript(targetAccount, command);
|
|
82263
82368
|
const result = executeAppleScript(script, { timeoutMs: 6e4 });
|
|
82264
82369
|
if (!result.success) {
|
|
@@ -82314,11 +82419,12 @@ ${this.errorEmit(" ")}
|
|
|
82314
82419
|
return { success: false, error: disabled };
|
|
82315
82420
|
}
|
|
82316
82421
|
const targetMailbox = this.resolveMailbox(name, targetAccount);
|
|
82317
|
-
const safeName = escapeForAppleScript(targetMailbox);
|
|
82318
82422
|
const safeAccount = escapeForAppleScript(targetAccount);
|
|
82319
82423
|
const script = buildAppLevelScript(`
|
|
82320
82424
|
try
|
|
82321
|
-
|
|
82425
|
+
${mailboxLookupFragment(`mailboxes of account "${safeAccount}"`, targetMailbox, "theMailbox")}
|
|
82426
|
+
if theMailbox is missing value then error "Mailbox \\"${escapeForAppleScript(targetMailbox)}\\" not found in account \\"${safeAccount}\\""
|
|
82427
|
+
delete theMailbox
|
|
82322
82428
|
return "ok"
|
|
82323
82429
|
on error errMsg
|
|
82324
82430
|
return "error:" & errMsg
|
|
@@ -82344,6 +82450,14 @@ ${this.errorEmit(" ")}
|
|
|
82344
82450
|
console.error(`Refusing to rename mailbox: ${serverSide}`);
|
|
82345
82451
|
return { success: false, error: serverSide };
|
|
82346
82452
|
}
|
|
82453
|
+
let resolvedOld;
|
|
82454
|
+
try {
|
|
82455
|
+
resolvedOld = this.resolveMailbox(oldName, targetAccount);
|
|
82456
|
+
} catch (err) {
|
|
82457
|
+
const error2 = err instanceof Error ? err.message : String(err);
|
|
82458
|
+
console.error(`Refusing to rename mailbox: ${error2}`);
|
|
82459
|
+
return { success: false, error: error2 };
|
|
82460
|
+
}
|
|
82347
82461
|
const created = this.createMailbox(newName, targetAccount);
|
|
82348
82462
|
if (!created.success) {
|
|
82349
82463
|
return {
|
|
@@ -82351,15 +82465,25 @@ ${this.errorEmit(" ")}
|
|
|
82351
82465
|
error: created.error ?? `Could not create the destination mailbox "${newName}" needed for the rename.`
|
|
82352
82466
|
};
|
|
82353
82467
|
}
|
|
82354
|
-
|
|
82355
|
-
|
|
82356
|
-
|
|
82357
|
-
|
|
82468
|
+
let resolvedNew;
|
|
82469
|
+
try {
|
|
82470
|
+
resolvedNew = this.resolveMailbox(newName, targetAccount);
|
|
82471
|
+
} catch (err) {
|
|
82472
|
+
const rolledBack = this.deleteMailboxIfEmpty(newName, targetAccount);
|
|
82473
|
+
let error2 = err instanceof Error ? err.message : String(err);
|
|
82474
|
+
error2 += rolledBack ? ` The empty destination mailbox "${newName}" was rolled back, so no orphan was left.` : ` The destination mailbox "${newName}" was created and could not be auto-removed; delete it manually if it is an empty leftover.`;
|
|
82475
|
+
console.error(`Failed to rename mailbox: ${error2}`);
|
|
82476
|
+
this.invalidateCache();
|
|
82477
|
+
return { success: false, error: error2 };
|
|
82478
|
+
}
|
|
82358
82479
|
const safeAccount = escapeForAppleScript(targetAccount);
|
|
82480
|
+
const mbColl = `mailboxes of account "${safeAccount}"`;
|
|
82359
82481
|
const moveScript = buildAppLevelScript(`
|
|
82360
82482
|
try
|
|
82361
|
-
|
|
82362
|
-
|
|
82483
|
+
${mailboxLookupFragment(mbColl, resolvedOld, "srcMailbox")}
|
|
82484
|
+
${mailboxLookupFragment(mbColl, resolvedNew, "destMailbox")}
|
|
82485
|
+
if srcMailbox is missing value then error "Mailbox \\"${escapeForAppleScript(resolvedOld)}\\" not found in account \\"${safeAccount}\\""
|
|
82486
|
+
if destMailbox is missing value then error "Mailbox \\"${escapeForAppleScript(resolvedNew)}\\" not found in account \\"${safeAccount}\\""
|
|
82363
82487
|
set srcCount to count of messages of srcMailbox
|
|
82364
82488
|
set msgs to (every message of srcMailbox)
|
|
82365
82489
|
repeat with m in msgs
|
|
@@ -82369,7 +82493,7 @@ ${this.errorEmit(" ")}
|
|
|
82369
82493
|
end repeat
|
|
82370
82494
|
set srcAfter to count of messages of srcMailbox
|
|
82371
82495
|
if srcAfter is 0 then
|
|
82372
|
-
delete
|
|
82496
|
+
delete srcMailbox
|
|
82373
82497
|
return "ok${FIELD_SEP}" & srcCount
|
|
82374
82498
|
else
|
|
82375
82499
|
return "partial${FIELD_SEP}" & (srcCount - srcAfter) & "${FIELD_SEP}" & srcCount & "${FIELD_SEP}" & srcAfter
|
|
@@ -82866,14 +82990,16 @@ end tell`;
|
|
|
82866
82990
|
return accounts;
|
|
82867
82991
|
}
|
|
82868
82992
|
/**
|
|
82869
|
-
* Fetches mailbox
|
|
82993
|
+
* Fetches canonical mailbox paths for an account directly from Mail.app.
|
|
82870
82994
|
* Used internally by the cache; prefer getCachedMailboxNames().
|
|
82871
82995
|
*/
|
|
82872
82996
|
fetchMailboxNames(account) {
|
|
82873
82997
|
const body = `
|
|
82874
82998
|
set mbNames to {}
|
|
82875
82999
|
repeat with mb in ${isLocalStoreLabel(account) ? "_mbs" : "mailboxes"}
|
|
82876
|
-
set
|
|
83000
|
+
set mbPath to ""
|
|
83001
|
+
${mailboxPathFragment("mb", "mbPath")}
|
|
83002
|
+
set end of mbNames to mbPath
|
|
82877
83003
|
end repeat
|
|
82878
83004
|
return mbNames
|
|
82879
83005
|
`;
|
|
@@ -82977,10 +83103,22 @@ end tell`;
|
|
|
82977
83103
|
if (a.markFlagged) actionStmts.push(` set mark flagged of newRule to true`);
|
|
82978
83104
|
if (a.delete) actionStmts.push(` set delete message of newRule to true`);
|
|
82979
83105
|
if (a.moveTo) {
|
|
82980
|
-
const safeMbox = escapeForAppleScript(a.moveTo);
|
|
82981
|
-
const mboxRef = a.moveToAccount ? `mailbox "${safeMbox}" of account "${escapeForAppleScript(a.moveToAccount)}"` : `mailbox "${safeMbox}"`;
|
|
82982
83106
|
actionStmts.push(` set should move message of newRule to true`);
|
|
82983
|
-
|
|
83107
|
+
if (a.moveToAccount) {
|
|
83108
|
+
const resolvedMbox = this.resolveMailbox(a.moveTo, a.moveToAccount);
|
|
83109
|
+
const safeAccount = escapeForAppleScript(a.moveToAccount);
|
|
83110
|
+
actionStmts.push(
|
|
83111
|
+
` ${mailboxLookupFragment(`mailboxes of account "${safeAccount}"`, resolvedMbox, "_ruleDestMb")}`
|
|
83112
|
+
);
|
|
83113
|
+
actionStmts.push(
|
|
83114
|
+
` if _ruleDestMb is missing value then error "Mailbox \\"${escapeForAppleScript(resolvedMbox)}\\" not found in account \\"${safeAccount}\\""`
|
|
83115
|
+
);
|
|
83116
|
+
actionStmts.push(` set move message of newRule to _ruleDestMb`);
|
|
83117
|
+
} else {
|
|
83118
|
+
actionStmts.push(
|
|
83119
|
+
` set move message of newRule to mailbox "${escapeForAppleScript(a.moveTo)}"`
|
|
83120
|
+
);
|
|
83121
|
+
}
|
|
82984
83122
|
}
|
|
82985
83123
|
if (!actionStmts.length) {
|
|
82986
83124
|
return { success: false, error: "A rule needs at least one action." };
|
|
@@ -86996,7 +87134,7 @@ function appendLocalStoreRows(rows, failedAccounts) {
|
|
|
86996
87134
|
registerTool(
|
|
86997
87135
|
"list-mailboxes",
|
|
86998
87136
|
{
|
|
86999
|
-
description: 'Use when: discovering the mailbox/folder
|
|
87137
|
+
description: 'Use when: discovering the mailbox/folder paths (and unread/message counts) available in an account, e.g. before moving messages or searching a specific mailbox.\nReturns: each mailbox\'s canonical account-relative path in `name`, unread/message counts, and a total count. Use the full path for nested mailboxes (for example `Archive/Inbox`); a top-level `Inbox` remains `Inbox`. A source that could not be read is NAMED \u2014 the result carries `partial: true` + `failedAccounts` and the list is a floor, not the complete set \u2014 and a listing Mail refused outright (e.g. an account that does not exist) returns an ERROR naming the accounts that do exist, never an empty list.\nDo not use when: you want the messages inside a mailbox (use list-messages or search-messages) or the list of accounts (use list-accounts).\nNote: Mail\'s local "On My Mac" mailboxes are not part of any account, so they are reported under the synthetic account label "On My Mac" \u2014 an unscoped call includes them, and `account: "On My Mac"` lists only them. They will not appear in list-accounts, which reports real accounts only.',
|
|
87000
87138
|
inputSchema: {
|
|
87001
87139
|
account: external_exports.string().optional().describe("Account to list mailboxes from")
|
|
87002
87140
|
},
|
|
@@ -60,8 +60,8 @@ the MCP runtime from your Homebrew/dev Node, which can keep updating freely.
|
|
|
60
60
|
curl -O https://nodejs.org/dist/$VER/node-$VER-$ARCH.tar.gz
|
|
61
61
|
curl -O https://nodejs.org/dist/$VER/SHASUMS256.txt
|
|
62
62
|
grep " node-$VER-$ARCH.tar.gz$" SHASUMS256.txt | shasum -a 256 -c - # must print OK
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
mkdir -p node-current
|
|
64
|
+
tar -xzf node-$VER-$ARCH.tar.gz --strip-components=1 -C node-current
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
2. Confirm it's Developer-ID signed:
|
|
@@ -98,12 +98,36 @@ the MCP runtime from your Homebrew/dev Node, which can keep updating freely.
|
|
|
98
98
|
- *Automation*: the first time the server drives an app you'll get a one-time
|
|
99
99
|
`"node" wants to control "<App>"` prompt — click **Allow**.
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
⚠️ **A TCC grant is keyed to the binary's resolved *path*, not only to its
|
|
102
|
+
signature.** Because the steps above unpack Node into a fixed directory
|
|
103
|
+
(`~/mcp-runtime/node-current`) rather than a versioned one, that path never
|
|
104
|
+
moves and the grants survive Node updates — see "Updating" below. If you
|
|
105
|
+
instead point `node-current` at a `node-vX.Y.Z-…` directory, every update
|
|
106
|
+
changes the resolved path, presents a brand-new ungranted identity, and you
|
|
107
|
+
will be re-prompted for all of it.
|
|
108
|
+
|
|
109
|
+
You can delete any stale "node" rows from the Full Disk Access list.
|
|
104
110
|
|
|
105
111
|
### Updating the dedicated Node later
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
Replace the **contents** of the same directory — do not create a new one and do
|
|
114
|
+
not repoint anything:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
VER=v24.19.0 ARCH=darwin-arm64
|
|
118
|
+
cd ~/mcp-runtime
|
|
119
|
+
curl -O https://nodejs.org/dist/$VER/node-$VER-$ARCH.tar.gz
|
|
120
|
+
curl -O https://nodejs.org/dist/$VER/SHASUMS256.txt
|
|
121
|
+
grep " node-$VER-$ARCH.tar.gz$" SHASUMS256.txt | shasum -a 256 -c - # must print OK
|
|
122
|
+
rm -rf node-current && mkdir -p node-current
|
|
123
|
+
tar -xzf node-$VER-$ARCH.tar.gz --strip-components=1 -C node-current
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The path is unchanged and the official builds are Developer-ID signed with a
|
|
127
|
+
requirement that pins identifier + Team ID (no cdhash), so **both** halves of
|
|
128
|
+
the grant still match: existing grants carry over with no re-approval.
|
|
129
|
+
|
|
130
|
+
⚠️ **Restart your MCP client afterwards.** Replacing the binary unlinks the one
|
|
131
|
+
any already-running server is executing; macOS then cannot validate that path,
|
|
132
|
+
so those processes fail with *"Permission denied"* until they are restarted.
|
|
133
|
+
Nothing is wrong with your grants — a freshly launched server works fine.
|
package/package.json
CHANGED