safari-mcp 2.11.6 → 2.11.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/package.json +1 -1
- package/safari.js +65 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.8",
|
|
4
4
|
"mcpName": "io.github.achiya-automation/safari-mcp",
|
|
5
5
|
"description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 80 tools via AppleScript + JavaScript.",
|
|
6
6
|
"type": "module",
|
package/safari.js
CHANGED
|
@@ -279,8 +279,11 @@ if (SAFARI_PROFILE) {
|
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
281
|
try {
|
|
282
|
+
// Read-only window-name query — opt out of focus-guard so a rare
|
|
283
|
+
// user-app-switch race never triggers the hide-fallback against them.
|
|
282
284
|
const name = await osascriptFast(
|
|
283
|
-
`tell application "Safari" to return name of ${_targetWindowRef}
|
|
285
|
+
`tell application "Safari" to return name of ${_targetWindowRef}`,
|
|
286
|
+
{ noFocusGuard: true }
|
|
284
287
|
).catch(() => '');
|
|
285
288
|
if (name && !name.startsWith(`${SAFARI_PROFILE} \u2014`)) {
|
|
286
289
|
// Window ID no longer belongs to profile — invalidate cache immediately
|
|
@@ -394,17 +397,35 @@ async function _stampTab(idx) {
|
|
|
394
397
|
export async function runJSQuick(js) { return runJS(js); }
|
|
395
398
|
|
|
396
399
|
// ========== FOCUS PRESERVATION ==========
|
|
397
|
-
// Safari AppleScript can steal focus (bring Safari window to front)
|
|
398
|
-
//
|
|
400
|
+
// Safari AppleScript can steal focus (bring Safari window to front), especially
|
|
401
|
+
// on macOS Tahoe where window-mutation commands trigger an implicit activate.
|
|
402
|
+
// Strategy: 1) read frontmost from daemon (~0.1ms), 2) try to re-activate previous
|
|
403
|
+
// app, 3) settle 5ms (Tahoe needs time to honor the activate), 4) verify, and
|
|
404
|
+
// 5) fall back to hiding Safari if activate didn't take.
|
|
399
405
|
export async function saveFrontmostApp() {
|
|
400
406
|
const app = await _helperGetFrontApp();
|
|
401
407
|
return app?.bundleId || null;
|
|
402
408
|
}
|
|
403
409
|
export async function restoreFocusIfStolen(savedBundleId) {
|
|
404
410
|
if (!savedBundleId || savedBundleId === "com.apple.Safari") return;
|
|
405
|
-
|
|
411
|
+
let current = await _helperGetFrontApp();
|
|
412
|
+
if (current?.bundleId !== "com.apple.Safari") return;
|
|
413
|
+
|
|
414
|
+
// Primary: bring previous app back. Await so the caller doesn't return
|
|
415
|
+
// control to user-space while Safari is still frontmost.
|
|
416
|
+
await _helperActivateApp(savedBundleId).catch(() => {});
|
|
417
|
+
|
|
418
|
+
// Tahoe settle window — NSRunningApplication.activate() is async at the OS
|
|
419
|
+
// level; without this brief wait the verify check below races and reports a
|
|
420
|
+
// false success.
|
|
421
|
+
await new Promise(r => setTimeout(r, 5));
|
|
422
|
+
|
|
423
|
+
current = await _helperGetFrontApp();
|
|
406
424
|
if (current?.bundleId === "com.apple.Safari") {
|
|
407
|
-
|
|
425
|
+
// Activate did not take effect (Safari's window-server policy can block it
|
|
426
|
+
// under Tahoe). Hide Safari as a last-resort fallback — the OS auto-picks
|
|
427
|
+
// the next app, which is reliably the one we saved.
|
|
428
|
+
await _helperHideSafari().catch(() => {});
|
|
408
429
|
}
|
|
409
430
|
}
|
|
410
431
|
|
|
@@ -590,40 +611,52 @@ async function osascript(script, { timeout = 10000 } = {}) {
|
|
|
590
611
|
}
|
|
591
612
|
throw new Error(`AppleScript error: ${err.stderr || err.message}`);
|
|
592
613
|
} finally {
|
|
593
|
-
//
|
|
614
|
+
// Awaited restore — caller must not return to user-space while Safari is still frontmost.
|
|
594
615
|
if (shouldGuardFocus && frontApp?.bundleId && frontApp.bundleId !== 'com.apple.Safari') {
|
|
595
|
-
|
|
596
|
-
if (current?.bundleId === 'com.apple.Safari') {
|
|
597
|
-
_helperActivateApp(frontApp.bundleId).catch(() => {});
|
|
598
|
-
}
|
|
616
|
+
await restoreFocusIfStolen(frontApp.bundleId).catch(() => {});
|
|
599
617
|
}
|
|
600
618
|
}
|
|
601
619
|
}
|
|
602
620
|
|
|
603
621
|
// osascriptFast: uses persistent Swift daemon (~5ms) — 18x faster than subprocess (~90ms)
|
|
604
|
-
async function osascriptFast(script, { timeout = 10000 } = {}) {
|
|
622
|
+
async function osascriptFast(script, { timeout = 10000, noFocusGuard = false } = {}) {
|
|
605
623
|
if (!(await isSafariRunning())) throw safariNotRunningError();
|
|
606
624
|
if (!_helperProc) startHelper();
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
625
|
+
|
|
626
|
+
// Focus guard — Tahoe AppleScript can implicitly activate Safari (especially
|
|
627
|
+
// on window-mutating commands: set URL / set bounds / set current tab).
|
|
628
|
+
// Skip if an outer caller (extensionOrFallback / runJSLarge / osascript)
|
|
629
|
+
// already handles focus, or if the caller knows the script is read-only and
|
|
630
|
+
// not worth the round-trip overhead (e.g. background polling every 3s).
|
|
631
|
+
const shouldGuardFocus = !_focusGuardActive && !noFocusGuard;
|
|
632
|
+
const frontApp = shouldGuardFocus ? await _helperGetFrontApp() : null;
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
if (_helperProc) {
|
|
636
|
+
try {
|
|
637
|
+
return await _osascriptFastHelper(script, timeout);
|
|
638
|
+
} catch (err) {
|
|
639
|
+
// Retry once if the window ID became stale
|
|
640
|
+
if (isStaleWindowError(err) && SAFARI_PROFILE) {
|
|
641
|
+
const oldRef = _targetWindowRef;
|
|
642
|
+
await refreshTargetWindow(true);
|
|
643
|
+
if (_targetWindowRef !== oldRef) {
|
|
644
|
+
const retryScript = script.replace(new RegExp(oldRef.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), _targetWindowRef);
|
|
645
|
+
// Guard: helper may have died during the stale-window retry
|
|
646
|
+
if (!_helperProc) startHelper();
|
|
647
|
+
if (_helperProc) return await _osascriptFastHelper(retryScript, timeout);
|
|
648
|
+
return await osascript(retryScript, { timeout });
|
|
649
|
+
}
|
|
621
650
|
}
|
|
651
|
+
throw err;
|
|
622
652
|
}
|
|
623
|
-
|
|
653
|
+
}
|
|
654
|
+
return await osascript(script, { timeout });
|
|
655
|
+
} finally {
|
|
656
|
+
if (shouldGuardFocus && frontApp?.bundleId && frontApp.bundleId !== "com.apple.Safari") {
|
|
657
|
+
await restoreFocusIfStolen(frontApp.bundleId).catch(() => {});
|
|
624
658
|
}
|
|
625
659
|
}
|
|
626
|
-
return osascript(script, { timeout });
|
|
627
660
|
}
|
|
628
661
|
|
|
629
662
|
function _osascriptFastHelper(script, timeout) {
|
|
@@ -972,12 +1005,9 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
|
|
|
972
1005
|
return stdout.trim();
|
|
973
1006
|
} finally {
|
|
974
1007
|
unlink(tmpFile).catch(() => {});
|
|
975
|
-
//
|
|
1008
|
+
// Awaited restore — caller must not return to user-space while Safari is still frontmost.
|
|
976
1009
|
if (shouldGuard && frontApp?.bundleId && frontApp.bundleId !== 'com.apple.Safari') {
|
|
977
|
-
|
|
978
|
-
if (current?.bundleId === 'com.apple.Safari') {
|
|
979
|
-
_helperActivateApp(frontApp.bundleId).catch(() => {});
|
|
980
|
-
}
|
|
1010
|
+
await restoreFocusIfStolen(frontApp.bundleId).catch(() => {});
|
|
981
1011
|
}
|
|
982
1012
|
}
|
|
983
1013
|
}
|
|
@@ -2685,12 +2715,10 @@ export async function screenshot({ fullPage = false } = {}) {
|
|
|
2685
2715
|
);
|
|
2686
2716
|
}
|
|
2687
2717
|
}
|
|
2688
|
-
// Re-activate previous app if screencapture stole focus (common on macOS Tahoe)
|
|
2718
|
+
// Re-activate previous app if screencapture stole focus (common on macOS Tahoe).
|
|
2719
|
+
// Centralized restore handles settle delay + hide fallback if activate is blocked.
|
|
2689
2720
|
if (previousBundleId && previousBundleId !== "com.apple.Safari") {
|
|
2690
|
-
|
|
2691
|
-
if (cur?.bundleId === "com.apple.Safari") {
|
|
2692
|
-
await _helperActivateApp(previousBundleId).catch(() => {});
|
|
2693
|
-
}
|
|
2721
|
+
await restoreFocusIfStolen(previousBundleId).catch(() => {});
|
|
2694
2722
|
}
|
|
2695
2723
|
// Compress: convert PNG to JPEG (50% quality) + resize to max 1200px width
|
|
2696
2724
|
// Cuts ~600KB PNG → ~60KB JPEG — critical for staying under 20MB context limit
|