apple-tools-mcp 3.0.6 → 3.0.8
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/lib/appleScript.js +2 -2
- package/lib/mailWrite.js +197 -64
- package/lib/writeTools.js +9 -6
- package/package.json +1 -1
package/lib/appleScript.js
CHANGED
|
@@ -340,7 +340,7 @@ export const MAIL_TCC_GUIDANCE =
|
|
|
340
340
|
*/
|
|
341
341
|
export const MAIL_ACCESSIBILITY_GUIDANCE =
|
|
342
342
|
"macOS denied Accessibility (System Events could not drive Mail's compose window). " +
|
|
343
|
-
"mail_send / mail_draft fill the body via
|
|
343
|
+
"mail_send / mail_draft / mail_reply / mail_forward fill the body via System Events so Mail does not cite-wrap it (FB11734014); Mail's mailto command does not return an outgoing message on current Mini/MacBook Mail (-2753). " +
|
|
344
344
|
"Allow node in System Settings → Privacy & Security → Accessibility, and keep Automation → Mail allowed. " +
|
|
345
345
|
"This is not a Mail Automation deny (-1743 / -10004).";
|
|
346
346
|
|
|
@@ -355,7 +355,7 @@ export const SYSTEM_EVENTS_STICKY_DENY_GUIDANCE =
|
|
|
355
355
|
/** System Events GUI scripting missing — not a Mail send hang. */
|
|
356
356
|
export const MAIL_GUI_SCRIPTING_GUIDANCE =
|
|
357
357
|
"System Events GUI scripting is unavailable for this process, so the body cannot be typed into Mail. " +
|
|
358
|
-
"mail_send / mail_draft locate the compose body via System Events (title-marker + AX hit-test) so Mail does not cite-wrap the Sent body (FB11734014). " +
|
|
358
|
+
"mail_send / mail_draft / mail_reply / mail_forward locate the compose body via System Events (title-marker + AX hit-test) so Mail does not cite-wrap the Sent body (FB11734014). " +
|
|
359
359
|
"Allow node → System Events in System Settings → Privacy & Security → Automation (run apple-tools-mcp permissions after install/upgrade) in addition to node → Mail, and keep Accessibility allowed. " +
|
|
360
360
|
SYSTEM_EVENTS_STICKY_DENY_GUIDANCE + " " +
|
|
361
361
|
"This is not a Mail send timeout and not a Mail Automation deny (-1743 / -10004).";
|
package/lib/mailWrite.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
runAppleScript,
|
|
23
23
|
asString,
|
|
24
24
|
asInteger,
|
|
25
|
+
asBoolean,
|
|
25
26
|
MAIL_TCC_GUIDANCE,
|
|
26
27
|
MAIL_SEND_TIMEOUT_GUIDANCE,
|
|
27
28
|
MAIL_ACCESSIBILITY_GUIDANCE,
|
|
@@ -197,6 +198,8 @@ export const SYSTEM_EVENTS_AUTOMATION_PROBE_MARKER = "ATM_SYSTEM_EVENTS_AUTOMATI
|
|
|
197
198
|
export const MAIL_BODY_MIN_AX_HEIGHT = 50;
|
|
198
199
|
/** Historical Mini Tab count. 2.1.3 finds the body via title-marker + AX hit-test, not Tab. */
|
|
199
200
|
export const MAIL_BODY_TAB_MAX = 6;
|
|
201
|
+
/** Command-Up in the compose body (start of document). Reply/forward prepend uses this instead of Cmd-A. */
|
|
202
|
+
export const MAIL_BODY_START_KEY_CODE = 126;
|
|
200
203
|
/** Mail window titles may truncate; match this many leading subject characters. */
|
|
201
204
|
export const MAIL_COMPOSE_TITLE_MARKER_MAX = 60;
|
|
202
205
|
/** Top-level `UI elements` of process Mail only — never a deep tree walk. */
|
|
@@ -507,7 +510,9 @@ function systemEventsGrantKind(result) {
|
|
|
507
510
|
* — not `set value`, not `AXUIElementSetAttributeValue`. Re-read process
|
|
508
511
|
* `AXFocusedUIElement` and require role `AXWebArea` plus the same position
|
|
509
512
|
* and size. Mismatch is `BODY_FOCUS_UNPROVEN` (draft deleted, nothing sent).
|
|
510
|
-
* Deliver with
|
|
513
|
+
* Deliver with clipboard paste. Compose passes replaceBody=true (Cmd-A then
|
|
514
|
+
* paste). Reply/forward pass false: Command-Up to the top of the body, then
|
|
515
|
+
* paste, so the quoted original stays. No coordinate click. No Mail
|
|
511
516
|
* `content` / `html content`. AXTitle reads in the title helpers stay inside
|
|
512
517
|
* `tell application "System Events"` (2.1.4 osacompile -2741).
|
|
513
518
|
*/
|
|
@@ -515,6 +520,7 @@ export function buildMailBodyPasteHandler() {
|
|
|
515
520
|
const seSecs = asInteger(MAIL_SE_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 30, field: "seTimeout" });
|
|
516
521
|
const topMax = asInteger(MAIL_COMPOSE_TOP_UI_MAX, { min: 1, max: 64, field: "topUiMax" });
|
|
517
522
|
const tokenId = asInteger(MAIL_RECIPIENT_TOKEN_CHAR_ID, { min: 1, max: 1114111, field: "tokenCharId" });
|
|
523
|
+
const startKey = asInteger(MAIL_BODY_START_KEY_CODE, { min: 1, max: 127, field: "bodyStartKey" });
|
|
518
524
|
return `property atmProvenBx : -1
|
|
519
525
|
property atmProvenBy : -1
|
|
520
526
|
property atmProvenBw : -1
|
|
@@ -561,6 +567,13 @@ on atmTitleMatches(atmTitle, atmMarker)
|
|
|
561
567
|
return false
|
|
562
568
|
end atmTitleMatches
|
|
563
569
|
|
|
570
|
+
on atmComposeTitleMarker(atmSubject)
|
|
571
|
+
set atmText to atmSubject as string
|
|
572
|
+
if atmText is "" then return "New Message"
|
|
573
|
+
if (length of atmText) > ${MAIL_COMPOSE_TITLE_MARKER_MAX} then return text 1 thru ${MAIL_COMPOSE_TITLE_MARKER_MAX} of atmText
|
|
574
|
+
return atmText
|
|
575
|
+
end atmComposeTitleMarker
|
|
576
|
+
|
|
564
577
|
on atmTopElemExactTitle(atmEl, atmMarker)
|
|
565
578
|
try
|
|
566
579
|
if my atmTitleMatches(name of atmEl as string, atmMarker) then return true
|
|
@@ -655,7 +668,7 @@ on atmStopScreenSaver()
|
|
|
655
668
|
end try
|
|
656
669
|
end atmStopScreenSaver
|
|
657
670
|
|
|
658
|
-
on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
671
|
+
on atmFillMailBody(bodyText, needle, titleMarker, msg, replaceBody)
|
|
659
672
|
try
|
|
660
673
|
tell application "Mail" to activate
|
|
661
674
|
delay 0.25
|
|
@@ -767,8 +780,13 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
|
767
780
|
set hadClip to true
|
|
768
781
|
end try
|
|
769
782
|
try
|
|
770
|
-
|
|
771
|
-
|
|
783
|
+
if replaceBody then
|
|
784
|
+
keystroke "a" using command down
|
|
785
|
+
delay 0.2
|
|
786
|
+
else
|
|
787
|
+
key code ${startKey} using command down
|
|
788
|
+
delay 0.2
|
|
789
|
+
end if
|
|
772
790
|
set the clipboard to bodyText
|
|
773
791
|
delay 0.2
|
|
774
792
|
keystroke "v" using command down
|
|
@@ -845,8 +863,9 @@ ${bodyCheck}`;
|
|
|
845
863
|
* is filled by Probe 3: exact subject title, first AXWebArea under that
|
|
846
864
|
* window (`entire contents`), `set focused of` that element, prove focus
|
|
847
865
|
* (role + position + size) via a fresh AXFocusedUIElement read, then Cmd-A
|
|
848
|
-
* and clipboard paste. No coordinate click. No AXValue
|
|
849
|
-
* `content of` oracle (Mail 16 / macOS 26 stays empty). Missing
|
|
866
|
+
* (`replaceBody` true) and clipboard paste. No coordinate click. No AXValue
|
|
867
|
+
* write. No Mail `content of` oracle (Mail 16 / macOS 26 stays empty). Missing
|
|
868
|
+
* window or
|
|
850
869
|
* body is BODY_FOCUS_FAILED; a focus-proof mismatch is BODY_FOCUS_UNPROVEN.
|
|
851
870
|
* Either deletes the draft and sends nothing. Header checks stay. The body
|
|
852
871
|
* needle runs only when the focused element is still that proven body, so a
|
|
@@ -865,7 +884,7 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
|
|
|
865
884
|
const needle = composeBodyNeedle(plainBody);
|
|
866
885
|
const titleMarker = composeTitleMarker(subject);
|
|
867
886
|
const pasteAndVerify = plainBody
|
|
868
|
-
? ` atmFillMailBody(${asString(plainBody)}, ${asString(needle)}, ${asString(titleMarker)}, newMessage)
|
|
887
|
+
? ` atmFillMailBody(${asString(plainBody)}, ${asString(needle)}, ${asString(titleMarker)}, newMessage, ${asBoolean(true)})
|
|
869
888
|
tell application "Mail"
|
|
870
889
|
${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
|
|
871
890
|
end tell`
|
|
@@ -992,7 +1011,7 @@ function mailBoxesPreamble() {
|
|
|
992
1011
|
return ` set cutoff to (current date) - (10 * minutes)
|
|
993
1012
|
set boxes to {}
|
|
994
1013
|
try
|
|
995
|
-
set end of boxes to
|
|
1014
|
+
set end of boxes to outbox
|
|
996
1015
|
end try
|
|
997
1016
|
try
|
|
998
1017
|
set end of boxes to sent mailbox
|
|
@@ -1012,7 +1031,7 @@ function loadMailboxMessagesSnippet() {
|
|
|
1012
1031
|
return ` set recentMsgs to {}
|
|
1013
1032
|
set scanAll to false
|
|
1014
1033
|
try
|
|
1015
|
-
if boxRef is
|
|
1034
|
+
if boxRef is outbox then set scanAll to true
|
|
1016
1035
|
end try
|
|
1017
1036
|
try
|
|
1018
1037
|
set nm to name of boxRef as string
|
|
@@ -1028,7 +1047,7 @@ function loadMailboxMessagesSnippet() {
|
|
|
1028
1047
|
|
|
1029
1048
|
function verifyHitReturn() {
|
|
1030
1049
|
return ` try
|
|
1031
|
-
if boxRef is
|
|
1050
|
+
if boxRef is outbox then return "${SENT_VERIFY_OUTBOX}"
|
|
1032
1051
|
end try
|
|
1033
1052
|
try
|
|
1034
1053
|
set nm to name of boxRef as string
|
|
@@ -1438,7 +1457,7 @@ function finalizeMailWrite({
|
|
|
1438
1457
|
}
|
|
1439
1458
|
const verified = verifyQueuedMessage(match, { deadlineMs: deadline });
|
|
1440
1459
|
if (!verified) {
|
|
1441
|
-
return { ok: false, delivered: false, mailbox: null, message: verifyMissMessage(action, summary) };
|
|
1460
|
+
return { ok: false, delivered: false, mailbox: null, unconfirmed: true, message: verifyMissMessage(action, summary) };
|
|
1442
1461
|
}
|
|
1443
1462
|
return sendVerifiedSuccess({ action, successSummary, details, verified, recovered: false });
|
|
1444
1463
|
}
|
|
@@ -1558,35 +1577,110 @@ export function mailCompose(args = {}, { draft = false, indexerMode = isIndexerM
|
|
|
1558
1577
|
});
|
|
1559
1578
|
}
|
|
1560
1579
|
|
|
1580
|
+
/**
|
|
1581
|
+
* Intro text pasted above a reply/forward quote. Trailing newline keeps the
|
|
1582
|
+
* note from gluing to "On … wrote:". Needle still comes from composeBodyNeedle.
|
|
1583
|
+
*/
|
|
1584
|
+
export function quoteIntroPasteText(body) {
|
|
1585
|
+
const text = composeNativeBody(body);
|
|
1586
|
+
if (!text) return "";
|
|
1587
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function quoteBodyNeedleCheck(body) {
|
|
1591
|
+
const needle = composeBodyNeedle(body);
|
|
1592
|
+
if (!needle) return "";
|
|
1593
|
+
return ` if my atmFocusedIsProvenBody() then
|
|
1594
|
+
set atmAxVal to my atmAxFocusedValue()
|
|
1595
|
+
if atmAxVal is not "" then
|
|
1596
|
+
if my atmAxValueIsRecipientToken(atmAxVal) is false then
|
|
1597
|
+
if atmAxVal does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
1598
|
+
end if
|
|
1599
|
+
end if
|
|
1600
|
+
end if`;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Probe 3 prepend: visible window title from the live subject, no Cmd-A, no
|
|
1605
|
+
* AppleScript `content`. Mail 16 / macOS 26 leaves `set content` empty, which
|
|
1606
|
+
* is why reply/forward used to send a blank intro.
|
|
1607
|
+
*/
|
|
1608
|
+
function quoteBodyPasteAndVerify(outgoingVar, body) {
|
|
1609
|
+
const intro = quoteIntroPasteText(body);
|
|
1610
|
+
if (!intro) return "";
|
|
1611
|
+
const bodyCheck = quoteBodyNeedleCheck(intro);
|
|
1612
|
+
return ` tell application "Mail"
|
|
1613
|
+
set atmTitleMarker to my atmComposeTitleMarker(subject of ${outgoingVar} as string)
|
|
1614
|
+
end tell
|
|
1615
|
+
atmFillMailBody(${asString(intro)}, ${asString(composeBodyNeedle(intro))}, atmTitleMarker, ${outgoingVar}, ${asBoolean(false)})
|
|
1616
|
+
tell application "Mail"
|
|
1617
|
+
${bodyCheck}
|
|
1618
|
+
end tell`;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function quotePasteGuard(outgoingVar, fillBlock) {
|
|
1622
|
+
if (!fillBlock) return "";
|
|
1623
|
+
return `try
|
|
1624
|
+
${fillBlock}
|
|
1625
|
+
on error errMsg number errNum
|
|
1626
|
+
try
|
|
1627
|
+
tell application "Mail" to delete ${outgoingVar}
|
|
1628
|
+
end try
|
|
1629
|
+
error errMsg number errNum
|
|
1630
|
+
end try
|
|
1631
|
+
`;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function finishMailGuiWrite({ action, summary, result, indexerMode, finalize }) {
|
|
1635
|
+
if (
|
|
1636
|
+
!result.ok &&
|
|
1637
|
+
indexerMode &&
|
|
1638
|
+
(isMailGuiScriptingUnavailable(result) || isMailAccessibilityDenial(result))
|
|
1639
|
+
) {
|
|
1640
|
+
return guiScriptingUnavailableResult(action, summary, result, { indexerMode });
|
|
1641
|
+
}
|
|
1642
|
+
return finalize();
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1561
1645
|
export function buildReplyScript({ messageId, body, replyAll, sendNow }) {
|
|
1562
|
-
|
|
1646
|
+
const mailSecs = asInteger(MAIL_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 60, field: "mailTimeout" });
|
|
1647
|
+
const fillBlock = quoteBodyPasteAndVerify("theReply", body);
|
|
1648
|
+
const pasteAndVerify = quotePasteGuard("theReply", fillBlock);
|
|
1649
|
+
const replyAllClause = replyAll ? "with reply to all" : "without reply to all";
|
|
1650
|
+
|
|
1651
|
+
return `${buildMailBodyPasteHandler()}
|
|
1652
|
+
${findMessageHandler()}
|
|
1563
1653
|
|
|
1564
1654
|
set theMessage to atmFindMessage(${asString(messageId)})
|
|
1565
1655
|
tell application "Mail"
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
end
|
|
1584
|
-
|
|
1656
|
+
with timeout of ${mailSecs} seconds
|
|
1657
|
+
set theReply to missing value
|
|
1658
|
+
try
|
|
1659
|
+
set theReply to reply theMessage with opening window ${replyAllClause}
|
|
1660
|
+
end try
|
|
1661
|
+
if theReply is missing value then
|
|
1662
|
+
-- Some Mail versions do not return the outgoing message from a reply.
|
|
1663
|
+
-- Fall back to a new visible message addressed to the original sender.
|
|
1664
|
+
-- Never set AppleScript content (Mail 16 / macOS 26 stays empty).
|
|
1665
|
+
set origSubject to subject of theMessage
|
|
1666
|
+
set origSender to extract address from (sender of theMessage)
|
|
1667
|
+
set theReply to make new outgoing message with properties {subject:("Re: " & origSubject), visible:true}
|
|
1668
|
+
tell theReply
|
|
1669
|
+
make new to recipient at end of to recipients with properties {address:origSender}
|
|
1670
|
+
end tell
|
|
1671
|
+
end if
|
|
1672
|
+
activate
|
|
1673
|
+
end timeout
|
|
1674
|
+
end tell
|
|
1675
|
+
${pasteAndVerify}tell application "Mail"
|
|
1676
|
+
with timeout of ${mailSecs} seconds
|
|
1677
|
+
${sendNow ? "send theReply" : "save theReply"}
|
|
1678
|
+
end timeout
|
|
1585
1679
|
end tell
|
|
1586
1680
|
return "OK"`;
|
|
1587
1681
|
}
|
|
1588
1682
|
|
|
1589
|
-
export function mailReply(args = {}) {
|
|
1683
|
+
export function mailReply(args = {}, { indexerMode = isIndexerMode() } = {}) {
|
|
1590
1684
|
const action = "mail_reply";
|
|
1591
1685
|
const resolved = resolveMailMessageId({ messageId: args.message_id, filePath: args.file_path });
|
|
1592
1686
|
if (resolved.error) return { ok: false, message: `${action} refused: ${resolved.error}` };
|
|
@@ -1609,47 +1703,72 @@ export function mailReply(args = {}) {
|
|
|
1609
1703
|
});
|
|
1610
1704
|
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1611
1705
|
|
|
1706
|
+
if (quoteIntroPasteText(body.text)) {
|
|
1707
|
+
const se = probeSystemEventsGui();
|
|
1708
|
+
if (!se.ok) {
|
|
1709
|
+
return guiScriptingUnavailableResult(action, summary, se, { indexerMode });
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1612
1713
|
const deadlineMs = sentVerifyClock.now() + MCP_CALL_DEADLINE_MS;
|
|
1714
|
+
wakeDisplay();
|
|
1613
1715
|
const result = runAppleScript(
|
|
1614
1716
|
buildReplyScript({ messageId: resolved.messageId, body: body.text, replyAll, sendNow }),
|
|
1615
1717
|
{ timeout: MAIL_FIND_AND_SEND_TIMEOUT_MS, appName: "Mail" }
|
|
1616
1718
|
);
|
|
1617
|
-
return
|
|
1719
|
+
return finishMailGuiWrite({
|
|
1618
1720
|
action,
|
|
1619
1721
|
summary,
|
|
1620
1722
|
result,
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1723
|
+
indexerMode,
|
|
1724
|
+
finalize: () => finalizeMailWrite({
|
|
1725
|
+
action,
|
|
1726
|
+
summary,
|
|
1727
|
+
result,
|
|
1728
|
+
secrets: [body.text],
|
|
1729
|
+
sendNow,
|
|
1730
|
+
inReplyTo: sendNow ? resolved.messageId : null,
|
|
1731
|
+
successSummary: sendNow ? "reply sent" : "reply saved to Drafts",
|
|
1732
|
+
details: {
|
|
1733
|
+
message_id: resolved.messageId,
|
|
1734
|
+
reply_all: String(replyAll)
|
|
1735
|
+
},
|
|
1736
|
+
deadlineMs
|
|
1737
|
+
})
|
|
1630
1738
|
});
|
|
1631
1739
|
}
|
|
1632
1740
|
|
|
1633
1741
|
export function buildForwardScript({ messageId, to, body, sendNow }) {
|
|
1634
|
-
|
|
1742
|
+
const mailSecs = asInteger(MAIL_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 60, field: "mailTimeout" });
|
|
1743
|
+
const fillBlock = quoteBodyPasteAndVerify("theForward", body);
|
|
1744
|
+
const pasteAndVerify = quotePasteGuard("theForward", fillBlock);
|
|
1745
|
+
const handlers = fillBlock ? `${buildMailBodyPasteHandler()}\n` : "";
|
|
1746
|
+
|
|
1747
|
+
return `${handlers}${findMessageHandler()}
|
|
1635
1748
|
|
|
1636
1749
|
set theMessage to atmFindMessage(${asString(messageId)})
|
|
1637
1750
|
tell application "Mail"
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1751
|
+
with timeout of ${mailSecs} seconds
|
|
1752
|
+
set theForward to missing value
|
|
1753
|
+
try
|
|
1754
|
+
set theForward to forward theMessage with opening window
|
|
1755
|
+
end try
|
|
1756
|
+
if theForward is missing value then error "FORWARD_UNSUPPORTED"
|
|
1757
|
+
tell theForward
|
|
1645
1758
|
${to.map((address) => ` make new to recipient at end of to recipients with properties {address:${asString(address)}}`).join("\n")}
|
|
1646
|
-
|
|
1647
|
-
|
|
1759
|
+
end tell
|
|
1760
|
+
activate
|
|
1761
|
+
end timeout
|
|
1762
|
+
end tell
|
|
1763
|
+
${pasteAndVerify}tell application "Mail"
|
|
1764
|
+
with timeout of ${mailSecs} seconds
|
|
1765
|
+
${sendNow ? "send theForward" : "save theForward"}
|
|
1766
|
+
end timeout
|
|
1648
1767
|
end tell
|
|
1649
1768
|
return "OK"`;
|
|
1650
1769
|
}
|
|
1651
1770
|
|
|
1652
|
-
export function mailForward(args = {}) {
|
|
1771
|
+
export function mailForward(args = {}, { indexerMode = isIndexerMode() } = {}) {
|
|
1653
1772
|
const action = "mail_forward";
|
|
1654
1773
|
const resolved = resolveMailMessageId({ messageId: args.message_id, filePath: args.file_path });
|
|
1655
1774
|
if (resolved.error) return { ok: false, message: `${action} refused: ${resolved.error}` };
|
|
@@ -1674,25 +1793,39 @@ export function mailForward(args = {}) {
|
|
|
1674
1793
|
});
|
|
1675
1794
|
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1676
1795
|
|
|
1796
|
+
if (quoteIntroPasteText(body.text)) {
|
|
1797
|
+
const se = probeSystemEventsGui();
|
|
1798
|
+
if (!se.ok) {
|
|
1799
|
+
return guiScriptingUnavailableResult(action, summary, se, { indexerMode });
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1677
1803
|
const deadlineMs = sentVerifyClock.now() + MCP_CALL_DEADLINE_MS;
|
|
1804
|
+
wakeDisplay();
|
|
1678
1805
|
const result = runAppleScript(
|
|
1679
1806
|
buildForwardScript({ messageId: resolved.messageId, to: to.addresses, body: body.text, sendNow }),
|
|
1680
1807
|
{ timeout: MAIL_FIND_AND_SEND_TIMEOUT_MS, appName: "Mail" }
|
|
1681
1808
|
);
|
|
1682
|
-
return
|
|
1809
|
+
return finishMailGuiWrite({
|
|
1683
1810
|
action,
|
|
1684
1811
|
summary,
|
|
1685
1812
|
result,
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1813
|
+
indexerMode,
|
|
1814
|
+
finalize: () => finalizeMailWrite({
|
|
1815
|
+
action,
|
|
1816
|
+
summary,
|
|
1817
|
+
result,
|
|
1818
|
+
secrets: [body.text],
|
|
1819
|
+
sendNow,
|
|
1820
|
+
inReplyTo: sendNow ? resolved.messageId : null,
|
|
1821
|
+
forwardTo: sendNow ? to.addresses : null,
|
|
1822
|
+
successSummary: sendNow ? "forwarded" : "forward saved to Drafts",
|
|
1823
|
+
details: {
|
|
1824
|
+
message_id: resolved.messageId,
|
|
1825
|
+
to: to.addresses.join(", ")
|
|
1826
|
+
},
|
|
1827
|
+
deadlineMs
|
|
1828
|
+
})
|
|
1696
1829
|
});
|
|
1697
1830
|
}
|
|
1698
1831
|
|
package/lib/writeTools.js
CHANGED
|
@@ -82,12 +82,12 @@ 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 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.",
|
|
85
|
+
description: "Reply to an existing email. The reply text is pasted above the quoted original in a visible Mail window (System Events focus on the message body, then Command-Up and clipboard paste — not Cmd-A, which would replace the quote; AppleScript content is not set because Mail 16 leaves it empty). 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: {
|
|
89
89
|
...MESSAGE_TARGET_PROPS,
|
|
90
|
-
body: { type: "string", description: "Reply text, prepended above the quoted original (required)" },
|
|
90
|
+
body: { type: "string", description: "Reply text, prepended above the quoted original via System Events paste (required). AppleScript content is not set." },
|
|
91
91
|
reply_all: { type: "boolean", description: "Reply to all original recipients (default false)" },
|
|
92
92
|
save_as_draft: { type: "boolean", description: "Save the reply to Drafts instead of sending (default false)" },
|
|
93
93
|
...CONFIRM_PROPS
|
|
@@ -97,13 +97,13 @@ 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 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.",
|
|
100
|
+
description: "Forward an existing email to new recipients. An optional note is pasted above the forwarded original in a visible Mail window (same System Events prepend as mail_reply; AppleScript content is not set). 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: {
|
|
104
104
|
...MESSAGE_TARGET_PROPS,
|
|
105
105
|
to: { type: "array", items: { type: "string" }, description: "Forward recipients (required, at least one)" },
|
|
106
|
-
body: { type: "string", description: "Optional note
|
|
106
|
+
body: { type: "string", description: "Optional note pasted above the forwarded message (System Events prepend; omitted note skips paste)" },
|
|
107
107
|
save_as_draft: { type: "boolean", description: "Save the forward to Drafts instead of sending (default false)" },
|
|
108
108
|
...CONFIRM_PROPS
|
|
109
109
|
},
|
|
@@ -420,8 +420,11 @@ export function loggedBridgeHandler(run, log, now = () => Date.now()) {
|
|
|
420
420
|
const ok = Boolean(result && result.ok);
|
|
421
421
|
// Dry runs and confirm-gated previews come back ok:false with planned:true — nothing failed.
|
|
422
422
|
const planned = !ok && Boolean(result && result.planned);
|
|
423
|
-
|
|
424
|
-
|
|
423
|
+
// Mail accepted the send but Sent/Outbox didn't show it within budget — not a failure.
|
|
424
|
+
const unconfirmed = !ok && !planned && Boolean(result && result.unconfirmed);
|
|
425
|
+
const outcome = ok ? "ok" : planned ? "planned (dry run / needs confirm)" : unconfirmed ? "unconfirmed (sent; not yet seen in Sent/Outbox)" : "failed";
|
|
426
|
+
const why = ok || planned || unconfirmed ? "" : ` (${classifyWriteFailure(result && result.message)}${result && result.unsupported ? ", unsupported" : ""})`;
|
|
427
|
+
log(`${new Date(started).toISOString()} write bridge: ${tool} ${outcome} in ${now() - started}ms${why}`);
|
|
425
428
|
return result;
|
|
426
429
|
};
|
|
427
430
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-tools-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.8",
|
|
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",
|