apple-tools-mcp 3.0.2 → 3.0.4
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/mailWrite.js +48 -0
- package/lib/shell.js +23 -1
- package/package.json +1 -1
package/lib/mailWrite.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
isHardTccDenial
|
|
32
32
|
} from "./appleScript.js";
|
|
33
33
|
import { isIndexerMode } from "./processMode.js";
|
|
34
|
+
import { safeSpawnDetached } from "./shell.js";
|
|
34
35
|
import {
|
|
35
36
|
planWrite,
|
|
36
37
|
plannedWriteResult,
|
|
@@ -160,6 +161,30 @@ export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
|
|
|
160
161
|
export const BODY_FOCUS_FAILED_SENTINEL = "BODY_FOCUS_FAILED";
|
|
161
162
|
/** Focus request ran, but the re-read AXFocusedUIElement was not the chosen body. */
|
|
162
163
|
export const BODY_FOCUS_UNPROVEN_SENTINEL = "BODY_FOCUS_UNPROVEN";
|
|
164
|
+
/**
|
|
165
|
+
* System Events sees 0 Mail windows while Mail AppleScript still lists the compose:
|
|
166
|
+
* the screen saver is up (loginwindow owns the screen) or the display is asleep.
|
|
167
|
+
*/
|
|
168
|
+
export const DISPLAY_ASLEEP_GUI_UNAVAILABLE_SENTINEL = "DISPLAY_ASLEEP_GUI_UNAVAILABLE";
|
|
169
|
+
/** Outlasts the whole compose osascript (MAIL_COMPOSE_TIMEOUT_MS) so the assertion is live while it runs. */
|
|
170
|
+
export const DISPLAY_WAKE_SECONDS = 30;
|
|
171
|
+
export const DISPLAY_WAKE_POLL_ATTEMPTS = 20;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Declare user activity so a sleeping display wakes before GUI scripting. caffeinate runs
|
|
175
|
+
* detached for its own -t: a blocking spawn would either SIGKILL it early (3.0.3's 5s timeout
|
|
176
|
+
* cut the 15s assertion at ~4s) or release it before compose starts. This does not dismiss a
|
|
177
|
+
* running screen saver; the compose script stops that itself. Best-effort; never throws.
|
|
178
|
+
*/
|
|
179
|
+
export function wakeDisplay({ spawnDetached = safeSpawnDetached, platform = process.platform } = {}) {
|
|
180
|
+
if (platform !== "darwin" || (process.env.VITEST && spawnDetached === safeSpawnDetached)) return false;
|
|
181
|
+
try {
|
|
182
|
+
return spawnDetached("/usr/bin/caffeinate", ["-u", "-t", String(DISPLAY_WAKE_SECONDS)]) != null;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
163
188
|
/** Mail To: recipient chips surface as U+FFFC (object replacement), not body text. */
|
|
164
189
|
export const MAIL_RECIPIENT_TOKEN_CHAR_ID = 65532;
|
|
165
190
|
export const GUI_SCRIPTING_UNAVAILABLE_SENTINEL = "GUI_SCRIPTING_UNAVAILABLE";
|
|
@@ -618,6 +643,16 @@ on atmAxValueIsRecipientToken(atmVal)
|
|
|
618
643
|
return false
|
|
619
644
|
end atmAxValueIsRecipientToken
|
|
620
645
|
|
|
646
|
+
-- A running screen saver keeps loginwindow in front and hides every Mail window from
|
|
647
|
+
-- System Events; caffeinate's user-activity assertion does not dismiss it.
|
|
648
|
+
on atmStopScreenSaver()
|
|
649
|
+
try
|
|
650
|
+
tell application "System Events"
|
|
651
|
+
if running of screen saver preferences then stop current screen saver
|
|
652
|
+
end tell
|
|
653
|
+
end try
|
|
654
|
+
end atmStopScreenSaver
|
|
655
|
+
|
|
621
656
|
on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
622
657
|
try
|
|
623
658
|
tell application "Mail" to activate
|
|
@@ -628,8 +663,15 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
|
628
663
|
set atmSaveBh to -1
|
|
629
664
|
with timeout of ${seSecs} seconds
|
|
630
665
|
tell application "System Events"
|
|
666
|
+
my atmStopScreenSaver()
|
|
631
667
|
tell process "Mail"
|
|
632
668
|
set frontmost to true
|
|
669
|
+
repeat ${DISPLAY_WAKE_POLL_ATTEMPTS} times
|
|
670
|
+
if (count of (every window)) > 0 then exit repeat
|
|
671
|
+
my atmStopScreenSaver()
|
|
672
|
+
delay 0.5
|
|
673
|
+
end repeat
|
|
674
|
+
set atmSeWindowCount to count of (every window)
|
|
633
675
|
delay 0.3
|
|
634
676
|
set atmWin to missing value
|
|
635
677
|
repeat with atmCand in windows
|
|
@@ -658,6 +700,7 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
|
658
700
|
end if
|
|
659
701
|
end repeat
|
|
660
702
|
end if
|
|
703
|
+
if atmWin is missing value and atmSeWindowCount is 0 then error "${DISPLAY_ASLEEP_GUI_UNAVAILABLE_SENTINEL}"
|
|
661
704
|
if atmWin is missing value then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
662
705
|
try
|
|
663
706
|
perform action "AXRaise" of atmWin
|
|
@@ -744,6 +787,7 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
|
744
787
|
on error errMsg number errNum
|
|
745
788
|
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
746
789
|
if errMsg contains "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}" then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
790
|
+
if errMsg contains "${DISPLAY_ASLEEP_GUI_UNAVAILABLE_SENTINEL}" then error "${DISPLAY_ASLEEP_GUI_UNAVAILABLE_SENTINEL}"
|
|
747
791
|
error errMsg number errNum
|
|
748
792
|
end try
|
|
749
793
|
end atmFillMailBody`;
|
|
@@ -1292,6 +1336,9 @@ function failure(action, summary, result, secrets) {
|
|
|
1292
1336
|
if (err.includes("gui_scripting_unavailable")) {
|
|
1293
1337
|
return `${action} failed — attempted to ${summary}. ${MAIL_GUI_SCRIPTING_GUIDANCE}`;
|
|
1294
1338
|
}
|
|
1339
|
+
if (err.includes("display_asleep_gui_unavailable")) {
|
|
1340
|
+
return `${action} failed — attempted to ${summary}. The display is asleep or the screen is locked, so System Events cannot see Mail's compose window (GUI unavailable even after waking the display and stopping the screen saver); nothing was sent. Wake or unlock the Mac and retry.`;
|
|
1341
|
+
}
|
|
1295
1342
|
if (err.includes("body_focus_unproven")) {
|
|
1296
1343
|
return `${action} failed — attempted to ${summary}. Keyboard focus was not on the message body (focus proof failed); the draft was discarded and nothing was sent.`;
|
|
1297
1344
|
}
|
|
@@ -1480,6 +1527,7 @@ export function mailCompose(args = {}, { draft = false, indexerMode = isIndexerM
|
|
|
1480
1527
|
});
|
|
1481
1528
|
|
|
1482
1529
|
const deadlineMs = sentVerifyClock.now() + MCP_CALL_DEADLINE_MS;
|
|
1530
|
+
wakeDisplay();
|
|
1483
1531
|
const result = runAppleScript(script, { timeout: MAIL_COMPOSE_TIMEOUT_MS, appName: "Mail" });
|
|
1484
1532
|
if (
|
|
1485
1533
|
!result.ok &&
|
package/lib/shell.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Validating inputs before execution
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { spawnSync } from "child_process";
|
|
11
|
+
import { spawn, spawnSync } from "child_process";
|
|
12
12
|
import path from "path";
|
|
13
13
|
import { escapeSQL } from "./validators.js";
|
|
14
14
|
|
|
@@ -340,6 +340,28 @@ export function safeOpenApp(appName, options = {}) {
|
|
|
340
340
|
return String(result.stdout || "").trim();
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Start a command in the background and return immediately (no shell, no
|
|
345
|
+
* timeout, stdio ignored). The child is detached and unref'd, so it is never
|
|
346
|
+
* killed mid-run by a spawnSync timeout and never holds the caller open — for
|
|
347
|
+
* self-terminating helpers like `caffeinate -t N`. Returns the pid, or null if
|
|
348
|
+
* the spawn failed.
|
|
349
|
+
*/
|
|
350
|
+
export function safeSpawnDetached(command, args = []) {
|
|
351
|
+
if (!command || typeof command !== 'string') {
|
|
352
|
+
throw new Error('Command is required');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!Array.isArray(args)) {
|
|
356
|
+
throw new Error('Args must be an array');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore', shell: false });
|
|
360
|
+
child.on('error', () => {});
|
|
361
|
+
child.unref();
|
|
362
|
+
return child.pid ?? null;
|
|
363
|
+
}
|
|
364
|
+
|
|
343
365
|
export function safeSpawn(command, args = [], options = {}) {
|
|
344
366
|
const {
|
|
345
367
|
timeout = 30000,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-tools-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
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",
|