apple-tools-mcp 2.0.6 → 2.0.7
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 +5 -2
- package/index.js +3 -5
- package/lib/appleScript.js +1 -1
- package/lib/calendarWrite.js +5 -4
- package/lib/contactsWrite.js +4 -3
- package/lib/mailWrite.js +257 -65
- package/lib/messagesWrite.js +3 -2
- package/lib/writeGuards.js +18 -0
- package/lib/writeTools.js +20 -3
- package/package.json +1 -1
- package/scripts/smoke-writes.js +6 -5
package/README.md
CHANGED
|
@@ -446,6 +446,7 @@ Two arguments are available on **every** write tool:
|
|
|
446
446
|
- **Deletes always need `confirm: true`** — `mail_trash`, `calendar_remove`, `contacts_remove`, and `contacts_edit` when it clears every email or phone. Without it the call returns a `CONFIRMATION REQUIRED` preview and changes nothing. There is no bulk delete tool and no silent mass delete: one id per call.
|
|
447
447
|
- **Multi-recipient sends always need `confirm: true`** — `mail_send` / `mail_forward` when `to` + `cc` + `bcc` total more than one address, `mail_reply` with `reply_all: true`, and `messages_send` to multiple handles or to a group chat. A single-recipient send runs on the first call.
|
|
448
448
|
- **`mail_draft` is exempt from the recipient rule** because a draft is never delivered.
|
|
449
|
+
- **`dry_run` and confirm-blocked calls are not deliveries.** Planned writes return `ok: false`, `planned: true`, `delivered: false`, and MCP sets `isError: true`, so a client that only checks `isError` cannot treat a preview as sent. The text is `DRY RUN` / `CONFIRMATION REQUIRED` and never `verified in Sent`. Re-run with `dry_run=false` (and `confirm=true` when required) to actually send.
|
|
449
450
|
- Responses name what happened (tool, ids, recipients, titles) and never echo message bodies — including on the error path.
|
|
450
451
|
- Writes never invent data. A missing or malformed `message_id`, `event_id`, `contact_id`, `chat_id`, or recipient is refused with a message saying so. Calendar times must be explicit local datetimes (`YYYY-MM-DD HH:MM`); natural language such as "next Tuesday" is rejected for writes.
|
|
451
452
|
|
|
@@ -467,11 +468,13 @@ Two arguments are available on **every** write tool:
|
|
|
467
468
|
|
|
468
469
|
Paste uses System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) in addition to Automation → Mail. That Accessibility deny is not a Mail Automation deny (`-1743` / `-10004`). Body focus uses `text area` / `scroll area` and `UI element whose role is "AXWebArea"` — never the System Events class `web area`, which does not compile on macOS 26.x (**-2741**, “Expected class name but found identifier”).
|
|
469
470
|
|
|
470
|
-
**
|
|
471
|
+
**mail_send success is Sent/Outbox verify (2.0.7).** AppleScript `send` returning without throw is not enough. After a real send the tool looks in **Sent** and **Outbox** and reports success only if the message is there. The success text names the delivery state (`mailbox: sent` + `delivery: sent`, or `mailbox: outbox` + `delivery: outbox` while still sending). If verify misses, the tool returns a failure (`isError`) — not success. Hang/timeout recover matches **To + subject** (and Message-ID when compose captured one). It never matches subject alone (short subjects like `test` are unsafe). `from` / account selection is out of scope; Mail's default From is used.
|
|
472
|
+
|
|
473
|
+
**Manual prove (2.0.6 on the Mac host):** `mail_send` a short plain message and a short `body_format: "html"` message whose body looks like ordinary paragraphs (for example `<p>Quick note</p>`), neither with a `Re:`/`Fwd:` subject. Inspect each Sent `.emlx`: the text/plain part must not prefix every body line with `>`, and any HTML alternative must not wrap the whole body in `<blockquote type="cite">`. Compose must succeed (no `-2753` / undefined `newMessage`, no **-2741** on body focus). That quote-prefix dual-host Sent prove is a separate bar from the 2.0.7 verify contract.
|
|
471
474
|
|
|
472
475
|
Emails are addressed by their RFC822 **Message-ID**. Pass `message_id`, or pass the `file_path` from `mail_search` / `mail_recent` and the server reads the Message-ID out of the `.emlx` headers for you. `mail_archive` moves the message to its account's Archive (or All Mail) mailbox; `mail_trash` moves it to that account's Trash.
|
|
473
476
|
|
|
474
|
-
**Send timeout vs TCC — check Sent before retrying.** `mail_send` / `mail_reply` / `mail_forward` can hang after Mail has already put the message in Sent. That hang is a **timeout** (`ETIMEDOUT` / `-1712` / AppleEvent timed out), not a TCC deny. The same hang-vs-TCC split applies to **find / reply / open before send** — an Allowed `ETIMEDOUT` there is never `MAIL_TCC_GUIDANCE`. Real Mail Automation denials report **`-1743`**, **`-10004`**, or “not authorized to send Apple events”. After a send hang the tool looks in Sent (and Outbox) and returns **success** if the message is there — never label a delivered send as TCC fail or timeout. Replies match `In-Reply-To` or an exact recent `Re:` + original subject (the fallback compose does not set reply headers). Forwards match the intended recipient plus an exact `Fwd:` subject or the original Message-ID in a forwarded body — not `In-Reply-To`, and not an unrelated `Fwd:` that only shares a To. Neither scan treats the original itself as this send. If Sent-verify misses, the error tells you it was a hang and to check Sent. **Clients must Sent-check before retrying a timed-out send**; retrying a message that already landed sends a second copy. This is not a silent TCC grant.
|
|
477
|
+
**Send timeout vs TCC — check Sent before retrying.** `mail_send` / `mail_reply` / `mail_forward` can hang after Mail has already put the message in Sent. That hang is a **timeout** (`ETIMEDOUT` / `-1712` / AppleEvent timed out), not a TCC deny. The same hang-vs-TCC split applies to **find / reply / open before send** — an Allowed `ETIMEDOUT` there is never `MAIL_TCC_GUIDANCE`. Real Mail Automation denials report **`-1743`**, **`-10004`**, or “not authorized to send Apple events”. After a send hang the tool looks in Sent (and Outbox) and returns **success** if the message is there — never label a delivered send as TCC fail or timeout. Replies match `In-Reply-To` or an exact recent `Re:` + original subject (the fallback compose does not set reply headers). Forwards match the intended recipient plus an exact `Fwd:` subject or the original Message-ID in a forwarded body — not `In-Reply-To`, and not an unrelated `Fwd:` that only shares a To. Compose (`mail_send`) matches the intended **To plus exact subject** (and Message-ID when available), never subject alone. Neither scan treats the original itself as this send. A real send that returns without throw is also verified the same way before success. If Sent-verify misses, the error tells you it was a hang (or a verify-miss after send returned) and to check Sent. **Clients must Sent-check before retrying a timed-out send**; retrying a message that already landed sends a second copy. This is not a silent TCC grant.
|
|
475
478
|
|
|
476
479
|
### Messages write tool
|
|
477
480
|
|
package/index.js
CHANGED
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
WRITE_TOOL_DEFINITIONS,
|
|
28
28
|
isWriteTool,
|
|
29
29
|
dispatchWriteTool,
|
|
30
|
-
executeWriteToolLocally
|
|
30
|
+
executeWriteToolLocally,
|
|
31
|
+
mcpWriteResult
|
|
31
32
|
} from "./lib/writeTools.js";
|
|
32
33
|
import { startWriteBridgeServer, defaultSocketPath } from "./lib/writeBridge.js";
|
|
33
34
|
import { closeEventKitSession, ensureEventKitSession } from "./lib/eventKitSession.js";
|
|
@@ -1373,10 +1374,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1373
1374
|
socketPath: WRITE_SOCKET_PATH,
|
|
1374
1375
|
log: (msg) => console.error(msg)
|
|
1375
1376
|
});
|
|
1376
|
-
return
|
|
1377
|
-
content: [{ type: "text", text: writeResult.message }],
|
|
1378
|
-
...(writeResult.ok === false ? { isError: true } : {})
|
|
1379
|
-
};
|
|
1377
|
+
return mcpWriteResult(writeResult);
|
|
1380
1378
|
}
|
|
1381
1379
|
|
|
1382
1380
|
switch (name) {
|
package/lib/appleScript.js
CHANGED
|
@@ -353,7 +353,7 @@ export const MAIL_SEND_TIMEOUT_GUIDANCE =
|
|
|
353
353
|
"Mail AppleScript timed out (ETIMEDOUT / -1712 / AppleEvent timed out) — this is a find/reply/send hang (including find/reply/open before send), not a TCC / Automation deny. " +
|
|
354
354
|
"Real Mail Automation denials report -1743, -10004, or \"not authorized to send Apple events\". " +
|
|
355
355
|
"A timeout can happen after Mail already delivered the message. Check Sent (and Outbox) for this send before retrying; a retry after a successful delivery sends a second copy. " +
|
|
356
|
-
"This tool verifies Sent when a send hangs; if nothing is there yet, wait and look again rather than immediately resending.";
|
|
356
|
+
"This tool verifies Sent/Outbox when a send hangs (To + subject, Message-ID when available — never subject alone); if nothing is there yet, wait and look again rather than immediately resending.";
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
359
|
* Messages writes are a separate Automation target from Mail / Contacts / Calendar.
|
package/lib/calendarWrite.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "./appleScript.js";
|
|
23
23
|
import {
|
|
24
24
|
planWrite,
|
|
25
|
+
plannedWriteResult,
|
|
25
26
|
validateEventId,
|
|
26
27
|
validateEventKitId,
|
|
27
28
|
validateCalendarName,
|
|
@@ -772,7 +773,7 @@ export function calendarAdd(args = {}) {
|
|
|
772
773
|
`${alerts.minutes.length ? ` with alerts ${alerts.minutes.join(", ")} min before` : ""}`;
|
|
773
774
|
|
|
774
775
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
775
|
-
if (!plan.proceed) return
|
|
776
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
776
777
|
|
|
777
778
|
// Non-recurring creates MUST go through EventKit. Mini 8be6cd5: EventKit
|
|
778
779
|
// add failed (likely writeOnly cannot list calendars) and we silently
|
|
@@ -1037,7 +1038,7 @@ export function calendarEdit(args = {}) {
|
|
|
1037
1038
|
|
|
1038
1039
|
const summary = `update event ${eventId}: ${changed.join(", ")}`;
|
|
1039
1040
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
1040
|
-
if (!plan.proceed) return
|
|
1041
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1041
1042
|
|
|
1042
1043
|
const preferEventKit = Boolean(eventKitId) || eventId.includes(":");
|
|
1043
1044
|
if (preferEventKit && !recurrence.rule) {
|
|
@@ -1219,7 +1220,7 @@ export function calendarRemove(args = {}) {
|
|
|
1219
1220
|
dryRun: isFlagTrue(args.dry_run),
|
|
1220
1221
|
confirm: isFlagTrue(args.confirm)
|
|
1221
1222
|
});
|
|
1222
|
-
if (!plan.proceed) return
|
|
1223
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1223
1224
|
|
|
1224
1225
|
// writeOnly cannot re-query; prefer the EventKit session that cached
|
|
1225
1226
|
// the EKEvent from create. One-shot lookup is a full-access fallback.
|
|
@@ -1339,7 +1340,7 @@ export function calendarRsvp(args = {}) {
|
|
|
1339
1340
|
|
|
1340
1341
|
const summary = `RSVP ${responseRaw} to event ${eventId}`;
|
|
1341
1342
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
1342
|
-
if (!plan.proceed) return
|
|
1343
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1343
1344
|
|
|
1344
1345
|
const result = runAppleScript(
|
|
1345
1346
|
buildRsvpScript({ eventId, status: RSVP_STATUS[responseRaw], attendeeEmail }),
|
package/lib/contactsWrite.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { safeOpenApp } from "./shell.js";
|
|
21
21
|
import {
|
|
22
22
|
planWrite,
|
|
23
|
+
plannedWriteResult,
|
|
23
24
|
normalizeList,
|
|
24
25
|
isEmailAddress,
|
|
25
26
|
isPhoneNumber,
|
|
@@ -254,7 +255,7 @@ export function contactsAdd(args = {}, deps = {}) {
|
|
|
254
255
|
`${phones.phones.length ? ` and ${phones.phones.length} phone number(s)` : ""}`;
|
|
255
256
|
|
|
256
257
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
257
|
-
if (!plan.proceed) return
|
|
258
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
258
259
|
|
|
259
260
|
const properties = {};
|
|
260
261
|
if (firstName.text) properties["first name"] = firstName.text;
|
|
@@ -357,7 +358,7 @@ export function contactsEdit(args = {}, deps = {}) {
|
|
|
357
358
|
dryRun: isFlagTrue(args.dry_run),
|
|
358
359
|
confirm: isFlagTrue(args.confirm)
|
|
359
360
|
});
|
|
360
|
-
if (!plan.proceed) return
|
|
361
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
361
362
|
|
|
362
363
|
const ready = ensureContactsAppReady(deps);
|
|
363
364
|
if (!ready.ok) return { ok: false, message: failure(action, summary, ready) };
|
|
@@ -413,7 +414,7 @@ export function contactsRemove(args = {}, deps = {}) {
|
|
|
413
414
|
dryRun: isFlagTrue(args.dry_run),
|
|
414
415
|
confirm: isFlagTrue(args.confirm)
|
|
415
416
|
});
|
|
416
|
-
if (!plan.proceed) return
|
|
417
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
417
418
|
|
|
418
419
|
const ready = ensureContactsAppReady(deps);
|
|
419
420
|
if (!ready.ok) return { ok: false, message: failure(action, summary, ready) };
|
package/lib/mailWrite.js
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* is resolved to a Message-ID by reading the .emlx headers, so callers never
|
|
8
8
|
* have to invent an identifier.
|
|
9
9
|
*
|
|
10
|
-
* After a send/reply/forward hang, Sent
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* After a real send (and after a send/reply/forward hang), Sent and Outbox
|
|
11
|
+
* are checked before the tool reports success. AppleScript `send` returning
|
|
12
|
+
* without throw is not enough. A message already in Sent or still in Outbox
|
|
13
|
+
* is success — never a TCC fail. Hang recover matches To + subject (and
|
|
14
|
+
* Message-ID when available), never subject alone. ETIMEDOUT / -1712 is a
|
|
15
|
+
* timeout (find/reply/open before send too); -1743 / -10004 is a hard deny.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import fs from "fs";
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
} from "./appleScript.js";
|
|
30
32
|
import {
|
|
31
33
|
planWrite,
|
|
34
|
+
plannedWriteResult,
|
|
32
35
|
validateEmailList,
|
|
33
36
|
validateBody,
|
|
34
37
|
validateSubject,
|
|
@@ -336,9 +339,14 @@ on error errMsg number errNum
|
|
|
336
339
|
end try
|
|
337
340
|
error errMsg number errNum
|
|
338
341
|
end try
|
|
342
|
+
set atmOutgoingId to ""
|
|
339
343
|
tell application "Mail"
|
|
344
|
+
try
|
|
345
|
+
set atmOutgoingId to message id of newMessage as string
|
|
346
|
+
end try
|
|
340
347
|
${send ? "send newMessage" : "save newMessage"}
|
|
341
348
|
end tell
|
|
349
|
+
if atmOutgoingId is not "" then return atmOutgoingId
|
|
342
350
|
return "OK"`;
|
|
343
351
|
}
|
|
344
352
|
|
|
@@ -350,8 +358,31 @@ function summarizeRecipients(to, cc, bcc) {
|
|
|
350
358
|
return parts.join("; ");
|
|
351
359
|
}
|
|
352
360
|
|
|
361
|
+
export const SENT_VERIFY_SENT = "SENT";
|
|
362
|
+
export const SENT_VERIFY_OUTBOX = "OUTBOX";
|
|
363
|
+
export const SENT_VERIFY_NOT_FOUND = "NOT_FOUND";
|
|
364
|
+
/** Older hang-recovery scripts returned FOUND to mean Sent. Still accepted. */
|
|
353
365
|
export const SENT_VERIFY_FOUND = "FOUND";
|
|
354
366
|
export const SENT_VERIFY_TIMEOUT_MS = 15000;
|
|
367
|
+
export const SENT_VERIFY_ATTEMPTS = 3;
|
|
368
|
+
export const SENT_VERIFY_RETRY_MS = 400;
|
|
369
|
+
|
|
370
|
+
export const MAIL_VERIFY_MISS_GUIDANCE =
|
|
371
|
+
"Mail reported the send call succeeded, but the message was not found in Sent or Outbox. Do not assume it was delivered.";
|
|
372
|
+
|
|
373
|
+
function defaultSentVerifySleep(ms) {
|
|
374
|
+
if (!ms || ms <= 0) return;
|
|
375
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Test hook: skip the retry delay without changing attempt count. */
|
|
379
|
+
export const sentVerifyClock = {
|
|
380
|
+
sleep: defaultSentVerifySleep
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
export function resetSentVerifyClock() {
|
|
384
|
+
sentVerifyClock.sleep = defaultSentVerifySleep;
|
|
385
|
+
}
|
|
355
386
|
|
|
356
387
|
/**
|
|
357
388
|
* Hard Mail Automation deny: -1743 / -10004 / "not authorized…", or
|
|
@@ -376,10 +407,10 @@ function mailBoxesPreamble() {
|
|
|
376
407
|
return ` set cutoff to (current date) - (10 * minutes)
|
|
377
408
|
set boxes to {}
|
|
378
409
|
try
|
|
379
|
-
set end of boxes to
|
|
410
|
+
set end of boxes to outgoing mailbox
|
|
380
411
|
end try
|
|
381
412
|
try
|
|
382
|
-
set end of boxes to
|
|
413
|
+
set end of boxes to sent mailbox
|
|
383
414
|
end try
|
|
384
415
|
repeat with acct in accounts
|
|
385
416
|
try
|
|
@@ -392,6 +423,48 @@ function appleScriptToList(addresses = []) {
|
|
|
392
423
|
return `{${addresses.map((address) => asString(address)).join(", ")}}`;
|
|
393
424
|
}
|
|
394
425
|
|
|
426
|
+
function loadMailboxMessagesSnippet() {
|
|
427
|
+
return ` set recentMsgs to {}
|
|
428
|
+
set scanAll to false
|
|
429
|
+
try
|
|
430
|
+
if boxRef is outgoing mailbox then set scanAll to true
|
|
431
|
+
end try
|
|
432
|
+
try
|
|
433
|
+
set nm to name of boxRef as string
|
|
434
|
+
if nm contains "Outbox" then set scanAll to true
|
|
435
|
+
if nm contains "Outgoing" then set scanAll to true
|
|
436
|
+
end try
|
|
437
|
+
if scanAll then
|
|
438
|
+
set recentMsgs to messages of boxRef
|
|
439
|
+
else
|
|
440
|
+
set recentMsgs to (messages of boxRef whose date sent > cutoff)
|
|
441
|
+
end if`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function verifyHitReturn() {
|
|
445
|
+
return ` try
|
|
446
|
+
if boxRef is outgoing mailbox then return "${SENT_VERIFY_OUTBOX}"
|
|
447
|
+
end try
|
|
448
|
+
try
|
|
449
|
+
set nm to name of boxRef as string
|
|
450
|
+
if nm contains "Outbox" then return "${SENT_VERIFY_OUTBOX}"
|
|
451
|
+
if nm contains "Outgoing" then return "${SENT_VERIFY_OUTBOX}"
|
|
452
|
+
end try
|
|
453
|
+
return "${SENT_VERIFY_SENT}"`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function recipientHitSnippet() {
|
|
457
|
+
return ` set hitTo to false
|
|
458
|
+
try
|
|
459
|
+
repeat with recip in (to recipients of msg)
|
|
460
|
+
set recipAddr to address of recip as string
|
|
461
|
+
repeat with wanted in wantedTos
|
|
462
|
+
if recipAddr is (wanted as string) then set hitTo to true
|
|
463
|
+
end repeat
|
|
464
|
+
end repeat
|
|
465
|
+
end try`;
|
|
466
|
+
}
|
|
467
|
+
|
|
395
468
|
/**
|
|
396
469
|
* Look in Sent / Outbox for a reply of `messageId`.
|
|
397
470
|
* Proper Mail replies set In-Reply-To. The fallback `make new outgoing
|
|
@@ -414,7 +487,7 @@ tell application "Mail"
|
|
|
414
487
|
${mailBoxesPreamble()}
|
|
415
488
|
repeat with boxRef in boxes
|
|
416
489
|
try
|
|
417
|
-
|
|
490
|
+
${loadMailboxMessagesSnippet()}
|
|
418
491
|
repeat with msg in recentMsgs
|
|
419
492
|
try
|
|
420
493
|
set candId to message id of msg
|
|
@@ -423,12 +496,18 @@ ${mailBoxesPreamble()}
|
|
|
423
496
|
else
|
|
424
497
|
try
|
|
425
498
|
set src to source of msg
|
|
426
|
-
if src contains ("In-Reply-To: " & ${needleAngle}) then
|
|
427
|
-
|
|
499
|
+
if src contains ("In-Reply-To: " & ${needleAngle}) then
|
|
500
|
+
${verifyHitReturn()}
|
|
501
|
+
end if
|
|
502
|
+
if src contains ("In-Reply-To: " & ${needleBare}) then
|
|
503
|
+
${verifyHitReturn()}
|
|
504
|
+
end if
|
|
428
505
|
end try
|
|
429
506
|
if origSubject is not "" then
|
|
430
507
|
set subj to subject of msg
|
|
431
|
-
if subj is ("Re: " & origSubject) then
|
|
508
|
+
if subj is ("Re: " & origSubject) then
|
|
509
|
+
${verifyHitReturn()}
|
|
510
|
+
end if
|
|
432
511
|
end if
|
|
433
512
|
end if
|
|
434
513
|
end try
|
|
@@ -436,7 +515,7 @@ ${mailBoxesPreamble()}
|
|
|
436
515
|
end try
|
|
437
516
|
end repeat
|
|
438
517
|
end tell
|
|
439
|
-
return "
|
|
518
|
+
return "${SENT_VERIFY_NOT_FOUND}"`;
|
|
440
519
|
}
|
|
441
520
|
|
|
442
521
|
/**
|
|
@@ -463,7 +542,7 @@ ${mailBoxesPreamble()}
|
|
|
463
542
|
set wantedTos to ${toList}
|
|
464
543
|
repeat with boxRef in boxes
|
|
465
544
|
try
|
|
466
|
-
|
|
545
|
+
${loadMailboxMessagesSnippet()}
|
|
467
546
|
repeat with msg in recentMsgs
|
|
468
547
|
try
|
|
469
548
|
set candId to message id of msg
|
|
@@ -474,15 +553,7 @@ ${mailBoxesPreamble()}
|
|
|
474
553
|
if src contains ("In-Reply-To: " & ${needleAngle}) or src contains ("In-Reply-To: " & ${needleBare}) then
|
|
475
554
|
-- A reply to the original is not this forward.
|
|
476
555
|
else
|
|
477
|
-
|
|
478
|
-
try
|
|
479
|
-
repeat with recip in (to recipients of msg)
|
|
480
|
-
set recipAddr to address of recip as string
|
|
481
|
-
repeat with wanted in wantedTos
|
|
482
|
-
if recipAddr is (wanted as string) then set hitTo to true
|
|
483
|
-
end repeat
|
|
484
|
-
end repeat
|
|
485
|
-
end try
|
|
556
|
+
${recipientHitSnippet()}
|
|
486
557
|
if hitTo then
|
|
487
558
|
set subj to subject of msg
|
|
488
559
|
set exactFwd to false
|
|
@@ -498,8 +569,12 @@ ${mailBoxesPreamble()}
|
|
|
498
569
|
set mentionsOrigId to false
|
|
499
570
|
if src contains ${needleAngle} then set mentionsOrigId to true
|
|
500
571
|
if src contains ${needleBare} then set mentionsOrigId to true
|
|
501
|
-
if exactFwd then
|
|
502
|
-
|
|
572
|
+
if exactFwd then
|
|
573
|
+
${verifyHitReturn()}
|
|
574
|
+
end if
|
|
575
|
+
if looksForward and mentionsOrigId then
|
|
576
|
+
${verifyHitReturn()}
|
|
577
|
+
end if
|
|
503
578
|
end if
|
|
504
579
|
end if
|
|
505
580
|
end if
|
|
@@ -508,45 +583,125 @@ ${mailBoxesPreamble()}
|
|
|
508
583
|
end try
|
|
509
584
|
end repeat
|
|
510
585
|
end tell
|
|
511
|
-
return "
|
|
586
|
+
return "${SENT_VERIFY_NOT_FOUND}"`;
|
|
512
587
|
}
|
|
513
588
|
|
|
514
589
|
/**
|
|
515
|
-
* Look in Sent / Outbox for a compose
|
|
516
|
-
*
|
|
590
|
+
* Look in Sent / Outbox for a compose matching To + subject.
|
|
591
|
+
* Never matches subject alone — short subjects like "test" are unsafe.
|
|
592
|
+
* When `messageId` is present, prefer that id together with To.
|
|
517
593
|
*/
|
|
518
|
-
export function
|
|
519
|
-
const
|
|
594
|
+
export function buildFindSentByRecipientAndSubjectScript(subject, toAddresses = [], messageId = null) {
|
|
595
|
+
const toList = appleScriptToList(toAddresses);
|
|
596
|
+
const subj = asString(subject || "");
|
|
597
|
+
const idLit = messageId ? asString(messageId) : null;
|
|
598
|
+
const matchBody = idLit
|
|
599
|
+
? `${recipientHitSnippet()}
|
|
600
|
+
set candId to message id of msg
|
|
601
|
+
set hitId to false
|
|
602
|
+
try
|
|
603
|
+
if candId is ${idLit} then set hitId to true
|
|
604
|
+
end try
|
|
605
|
+
if hitId and hitTo then
|
|
606
|
+
${verifyHitReturn()}
|
|
607
|
+
end if
|
|
608
|
+
if hitTo then
|
|
609
|
+
set subj to subject of msg
|
|
610
|
+
if subj is ${subj} then
|
|
611
|
+
${verifyHitReturn()}
|
|
612
|
+
end if
|
|
613
|
+
end if`
|
|
614
|
+
: `${recipientHitSnippet()}
|
|
615
|
+
if hitTo then
|
|
616
|
+
set subj to subject of msg
|
|
617
|
+
if subj is ${subj} then
|
|
618
|
+
${verifyHitReturn()}
|
|
619
|
+
end if
|
|
620
|
+
end if`;
|
|
621
|
+
|
|
520
622
|
return `tell application "Mail"
|
|
521
623
|
${mailBoxesPreamble()}
|
|
624
|
+
set wantedTos to ${toList}
|
|
522
625
|
repeat with boxRef in boxes
|
|
523
626
|
try
|
|
524
|
-
|
|
525
|
-
|
|
627
|
+
${loadMailboxMessagesSnippet()}
|
|
628
|
+
repeat with msg in recentMsgs
|
|
629
|
+
try
|
|
630
|
+
${matchBody}
|
|
631
|
+
end try
|
|
632
|
+
end repeat
|
|
526
633
|
end try
|
|
527
634
|
end repeat
|
|
528
635
|
end tell
|
|
529
|
-
return "
|
|
636
|
+
return "${SENT_VERIFY_NOT_FOUND}"`;
|
|
530
637
|
}
|
|
531
638
|
|
|
532
639
|
/**
|
|
533
|
-
*
|
|
534
|
-
*
|
|
640
|
+
* @deprecated Subject-only matching is unsafe. Requires To as the second
|
|
641
|
+
* argument; without recipients the scan cannot hit.
|
|
535
642
|
*/
|
|
536
|
-
export function
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
643
|
+
export function buildFindSentBySubjectScript(subject, toAddresses = []) {
|
|
644
|
+
return buildFindSentByRecipientAndSubjectScript(subject, toAddresses);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export function parseSentVerifyOutput(output) {
|
|
648
|
+
const out = String(output || "").trim();
|
|
649
|
+
if (out === SENT_VERIFY_SENT || out === SENT_VERIFY_FOUND) {
|
|
650
|
+
return { found: true, mailbox: "sent" };
|
|
651
|
+
}
|
|
652
|
+
if (out === SENT_VERIFY_OUTBOX) {
|
|
653
|
+
return { found: true, mailbox: "outbox" };
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function parseOutgoingMessageId(output) {
|
|
659
|
+
const raw = String(output || "").trim();
|
|
660
|
+
if (!raw || raw === "OK") return null;
|
|
661
|
+
return validateMessageId(raw);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function pickSentVerifyScript({
|
|
665
|
+
inReplyTo = null,
|
|
666
|
+
subject = null,
|
|
667
|
+
forwardTo = null,
|
|
668
|
+
to = null,
|
|
669
|
+
messageId = null
|
|
670
|
+
} = {}) {
|
|
671
|
+
if (forwardTo && inReplyTo) return buildFindSentForwardScript(inReplyTo, forwardTo);
|
|
672
|
+
if (inReplyTo) return buildFindSentByInReplyToScript(inReplyTo);
|
|
673
|
+
const toList = Array.isArray(to) ? to : [];
|
|
674
|
+
if (toList.length > 0 && (subject || messageId)) {
|
|
675
|
+
return buildFindSentByRecipientAndSubjectScript(subject || "", toList, messageId);
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* After a send hang, ask Mail whether the message is already in Sent/Outbox.
|
|
682
|
+
* Compose matches require To + subject (Message-ID when available) — never
|
|
683
|
+
* subject alone. Returns a hit object or null. Verify failure is not success.
|
|
684
|
+
*/
|
|
685
|
+
export function recoverIfInSent(match = {}) {
|
|
686
|
+
const script = pickSentVerifyScript(match);
|
|
544
687
|
if (!script) return null;
|
|
545
688
|
const result = runAppleScript(script, { timeout: SENT_VERIFY_TIMEOUT_MS, appName: "Mail" });
|
|
546
689
|
if (!result.ok) return null;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
690
|
+
return parseSentVerifyOutput(result.output);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Poll Sent/Outbox after a send that returned without throw.
|
|
695
|
+
*/
|
|
696
|
+
export function verifyQueuedMessage(match = {}, { attempts = SENT_VERIFY_ATTEMPTS, retryMs = SENT_VERIFY_RETRY_MS } = {}) {
|
|
697
|
+
let last = null;
|
|
698
|
+
const n = Math.max(1, attempts);
|
|
699
|
+
for (let i = 0; i < n; i++) {
|
|
700
|
+
last = recoverIfInSent(match);
|
|
701
|
+
if (last) return last;
|
|
702
|
+
if (i < n - 1) sentVerifyClock.sleep(retryMs);
|
|
703
|
+
}
|
|
704
|
+
return last;
|
|
550
705
|
}
|
|
551
706
|
|
|
552
707
|
export function isMailAccessibilityDenial(result) {
|
|
@@ -586,9 +741,41 @@ function failure(action, summary, result, secrets) {
|
|
|
586
741
|
}
|
|
587
742
|
|
|
588
743
|
/**
|
|
589
|
-
* Finish a compose/reply/forward. A send
|
|
590
|
-
*
|
|
744
|
+
* Finish a compose/reply/forward. A real send is not success until Sent or
|
|
745
|
+
* Outbox verify hits. A send hang is not labeled TCC; if Mail already
|
|
746
|
+
* delivered, return success. Hard -1743/-10004 stays a deny.
|
|
591
747
|
*/
|
|
748
|
+
function deliverySummary(successSummary, mailbox, recovered) {
|
|
749
|
+
if (mailbox === "outbox") {
|
|
750
|
+
return recovered
|
|
751
|
+
? "queued in Outbox (still sending; verified after AppleScript hang)"
|
|
752
|
+
: "queued in Outbox (still sending)";
|
|
753
|
+
}
|
|
754
|
+
return recovered
|
|
755
|
+
? `${successSummary} (verified in Sent after AppleScript hang)`
|
|
756
|
+
: `${successSummary} (verified in Sent)`;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function sendVerifiedSuccess({ action, successSummary, details, verified, recovered }) {
|
|
760
|
+
const mailbox = verified.mailbox;
|
|
761
|
+
const delivered = mailbox === "sent";
|
|
762
|
+
return {
|
|
763
|
+
ok: true,
|
|
764
|
+
delivered,
|
|
765
|
+
mailbox,
|
|
766
|
+
recovered: recovered || undefined,
|
|
767
|
+
message: writeSuccessMessage(action, deliverySummary(successSummary, mailbox, recovered), {
|
|
768
|
+
...details,
|
|
769
|
+
mailbox,
|
|
770
|
+
delivery: delivered ? "sent" : "outbox"
|
|
771
|
+
})
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function verifyMissMessage(action, summary) {
|
|
776
|
+
return `${action} failed — attempted to ${summary}. ${MAIL_VERIFY_MISS_GUIDANCE}`;
|
|
777
|
+
}
|
|
778
|
+
|
|
592
779
|
function finalizeMailWrite({
|
|
593
780
|
action,
|
|
594
781
|
summary,
|
|
@@ -598,30 +785,33 @@ function finalizeMailWrite({
|
|
|
598
785
|
inReplyTo = null,
|
|
599
786
|
subject = null,
|
|
600
787
|
forwardTo = null,
|
|
788
|
+
to = null,
|
|
789
|
+
messageId = null,
|
|
601
790
|
successSummary,
|
|
602
791
|
details
|
|
603
792
|
}) {
|
|
793
|
+
const match = { inReplyTo, subject, forwardTo, to, messageId };
|
|
794
|
+
|
|
604
795
|
if (result.ok) {
|
|
605
|
-
|
|
796
|
+
if (!sendNow) {
|
|
797
|
+
return { ok: true, delivered: false, mailbox: null, message: writeSuccessMessage(action, successSummary, details) };
|
|
798
|
+
}
|
|
799
|
+
const verified = verifyQueuedMessage(match);
|
|
800
|
+
if (!verified) {
|
|
801
|
+
return { ok: false, delivered: false, mailbox: null, message: verifyMissMessage(action, summary) };
|
|
802
|
+
}
|
|
803
|
+
return sendVerifiedSuccess({ action, successSummary, details, verified, recovered: false });
|
|
606
804
|
}
|
|
607
805
|
|
|
608
806
|
if (sendNow && isMailSendTimeout(result)) {
|
|
609
|
-
const recovered = recoverIfInSent(
|
|
807
|
+
const recovered = recoverIfInSent(match);
|
|
610
808
|
if (recovered) {
|
|
611
|
-
return {
|
|
612
|
-
ok: true,
|
|
613
|
-
recovered: true,
|
|
614
|
-
message: writeSuccessMessage(
|
|
615
|
-
action,
|
|
616
|
-
`${successSummary} (verified in Sent after AppleScript hang)`,
|
|
617
|
-
details
|
|
618
|
-
)
|
|
619
|
-
};
|
|
809
|
+
return sendVerifiedSuccess({ action, successSummary, details, verified: recovered, recovered: true });
|
|
620
810
|
}
|
|
621
|
-
return { ok: false, message: failure(action, summary, result, secrets) };
|
|
811
|
+
return { ok: false, delivered: false, mailbox: null, message: failure(action, summary, result, secrets) };
|
|
622
812
|
}
|
|
623
813
|
|
|
624
|
-
return { ok: false, message: failure(action, summary, result, secrets) };
|
|
814
|
+
return { ok: false, delivered: false, mailbox: null, message: failure(action, summary, result, secrets) };
|
|
625
815
|
}
|
|
626
816
|
|
|
627
817
|
/**
|
|
@@ -664,7 +854,7 @@ export function mailCompose(args = {}, { draft = false } = {}) {
|
|
|
664
854
|
dryRun: isFlagTrue(args.dry_run),
|
|
665
855
|
confirm: isFlagTrue(args.confirm)
|
|
666
856
|
});
|
|
667
|
-
if (!plan.proceed) return
|
|
857
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
668
858
|
|
|
669
859
|
const script = buildComposeScript({
|
|
670
860
|
to: to.addresses,
|
|
@@ -684,6 +874,8 @@ export function mailCompose(args = {}, { draft = false } = {}) {
|
|
|
684
874
|
secrets: [body.text, subject.text],
|
|
685
875
|
sendNow: !draft,
|
|
686
876
|
subject: draft ? null : subject.text,
|
|
877
|
+
to: draft ? null : to.addresses,
|
|
878
|
+
messageId: draft ? null : parseOutgoingMessageId(result.output),
|
|
687
879
|
successSummary: draft ? "draft saved to Drafts" : "sent",
|
|
688
880
|
details: {
|
|
689
881
|
to: to.addresses.join(", "),
|
|
@@ -743,7 +935,7 @@ export function mailReply(args = {}) {
|
|
|
743
935
|
dryRun: isFlagTrue(args.dry_run),
|
|
744
936
|
confirm: isFlagTrue(args.confirm)
|
|
745
937
|
});
|
|
746
|
-
if (!plan.proceed) return
|
|
938
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
747
939
|
|
|
748
940
|
const result = runAppleScript(
|
|
749
941
|
buildReplyScript({ messageId: resolved.messageId, body: body.text, replyAll, sendNow }),
|
|
@@ -806,7 +998,7 @@ export function mailForward(args = {}) {
|
|
|
806
998
|
dryRun: isFlagTrue(args.dry_run),
|
|
807
999
|
confirm: isFlagTrue(args.confirm)
|
|
808
1000
|
});
|
|
809
|
-
if (!plan.proceed) return
|
|
1001
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
810
1002
|
|
|
811
1003
|
const result = runAppleScript(
|
|
812
1004
|
buildForwardScript({ messageId: resolved.messageId, to: to.addresses, body: body.text, sendNow }),
|
|
@@ -854,7 +1046,7 @@ export function mailMark(args = {}) {
|
|
|
854
1046
|
dryRun: isFlagTrue(args.dry_run),
|
|
855
1047
|
confirm: isFlagTrue(args.confirm)
|
|
856
1048
|
});
|
|
857
|
-
if (!plan.proceed) return
|
|
1049
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
858
1050
|
|
|
859
1051
|
const result = runAppleScript(buildMarkScript({ messageId: resolved.messageId, read: status === "read" }), { appName: "Mail" });
|
|
860
1052
|
if (!result.ok) return { ok: false, message: failure(action, summary, result, []) };
|
|
@@ -899,7 +1091,7 @@ export function mailArchive(args = {}) {
|
|
|
899
1091
|
dryRun: isFlagTrue(args.dry_run),
|
|
900
1092
|
confirm: isFlagTrue(args.confirm)
|
|
901
1093
|
});
|
|
902
|
-
if (!plan.proceed) return
|
|
1094
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
903
1095
|
|
|
904
1096
|
const result = runAppleScript(
|
|
905
1097
|
buildMoveScript({
|
|
@@ -932,7 +1124,7 @@ export function mailTrash(args = {}) {
|
|
|
932
1124
|
dryRun: isFlagTrue(args.dry_run),
|
|
933
1125
|
confirm: isFlagTrue(args.confirm)
|
|
934
1126
|
});
|
|
935
|
-
if (!plan.proceed) return
|
|
1127
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
936
1128
|
|
|
937
1129
|
const result = runAppleScript(
|
|
938
1130
|
buildMoveScript({
|
package/lib/messagesWrite.js
CHANGED
|
@@ -17,6 +17,7 @@ import { escapeSQL } from "./validators.js";
|
|
|
17
17
|
import { runAppleScript, asString, MESSAGES_TCC_GUIDANCE, MESSAGES_APP_NOT_RUNNING_GUIDANCE, ATTRIBUTION_GUIDANCE } from "./appleScript.js";
|
|
18
18
|
import {
|
|
19
19
|
planWrite,
|
|
20
|
+
plannedWriteResult,
|
|
20
21
|
normalizeList,
|
|
21
22
|
isMessagesHandle,
|
|
22
23
|
validateChatGuid,
|
|
@@ -261,7 +262,7 @@ export function messagesSend(args = {}, deps = {}) {
|
|
|
261
262
|
dryRun,
|
|
262
263
|
confirm
|
|
263
264
|
});
|
|
264
|
-
if (!plan.proceed) return
|
|
265
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
265
266
|
|
|
266
267
|
const result = runAppleScript(
|
|
267
268
|
buildSendToChatScript({ chatGuid: guid, text: body.text, attachmentPath: attachment.filePath }),
|
|
@@ -293,7 +294,7 @@ export function messagesSend(args = {}, deps = {}) {
|
|
|
293
294
|
|
|
294
295
|
const summary = `send a message to ${handles.join(", ")} over ${serviceRaw}${attachment.filePath ? " with an attachment" : ""}`;
|
|
295
296
|
const plan = planWrite({ action, summary, recipientCount: handles.length, dryRun, confirm });
|
|
296
|
-
if (!plan.proceed) return
|
|
297
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
297
298
|
|
|
298
299
|
const result = runAppleScript(
|
|
299
300
|
buildSendToHandlesScript({
|
package/lib/writeGuards.js
CHANGED
|
@@ -215,6 +215,24 @@ export function planWrite({
|
|
|
215
215
|
return { decision: "execute", proceed: true, requiresConfirm, message: null };
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Planned / dry_run / confirm-blocked results are not deliveries.
|
|
220
|
+
*
|
|
221
|
+
* MCP CallTool only surfaces `message` plus optional `isError`. Clients that
|
|
222
|
+
* treat a missing `isError` as success would read `ok: true` planned results
|
|
223
|
+
* as delivered. Planned writes therefore return `ok: false` with
|
|
224
|
+
* `planned: true` and `delivered: false`.
|
|
225
|
+
*/
|
|
226
|
+
export function plannedWriteResult(plan) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
planned: true,
|
|
230
|
+
delivered: false,
|
|
231
|
+
mailbox: null,
|
|
232
|
+
message: plan && plan.message ? plan.message : "Nothing was changed."
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
218
236
|
export function truncate(text, max = 200) {
|
|
219
237
|
if (typeof text !== "string") return "";
|
|
220
238
|
const oneLine = text.replace(/\s+/g, " ").trim();
|
package/lib/writeTools.js
CHANGED
|
@@ -48,7 +48,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
48
48
|
// ============ MAIL WRITES ============
|
|
49
49
|
{
|
|
50
50
|
name: "mail_send",
|
|
51
|
-
description: "Send an email through Mail.app. Sending to more than one recipient in total (to + cc + bcc) requires confirm=true.
|
|
51
|
+
description: "Send an email through Mail.app. Success means the message was verified in Sent or is still in Outbox (still sending) — AppleScript send returning is not enough. Sending to more than one recipient in total (to + cc + bcc) requires confirm=true. dry_run and unconfirmed multi-recipient calls are not deliveries (MCP isError). A send hang is a timeout, not TCC, unless Mail reports -1743/-10004; hang recover matches To+subject (Message-ID when available), never subject alone. Clients must Sent-check before retrying a timed-out send.",
|
|
52
52
|
inputSchema: {
|
|
53
53
|
type: "object",
|
|
54
54
|
properties: {
|
|
@@ -82,7 +82,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
name: "mail_reply",
|
|
85
|
-
description: "Reply to an existing email. reply_all=true fans out to every original recipient and requires confirm=true. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004;
|
|
85
|
+
description: "Reply to an existing email. reply_all=true fans out to every original recipient and requires confirm=true. A real send is verified in Sent/Outbox before success. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004; hang recover uses In-Reply-To. Clients must Sent-check before retrying a timed-out send.",
|
|
86
86
|
inputSchema: {
|
|
87
87
|
type: "object",
|
|
88
88
|
properties: {
|
|
@@ -97,7 +97,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
name: "mail_forward",
|
|
100
|
-
description: "Forward an existing email to new recipients. More than one recipient requires confirm=true. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004;
|
|
100
|
+
description: "Forward an existing email to new recipients. More than one recipient requires confirm=true. A real send is verified in Sent/Outbox before success. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004; hang recover matches intended To plus Fwd: subject or original Message-ID. Clients must Sent-check before retrying a timed-out send.",
|
|
101
101
|
inputSchema: {
|
|
102
102
|
type: "object",
|
|
103
103
|
properties: {
|
|
@@ -343,6 +343,23 @@ export function isWriteTool(name) {
|
|
|
343
343
|
return Object.prototype.hasOwnProperty.call(WRITE_TOOL_HANDLERS, name);
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
/**
|
|
347
|
+
* MCP CallTool payload for a write. Planned / dry_run / confirm-blocked
|
|
348
|
+
* results set isError so clients that only check isError cannot treat them
|
|
349
|
+
* as delivered. Verified sends (ok: true) do not set isError.
|
|
350
|
+
*
|
|
351
|
+
* @param {{ ok?: boolean, planned?: boolean, message?: string }} writeResult
|
|
352
|
+
*/
|
|
353
|
+
export function mcpWriteResult(writeResult) {
|
|
354
|
+
const message = writeResult && writeResult.message ? writeResult.message : "write failed";
|
|
355
|
+
const planned = Boolean(writeResult && writeResult.planned);
|
|
356
|
+
const failed = !writeResult || writeResult.ok === false;
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: "text", text: message }],
|
|
359
|
+
...((failed || planned) ? { isError: true } : {})
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
346
363
|
/**
|
|
347
364
|
* Run a write in this process.
|
|
348
365
|
* @returns {{ ok: boolean, message: string }}
|
package/package.json
CHANGED
package/scripts/smoke-writes.js
CHANGED
|
@@ -233,8 +233,9 @@ function line(label, value) {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
function step(name, result, severity = "error") {
|
|
236
|
-
const
|
|
237
|
-
const
|
|
236
|
+
const planned = result && result.planned === true;
|
|
237
|
+
const failed = result && result.ok === false && !planned;
|
|
238
|
+
const status = planned ? "PLANNED" : failed ? (severity === "warning" ? "WARN" : "FAIL") : "OK";
|
|
238
239
|
console.log(`\n[${status}] ${name}`);
|
|
239
240
|
console.log(` ${result.message}`);
|
|
240
241
|
return result;
|
|
@@ -348,7 +349,7 @@ async function main() {
|
|
|
348
349
|
}));
|
|
349
350
|
results.push(created);
|
|
350
351
|
|
|
351
|
-
if (created.ok !== false) {
|
|
352
|
+
if (created.ok !== false || created.planned) {
|
|
352
353
|
const contactId = apply ? extractContactId(created.message) : "ABCD1234-SMOKE:ABPerson";
|
|
353
354
|
if (apply && !contactId) {
|
|
354
355
|
console.log(" contacts_add reported success but returned no contact id; skipping edit/delete.");
|
|
@@ -405,7 +406,7 @@ async function main() {
|
|
|
405
406
|
}));
|
|
406
407
|
results.push(eventCreated);
|
|
407
408
|
|
|
408
|
-
if (eventCreated.ok !== false) {
|
|
409
|
+
if (eventCreated.ok !== false || eventCreated.planned) {
|
|
409
410
|
const eventId = apply ? extractEventId(eventCreated.message) : "ATM-SMOKE-EVENT-UID";
|
|
410
411
|
const extractedKitId = apply ? extractEventKitId(eventCreated.message) : "EK-SMOKE";
|
|
411
412
|
const eventKitId = extractedKitId || (eventId && eventId.includes(":") ? eventId : null);
|
|
@@ -451,7 +452,7 @@ async function main() {
|
|
|
451
452
|
}
|
|
452
453
|
}
|
|
453
454
|
|
|
454
|
-
const failed = results.some((r) => r && r.ok === false);
|
|
455
|
+
const failed = results.some((r) => r && r.ok === false && !r.planned);
|
|
455
456
|
console.log("\n" + "=".repeat(60));
|
|
456
457
|
if (failed) {
|
|
457
458
|
console.log("Result: FAILED - see the messages above.");
|