apple-tools-mcp 3.0.0 → 3.0.2
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 +209 -566
- package/lib/calendarWrite.js +83 -27
- package/lib/mailWrite.js +113 -25
- package/package.json +1 -1
package/lib/calendarWrite.js
CHANGED
|
@@ -44,6 +44,17 @@ export const RECURRENCE_FREQUENCIES = ["daily", "weekly", "monthly", "yearly"];
|
|
|
44
44
|
export const RECURRENCE_DAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
|
|
45
45
|
export const RSVP_RESPONSES = ["accept", "decline", "tentative"];
|
|
46
46
|
|
|
47
|
+
// Regression (2026-09-23): calendar_edit's classic-AppleScript fallback and
|
|
48
|
+
// calendar_rsvp both ran atmFindEvent (see findEventHandler below) under a
|
|
49
|
+
// flat 60000ms runAppleScript timeout -- exactly matching the MCP client
|
|
50
|
+
// SDK's own default 60s CallTool timeout, so the client could give up with
|
|
51
|
+
// a raw "Request timed out" before the server ever got to answer, even
|
|
52
|
+
// though the underlying operation kept running and generally succeeded
|
|
53
|
+
// (same class of bug already fixed for mail_send/reply/forward). Trimmed
|
|
54
|
+
// under that ceiling to leave real margin, including for atmFindEvent's own
|
|
55
|
+
// retry loop (~8s worst case).
|
|
56
|
+
export const CALENDAR_FIND_TIMEOUT_MS = 45000;
|
|
57
|
+
|
|
47
58
|
const RSVP_STATUS = {
|
|
48
59
|
accept: "accepted",
|
|
49
60
|
decline: "declined",
|
|
@@ -163,16 +174,36 @@ export function defaultEndParts(start, allDay) {
|
|
|
163
174
|
};
|
|
164
175
|
}
|
|
165
176
|
|
|
177
|
+
/**
|
|
178
|
+
* calendar_edit / calendar_rsvp's classic-AppleScript fallback both resolve
|
|
179
|
+
* an event by uid through this handler. Regression (2026-09-23): RSVPing to
|
|
180
|
+
* an event created moments earlier through the daemon's separate EventKit
|
|
181
|
+
* session ("no event with that id was found") failed on the first lookup
|
|
182
|
+
* even though the event genuinely existed -- Calendar.app's classic
|
|
183
|
+
* AppleScript object model can lag behind a write made through EventKit.
|
|
184
|
+
* An EventKit-based fix isn't available here: writeOnly EventKit can only
|
|
185
|
+
* re-query an event via the exact in-memory EKEvent object from the
|
|
186
|
+
* session that created it (see describeEventKitWriteFailure below), which
|
|
187
|
+
* doesn't exist for a real invitation synced in from elsewhere. Retrying
|
|
188
|
+
* the classic lookup a few times gives Calendar.app's own model a chance
|
|
189
|
+
* to catch up, well inside callers' existing 60s budget.
|
|
190
|
+
*/
|
|
166
191
|
function findEventHandler() {
|
|
167
192
|
return `on atmFindEvent(theUid)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
set attemptsLeft to 5
|
|
194
|
+
repeat
|
|
195
|
+
tell application "Calendar"
|
|
196
|
+
repeat with cal in calendars
|
|
197
|
+
try
|
|
198
|
+
set hits to (every event of cal whose uid is theUid)
|
|
199
|
+
if (count of hits) > 0 then return item 1 of hits
|
|
200
|
+
end try
|
|
201
|
+
end repeat
|
|
202
|
+
end tell
|
|
203
|
+
set attemptsLeft to attemptsLeft - 1
|
|
204
|
+
if attemptsLeft is 0 then exit repeat
|
|
205
|
+
delay 2
|
|
206
|
+
end repeat
|
|
176
207
|
error "EVENT_NOT_FOUND"
|
|
177
208
|
end atmFindEvent`;
|
|
178
209
|
}
|
|
@@ -503,11 +534,19 @@ function addRow(c) {
|
|
|
503
534
|
var local = type === 0 ? "yes" : "no";
|
|
504
535
|
rows.push(name + "<<>>" + writable + "<<>>" + local + "<<>>" + type + "<<>>" + srcName);
|
|
505
536
|
}
|
|
537
|
+
var listError = "";
|
|
506
538
|
try {
|
|
507
539
|
var cals = store.calendarsForEntityType($.EKEntityTypeEvent);
|
|
508
540
|
for (var i = 0; i < cals.count; i++) addRow(cals.objectAtIndex(i));
|
|
509
|
-
} catch (e) {
|
|
510
|
-
|
|
541
|
+
} catch (e) {
|
|
542
|
+
listError = String(e);
|
|
543
|
+
}
|
|
544
|
+
try { addRow(store.defaultCalendarForNewEvents); } catch (e) {
|
|
545
|
+
if (!listError) listError = String(e);
|
|
546
|
+
}
|
|
547
|
+
if (rows.length === 0 && listError) {
|
|
548
|
+
throw new Error("EVENTKIT_LIST_FAILED: " + listError);
|
|
549
|
+
}
|
|
511
550
|
rows.join("|||");`;
|
|
512
551
|
}
|
|
513
552
|
|
|
@@ -659,26 +698,37 @@ set AppleScript's text item delimiters to "|||"
|
|
|
659
698
|
return outputList as string`;
|
|
660
699
|
|
|
661
700
|
const result = runAppleScript(script, { timeout: 30000, appName: "Calendar" });
|
|
662
|
-
if (!result.ok) {
|
|
663
|
-
return { ok: false, message: failure(action, "list calendars", result) };
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
let calendars = result.output
|
|
667
|
-
.split("|||")
|
|
668
|
-
.map((entry) => entry.trim())
|
|
669
|
-
.filter((entry) => entry.length > 0)
|
|
670
|
-
.map((entry) => {
|
|
671
|
-
const [name, writable] = entry.split("<<>>");
|
|
672
|
-
return { name: name || "", writable: writable !== "no" };
|
|
673
|
-
});
|
|
674
701
|
|
|
702
|
+
// EventKit does not require Calendar.app to be running, so a real EventKit
|
|
703
|
+
// list can stand in when the classic AppleScript path fails (app closed).
|
|
704
|
+
// An empty EventKit string is not that list: the JXA helper used to swallow
|
|
705
|
+
// every error and still exit 0, which hid TCC / timeout / not-running.
|
|
675
706
|
const ekList = runAppleScript(buildEventKitListCalendarsScript(), {
|
|
676
707
|
timeout: 15000,
|
|
677
708
|
appName: "Calendar",
|
|
678
709
|
language: "JavaScript"
|
|
679
710
|
});
|
|
680
|
-
|
|
681
|
-
|
|
711
|
+
const ekCalendars = ekList.ok ? parseEventKitCalendarList(ekList.output) : [];
|
|
712
|
+
|
|
713
|
+
if (!result.ok && ekCalendars.length === 0) {
|
|
714
|
+
return { ok: false, message: failure(action, "list calendars", result) };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let calendars = result.ok
|
|
718
|
+
? result.output
|
|
719
|
+
.split("|||")
|
|
720
|
+
.map((entry) => entry.trim())
|
|
721
|
+
.filter((entry) => entry.length > 0)
|
|
722
|
+
.map((entry) => {
|
|
723
|
+
const [name, writable] = entry.split("<<>>");
|
|
724
|
+
return { name: name || "", writable: writable !== "no" };
|
|
725
|
+
})
|
|
726
|
+
: [];
|
|
727
|
+
|
|
728
|
+
if (ekCalendars.length > 0) {
|
|
729
|
+
calendars = result.ok
|
|
730
|
+
? mergeCalendarSources(calendars, ekCalendars)
|
|
731
|
+
: ekCalendars;
|
|
682
732
|
}
|
|
683
733
|
|
|
684
734
|
if (calendars.length === 0) {
|
|
@@ -1115,7 +1165,7 @@ export function calendarEdit(args = {}) {
|
|
|
1115
1165
|
return { ok: false, message: `${action} refused: ${e.message}` };
|
|
1116
1166
|
}
|
|
1117
1167
|
|
|
1118
|
-
const result = runAppleScript(script, { timeout:
|
|
1168
|
+
const result = runAppleScript(script, { timeout: CALENDAR_FIND_TIMEOUT_MS, appName: "Calendar" });
|
|
1119
1169
|
if (!result.ok) return { ok: false, message: failure(action, summary, result, [updates.description || ""]) };
|
|
1120
1170
|
|
|
1121
1171
|
return {
|
|
@@ -1284,10 +1334,16 @@ export function calendarRemove(args = {}) {
|
|
|
1284
1334
|
}
|
|
1285
1335
|
|
|
1286
1336
|
export function buildRsvpScript({ eventId, status, attendeeEmail }) {
|
|
1337
|
+
// Calendar.app's AppleScript dictionary defines `participation status` as
|
|
1338
|
+
// one of unknown/accepted/declined/tentative (confirmed via `sdef`/aete
|
|
1339
|
+
// dump) -- there is no "needs action" term, so that bare two-word phrase
|
|
1340
|
+
// was a syntax error (AppleScript parsed "needs" as an identifier and
|
|
1341
|
+
// choked on "action" where it expected "then"). `unknown` is the correct
|
|
1342
|
+
// term for "no answer yet".
|
|
1287
1343
|
const match = attendeeEmail
|
|
1288
1344
|
? ` if (email of att) is ${asString(attendeeEmail)} then set theAttendee to att`
|
|
1289
1345
|
: ` try
|
|
1290
|
-
if (participation status of att) is
|
|
1346
|
+
if (participation status of att) is unknown then set theAttendee to att
|
|
1291
1347
|
end try`;
|
|
1292
1348
|
|
|
1293
1349
|
return `${findEventHandler()}
|
|
@@ -1344,7 +1400,7 @@ export function calendarRsvp(args = {}) {
|
|
|
1344
1400
|
|
|
1345
1401
|
const result = runAppleScript(
|
|
1346
1402
|
buildRsvpScript({ eventId, status: RSVP_STATUS[responseRaw], attendeeEmail }),
|
|
1347
|
-
{ timeout:
|
|
1403
|
+
{ timeout: CALENDAR_FIND_TIMEOUT_MS, appName: "Calendar" }
|
|
1348
1404
|
);
|
|
1349
1405
|
|
|
1350
1406
|
if (!result.ok) {
|
package/lib/mailWrite.js
CHANGED
|
@@ -182,6 +182,27 @@ export const MAIL_SE_PROBE_TIMEOUT_MS = 4000;
|
|
|
182
182
|
export const SYSTEM_EVENTS_AUTOMATION_PROBE_TIMEOUT_MS = 30000;
|
|
183
183
|
/** Compose osascript budget (make + keystroke + send). Far below the ~60s SE wedge. */
|
|
184
184
|
export const MAIL_COMPOSE_TIMEOUT_MS = 25000;
|
|
185
|
+
// Regression (2026-09-23): atmFindMessage's `messages of <box> whose message
|
|
186
|
+
// id is X` scan is genuinely slow on a large mailbox (observed 30-38s on a
|
|
187
|
+
// ~68,500-message account, even matching in the first/fastest box checked --
|
|
188
|
+
// this is a known AppleScript `whose`-filter characteristic, not specific to
|
|
189
|
+
// one mailbox choice). mail_mark / mail_archive / mail_trash had no explicit
|
|
190
|
+
// timeout at all (defaulting to the shared 30s DEFAULT_SCRIPT_TIMEOUT_MS),
|
|
191
|
+
// so they timed out before that scan could finish on this account. Widened
|
|
192
|
+
// with real margin over the observed worst case, while staying under the
|
|
193
|
+
// MCP client SDK's 60s default CallTool timeout (mail_send's fix comment
|
|
194
|
+
// above SENT_VERIFY_ATTEMPTS has the full story on why that ceiling matters).
|
|
195
|
+
export const MAIL_FIND_TIMEOUT_MS = 50000;
|
|
196
|
+
// Whole tool call must answer before the MCP client's default 60s CallTool
|
|
197
|
+
// timeout. 55s leaves the process time to format that answer.
|
|
198
|
+
export const MCP_CALL_DEADLINE_MS = 55000;
|
|
199
|
+
// mail_reply / mail_forward run atmFindMessage and then the send inside one
|
|
200
|
+
// osascript. The find alone has been observed at 30-38s, so a combined
|
|
201
|
+
// budget at that observed ceiling kills the script after the lookup and
|
|
202
|
+
// before send. Match the find-only budget (50s): a 38s lookup still has
|
|
203
|
+
// ~12s left for send. Verification after the script returns is capped to
|
|
204
|
+
// whatever remains under MCP_CALL_DEADLINE_MS, not a second full window.
|
|
205
|
+
export const MAIL_FIND_AND_SEND_TIMEOUT_MS = MAIL_FIND_TIMEOUT_MS;
|
|
185
206
|
export const MAIL_SE_APPLEEVENT_TIMEOUT_SEC = 8;
|
|
186
207
|
export const MAIL_APPLEEVENT_TIMEOUT_SEC = 15;
|
|
187
208
|
export const MAIL_SE_PROBE_APPLEEVENT_TIMEOUT_SEC = 3;
|
|
@@ -504,17 +525,27 @@ on atmTopElemBits(atmEl)
|
|
|
504
525
|
return atmBits
|
|
505
526
|
end atmTopElemBits
|
|
506
527
|
|
|
528
|
+
-- Mail titles a compose window with the full subject, while the marker is
|
|
529
|
+
-- cut to ${MAIL_COMPOSE_TITLE_MARKER_MAX} characters for long subjects. A marker that length is a
|
|
530
|
+
-- prefix; a shorter one is the whole subject and must match exactly.
|
|
531
|
+
on atmTitleMatches(atmTitle, atmMarker)
|
|
532
|
+
if atmTitle is atmMarker then return true
|
|
533
|
+
if (length of atmMarker) >= ${MAIL_COMPOSE_TITLE_MARKER_MAX} and atmTitle starts with atmMarker then return true
|
|
534
|
+
return false
|
|
535
|
+
end atmTitleMatches
|
|
536
|
+
|
|
507
537
|
on atmTopElemExactTitle(atmEl, atmMarker)
|
|
508
538
|
try
|
|
509
|
-
if (name of atmEl as string
|
|
539
|
+
if my atmTitleMatches(name of atmEl as string, atmMarker) then return true
|
|
510
540
|
end try
|
|
511
541
|
try
|
|
512
|
-
if (title of atmEl as string
|
|
542
|
+
if my atmTitleMatches(title of atmEl as string, atmMarker) then return true
|
|
513
543
|
end try
|
|
514
544
|
try
|
|
515
545
|
tell application "System Events"
|
|
516
|
-
|
|
546
|
+
set atmAxTitle to (value of attribute "AXTitle" of atmEl as string)
|
|
517
547
|
end tell
|
|
548
|
+
if my atmTitleMatches(atmAxTitle, atmMarker) then return true
|
|
518
549
|
end try
|
|
519
550
|
return false
|
|
520
551
|
end atmTopElemExactTitle
|
|
@@ -603,7 +634,7 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
|
603
634
|
set atmWin to missing value
|
|
604
635
|
repeat with atmCand in windows
|
|
605
636
|
try
|
|
606
|
-
if (name of atmCand as string
|
|
637
|
+
if my atmTitleMatches(name of atmCand as string, titleMarker) then
|
|
607
638
|
set atmWin to atmCand
|
|
608
639
|
exit repeat
|
|
609
640
|
end if
|
|
@@ -840,11 +871,41 @@ export const SENT_VERIFY_NOT_FOUND = "NOT_FOUND";
|
|
|
840
871
|
/** Older hang-recovery scripts returned FOUND to mean Sent. Still accepted. */
|
|
841
872
|
export const SENT_VERIFY_FOUND = "FOUND";
|
|
842
873
|
export const SENT_VERIFY_TIMEOUT_MS = 15000;
|
|
843
|
-
|
|
844
|
-
export const
|
|
874
|
+
/** Skip a poll that could not return before the call deadline. */
|
|
875
|
+
export const SENT_VERIFY_MIN_POLL_MS = 1000;
|
|
876
|
+
// Regression (2026-09-23): 6 consecutive real sends on an iCloud/IMAP
|
|
877
|
+
// account all delivered successfully, but each took a different, unbounded
|
|
878
|
+
// amount of time before Mail made the message queryable in Sent via
|
|
879
|
+
// AppleScript (observed: 20s, 20s, 30s, 65s, 47s, ~64s), even though Mail's
|
|
880
|
+
// `send` call itself returned in a few seconds. The old budget (3 attempts
|
|
881
|
+
// x 400ms = ~1.2s) gave up long before that and reported a false-negative
|
|
882
|
+
// failure on messages that really were delivered.
|
|
883
|
+
//
|
|
884
|
+
// A larger budget alone cannot fully fix this: the MCP client SDK's own
|
|
885
|
+
// default CallTool timeout is 60s (McpError -32001 "Request timed out"),
|
|
886
|
+
// confirmed by testing an attempts=30/retryMs=4000 (~120s worst case)
|
|
887
|
+
// budget against a real client -- the client gave up and errored while the
|
|
888
|
+
// server was still polling and about to find the (already-delivered)
|
|
889
|
+
// message. Any purely-synchronous verification longer than that ceiling is
|
|
890
|
+
// self-defeating: it cannot outrun the client's own timeout, and going past
|
|
891
|
+
// it only means the client sees a raw protocol timeout instead of a useful
|
|
892
|
+
// answer. So the budget below stays safely under that ceiling (well under
|
|
893
|
+
// 60s total including the initial send), and a miss within budget is
|
|
894
|
+
// reported as *unconfirmed*, not failed -- honesty over false confidence,
|
|
895
|
+
// since 9/9 real sends observed here eventually succeeded.
|
|
896
|
+
//
|
|
897
|
+
// Attempt count and sleep are a ceiling, not a guaranteed wait. Each poll
|
|
898
|
+
// also runs under SENT_VERIFY_TIMEOUT_MS (15s), so 6 x 15s plus the sleeps
|
|
899
|
+
// can blow past the 60s client timeout if a poll hangs. verifyQueuedMessage
|
|
900
|
+
// stops at MCP_CALL_DEADLINE_MS and shrinks each poll to the time left.
|
|
901
|
+
export const SENT_VERIFY_ATTEMPTS = 6;
|
|
902
|
+
export const SENT_VERIFY_RETRY_MS = 2000;
|
|
845
903
|
|
|
846
904
|
export const MAIL_VERIFY_MISS_GUIDANCE =
|
|
847
|
-
"Mail reported the send call succeeded,
|
|
905
|
+
"Mail reported the send call succeeded, and this account's Sent folder has been observed to take up to a minute or more to " +
|
|
906
|
+
"reflect a new message (iCloud/IMAP sync lag), longer than it is safe to block a single tool call for. This is not a known " +
|
|
907
|
+
"failure -- treat it as unconfirmed. Check Sent (and Outbox) before deciding whether to retry; retrying a message that " +
|
|
908
|
+
"already landed sends a second copy.";
|
|
848
909
|
|
|
849
910
|
function defaultSentVerifySleep(ms) {
|
|
850
911
|
if (!ms || ms <= 0) return;
|
|
@@ -853,11 +914,13 @@ function defaultSentVerifySleep(ms) {
|
|
|
853
914
|
|
|
854
915
|
/** Test hook: skip the retry delay without changing attempt count. */
|
|
855
916
|
export const sentVerifyClock = {
|
|
856
|
-
sleep: defaultSentVerifySleep
|
|
917
|
+
sleep: defaultSentVerifySleep,
|
|
918
|
+
now: () => Date.now()
|
|
857
919
|
};
|
|
858
920
|
|
|
859
921
|
export function resetSentVerifyClock() {
|
|
860
922
|
sentVerifyClock.sleep = defaultSentVerifySleep;
|
|
923
|
+
sentVerifyClock.now = () => Date.now();
|
|
861
924
|
}
|
|
862
925
|
|
|
863
926
|
/**
|
|
@@ -1158,10 +1221,12 @@ function pickSentVerifyScript({
|
|
|
1158
1221
|
* Compose matches require To + subject (Message-ID when available) — never
|
|
1159
1222
|
* subject alone. Returns a hit object or null. Verify failure is not success.
|
|
1160
1223
|
*/
|
|
1161
|
-
export function recoverIfInSent(match = {}) {
|
|
1224
|
+
export function recoverIfInSent(match = {}, { timeout = SENT_VERIFY_TIMEOUT_MS } = {}) {
|
|
1162
1225
|
const script = pickSentVerifyScript(match);
|
|
1163
1226
|
if (!script) return null;
|
|
1164
|
-
const
|
|
1227
|
+
const pollTimeout = Math.min(SENT_VERIFY_TIMEOUT_MS, Math.max(0, timeout));
|
|
1228
|
+
if (pollTimeout < SENT_VERIFY_MIN_POLL_MS) return null;
|
|
1229
|
+
const result = runAppleScript(script, { timeout: pollTimeout, appName: "Mail" });
|
|
1165
1230
|
if (!result.ok) return null;
|
|
1166
1231
|
return parseSentVerifyOutput(result.output);
|
|
1167
1232
|
}
|
|
@@ -1169,13 +1234,25 @@ export function recoverIfInSent(match = {}) {
|
|
|
1169
1234
|
/**
|
|
1170
1235
|
* Poll Sent/Outbox after a send that returned without throw.
|
|
1171
1236
|
*/
|
|
1172
|
-
export function verifyQueuedMessage(match = {}, {
|
|
1237
|
+
export function verifyQueuedMessage(match = {}, {
|
|
1238
|
+
attempts = SENT_VERIFY_ATTEMPTS,
|
|
1239
|
+
retryMs = SENT_VERIFY_RETRY_MS,
|
|
1240
|
+
deadlineMs = null
|
|
1241
|
+
} = {}) {
|
|
1242
|
+
const deadline = Number.isFinite(deadlineMs)
|
|
1243
|
+
? deadlineMs
|
|
1244
|
+
: sentVerifyClock.now() + MCP_CALL_DEADLINE_MS;
|
|
1173
1245
|
let last = null;
|
|
1174
1246
|
const n = Math.max(1, attempts);
|
|
1175
1247
|
for (let i = 0; i < n; i++) {
|
|
1176
|
-
|
|
1248
|
+
const remaining = deadline - sentVerifyClock.now();
|
|
1249
|
+
if (remaining < SENT_VERIFY_MIN_POLL_MS) break;
|
|
1250
|
+
last = recoverIfInSent(match, { timeout: Math.min(SENT_VERIFY_TIMEOUT_MS, remaining) });
|
|
1177
1251
|
if (last) return last;
|
|
1178
|
-
if (i
|
|
1252
|
+
if (i >= n - 1) break;
|
|
1253
|
+
const sleepFor = Math.min(retryMs, deadline - sentVerifyClock.now() - SENT_VERIFY_MIN_POLL_MS);
|
|
1254
|
+
if (sleepFor <= 0) break;
|
|
1255
|
+
sentVerifyClock.sleep(sleepFor);
|
|
1179
1256
|
}
|
|
1180
1257
|
return last;
|
|
1181
1258
|
}
|
|
@@ -1282,7 +1359,7 @@ function sendVerifiedSuccess({ action, successSummary, details, verified, recove
|
|
|
1282
1359
|
}
|
|
1283
1360
|
|
|
1284
1361
|
function verifyMissMessage(action, summary) {
|
|
1285
|
-
return `${action}
|
|
1362
|
+
return `${action}: unconfirmed — attempted to ${summary}. ${MAIL_VERIFY_MISS_GUIDANCE}`;
|
|
1286
1363
|
}
|
|
1287
1364
|
|
|
1288
1365
|
function finalizeMailWrite({
|
|
@@ -1297,15 +1374,20 @@ function finalizeMailWrite({
|
|
|
1297
1374
|
to = null,
|
|
1298
1375
|
messageId = null,
|
|
1299
1376
|
successSummary,
|
|
1300
|
-
details
|
|
1377
|
+
details,
|
|
1378
|
+
deadlineMs = null
|
|
1301
1379
|
}) {
|
|
1302
1380
|
const match = { inReplyTo, subject, forwardTo, to, messageId };
|
|
1381
|
+
const deadline = Number.isFinite(deadlineMs)
|
|
1382
|
+
? deadlineMs
|
|
1383
|
+
: sentVerifyClock.now() + MCP_CALL_DEADLINE_MS;
|
|
1384
|
+
const remaining = () => deadline - sentVerifyClock.now();
|
|
1303
1385
|
|
|
1304
1386
|
if (result.ok) {
|
|
1305
1387
|
if (!sendNow) {
|
|
1306
1388
|
return { ok: true, delivered: false, mailbox: null, message: writeSuccessMessage(action, successSummary, details) };
|
|
1307
1389
|
}
|
|
1308
|
-
const verified = verifyQueuedMessage(match);
|
|
1390
|
+
const verified = verifyQueuedMessage(match, { deadlineMs: deadline });
|
|
1309
1391
|
if (!verified) {
|
|
1310
1392
|
return { ok: false, delivered: false, mailbox: null, message: verifyMissMessage(action, summary) };
|
|
1311
1393
|
}
|
|
@@ -1313,7 +1395,7 @@ function finalizeMailWrite({
|
|
|
1313
1395
|
}
|
|
1314
1396
|
|
|
1315
1397
|
if (sendNow && isMailSendTimeout(result)) {
|
|
1316
|
-
const recovered = recoverIfInSent(match);
|
|
1398
|
+
const recovered = recoverIfInSent(match, { timeout: remaining() });
|
|
1317
1399
|
if (recovered) {
|
|
1318
1400
|
return sendVerifiedSuccess({ action, successSummary, details, verified: recovered, recovered: true });
|
|
1319
1401
|
}
|
|
@@ -1397,6 +1479,7 @@ export function mailCompose(args = {}, { draft = false, indexerMode = isIndexerM
|
|
|
1397
1479
|
html: bodyFormat === "html"
|
|
1398
1480
|
});
|
|
1399
1481
|
|
|
1482
|
+
const deadlineMs = sentVerifyClock.now() + MCP_CALL_DEADLINE_MS;
|
|
1400
1483
|
const result = runAppleScript(script, { timeout: MAIL_COMPOSE_TIMEOUT_MS, appName: "Mail" });
|
|
1401
1484
|
if (
|
|
1402
1485
|
!result.ok &&
|
|
@@ -1420,7 +1503,8 @@ export function mailCompose(args = {}, { draft = false, indexerMode = isIndexerM
|
|
|
1420
1503
|
cc: cc.addresses.join(", ") || undefined,
|
|
1421
1504
|
bcc: bcc.addresses.length ? `${bcc.addresses.length} recipient(s)` : undefined,
|
|
1422
1505
|
subject: truncate(subject.text, 150)
|
|
1423
|
-
}
|
|
1506
|
+
},
|
|
1507
|
+
deadlineMs
|
|
1424
1508
|
});
|
|
1425
1509
|
}
|
|
1426
1510
|
|
|
@@ -1475,9 +1559,10 @@ export function mailReply(args = {}) {
|
|
|
1475
1559
|
});
|
|
1476
1560
|
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1477
1561
|
|
|
1562
|
+
const deadlineMs = sentVerifyClock.now() + MCP_CALL_DEADLINE_MS;
|
|
1478
1563
|
const result = runAppleScript(
|
|
1479
1564
|
buildReplyScript({ messageId: resolved.messageId, body: body.text, replyAll, sendNow }),
|
|
1480
|
-
{ timeout:
|
|
1565
|
+
{ timeout: MAIL_FIND_AND_SEND_TIMEOUT_MS, appName: "Mail" }
|
|
1481
1566
|
);
|
|
1482
1567
|
return finalizeMailWrite({
|
|
1483
1568
|
action,
|
|
@@ -1490,7 +1575,8 @@ export function mailReply(args = {}) {
|
|
|
1490
1575
|
details: {
|
|
1491
1576
|
message_id: resolved.messageId,
|
|
1492
1577
|
reply_all: String(replyAll)
|
|
1493
|
-
}
|
|
1578
|
+
},
|
|
1579
|
+
deadlineMs
|
|
1494
1580
|
});
|
|
1495
1581
|
}
|
|
1496
1582
|
|
|
@@ -1538,9 +1624,10 @@ export function mailForward(args = {}) {
|
|
|
1538
1624
|
});
|
|
1539
1625
|
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1540
1626
|
|
|
1627
|
+
const deadlineMs = sentVerifyClock.now() + MCP_CALL_DEADLINE_MS;
|
|
1541
1628
|
const result = runAppleScript(
|
|
1542
1629
|
buildForwardScript({ messageId: resolved.messageId, to: to.addresses, body: body.text, sendNow }),
|
|
1543
|
-
{ timeout:
|
|
1630
|
+
{ timeout: MAIL_FIND_AND_SEND_TIMEOUT_MS, appName: "Mail" }
|
|
1544
1631
|
);
|
|
1545
1632
|
return finalizeMailWrite({
|
|
1546
1633
|
action,
|
|
@@ -1554,7 +1641,8 @@ export function mailForward(args = {}) {
|
|
|
1554
1641
|
details: {
|
|
1555
1642
|
message_id: resolved.messageId,
|
|
1556
1643
|
to: to.addresses.join(", ")
|
|
1557
|
-
}
|
|
1644
|
+
},
|
|
1645
|
+
deadlineMs
|
|
1558
1646
|
});
|
|
1559
1647
|
}
|
|
1560
1648
|
|
|
@@ -1586,7 +1674,7 @@ export function mailMark(args = {}) {
|
|
|
1586
1674
|
});
|
|
1587
1675
|
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1588
1676
|
|
|
1589
|
-
const result = runAppleScript(buildMarkScript({ messageId: resolved.messageId, read: status === "read" }), { appName: "Mail" });
|
|
1677
|
+
const result = runAppleScript(buildMarkScript({ messageId: resolved.messageId, read: status === "read" }), { timeout: MAIL_FIND_TIMEOUT_MS, appName: "Mail" });
|
|
1590
1678
|
if (!result.ok) return { ok: false, message: failure(action, summary, result, []) };
|
|
1591
1679
|
|
|
1592
1680
|
return { ok: true, message: writeSuccessMessage(action, `marked as ${status}`, { message_id: resolved.messageId }) };
|
|
@@ -1637,7 +1725,7 @@ export function mailArchive(args = {}) {
|
|
|
1637
1725
|
mailboxNames: ["Archive", "All Mail", "Archived"],
|
|
1638
1726
|
allowDeleteFallback: false
|
|
1639
1727
|
}),
|
|
1640
|
-
{ appName: "Mail" }
|
|
1728
|
+
{ timeout: MAIL_FIND_TIMEOUT_MS, appName: "Mail" }
|
|
1641
1729
|
);
|
|
1642
1730
|
if (!result.ok) {
|
|
1643
1731
|
if (result.kind === "not_found" && String(result.error).includes("ARCHIVE_MAILBOX_NOT_FOUND")) {
|
|
@@ -1670,7 +1758,7 @@ export function mailTrash(args = {}) {
|
|
|
1670
1758
|
mailboxNames: ["Trash", "Deleted Messages", "Bin"],
|
|
1671
1759
|
allowDeleteFallback: true
|
|
1672
1760
|
}),
|
|
1673
|
-
{ appName: "Mail" }
|
|
1761
|
+
{ timeout: MAIL_FIND_TIMEOUT_MS, appName: "Mail" }
|
|
1674
1762
|
);
|
|
1675
1763
|
if (!result.ok) return { ok: false, message: failure(action, summary, result, []) };
|
|
1676
1764
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-tools-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "MCP server for semantic search and write actions across Apple Mail, Messages, Calendar, and Contacts. Speaks stdio (local) or bearer-token-authenticated Streamable HTTP (LAN / Tailscale).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|