safari-mcp 2.10.9 → 2.10.10
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 +28 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.10",
|
|
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
|
@@ -216,9 +216,14 @@ async function refreshTargetWindow(force = false) {
|
|
|
216
216
|
if (!force && _targetWindowRef && (now - _targetWindowCacheTime) < TARGET_WINDOW_CACHE_MS) return;
|
|
217
217
|
const safeProfile = SAFARI_PROFILE.replace(/"/g, '\\"');
|
|
218
218
|
// Find profile window by name AND verify the window ID still matches
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
const detectScript = `tell application "Safari"\n repeat with w in every window\n if name of w starts with "${safeProfile} \u2014" then return (id of w as text) & "|" & name of w\n end repeat\n return "0|"\nend tell`;
|
|
220
|
+
let result = await osascriptFast(detectScript).catch(() => '0|');
|
|
221
|
+
// The persistent helper occasionally returns '0|' for a window that genuinely
|
|
222
|
+
// exists (daemon timeout / restart race). Before concluding the window is
|
|
223
|
+
// missing, retry once with a plain osascript subprocess \u2014 slower but reliable.
|
|
224
|
+
if (String(result).split('|')[0] === '0') {
|
|
225
|
+
result = await osascript(detectScript).catch(() => '0|');
|
|
226
|
+
}
|
|
222
227
|
const [idStr, windowName] = String(result).split('|');
|
|
223
228
|
const id = Number(idStr);
|
|
224
229
|
if (id > 0) {
|
|
@@ -243,7 +248,12 @@ async function refreshTargetWindow(force = false) {
|
|
|
243
248
|
// Background verification: periodically check that cached window ID still belongs to profile
|
|
244
249
|
if (SAFARI_PROFILE) {
|
|
245
250
|
setInterval(async () => {
|
|
246
|
-
|
|
251
|
+
// No cached window (e.g. flaky detection at startup) — keep trying to
|
|
252
|
+
// rediscover it so the server self-heals instead of staying stuck.
|
|
253
|
+
if (!_targetWindowRef || !_targetWindowId) {
|
|
254
|
+
await refreshTargetWindow(true).catch(() => {});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
247
257
|
try {
|
|
248
258
|
const name = await osascriptFast(
|
|
249
259
|
`tell application "Safari" to return name of ${_targetWindowRef}`
|
|
@@ -374,15 +384,17 @@ async function resolveActiveTab() {
|
|
|
374
384
|
|
|
375
385
|
// Strategy 1: window.__mcpTabMarker (bulletproof — survives navigation, redirects, query changes)
|
|
376
386
|
// Only attempted if we have a marker AND a cached index hint
|
|
377
|
-
if (_activeTabMarker
|
|
387
|
+
if (_activeTabMarker) {
|
|
378
388
|
try {
|
|
379
389
|
const safeMarker = _activeTabMarker.replace(/'/g, "\\'");
|
|
380
390
|
// Single JS call: check cached index first, then walk all tabs to find marker
|
|
381
391
|
const checkScript = `(function(){return window.__mcpTabMarker==='${safeMarker}'?'1':'0'})()`;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
392
|
+
if (_activeTabIndex) {
|
|
393
|
+
const matchAtCached = await osascriptFast(
|
|
394
|
+
`tell application "Safari" to do JavaScript "${checkScript}" in tab ${_activeTabIndex} of ${getTargetWindowRef()}`
|
|
395
|
+
).catch(() => '0');
|
|
396
|
+
if (String(matchAtCached).trim() === '1') return _activeTabIndex;
|
|
397
|
+
}
|
|
386
398
|
// Cached index doesn't match — scan all tabs for marker
|
|
387
399
|
const tabCountStr = await osascriptFast(
|
|
388
400
|
`tell application "Safari" to return count of tabs of ${getTargetWindowRef()}`
|
|
@@ -764,13 +776,18 @@ async function runJS(js, { tabIndex, timeout = 15000 } = {}) {
|
|
|
764
776
|
if (!idx && _activeTabIndex && _activeTabURL && (Date.now() - _lastResolveTime < RESOLVE_CACHE_MS)) {
|
|
765
777
|
// Recently verified and tab count unchanged — use cached index
|
|
766
778
|
idx = _activeTabIndex;
|
|
767
|
-
} else if (!idx && _activeTabURL
|
|
768
|
-
// Resolve by URL —
|
|
779
|
+
} else if (!idx && (_activeTabURL || _activeTabMarker)) {
|
|
780
|
+
// Resolve by URL or marker — the marker scan still finds an owned tab
|
|
781
|
+
// even after _activeTabURL has been cleared by a failed URL lookup.
|
|
769
782
|
const resolved = await resolveActiveTab();
|
|
770
783
|
if (resolved) { idx = resolved; _lastResolveTime = Date.now(); }
|
|
771
784
|
}
|
|
772
785
|
// ALWAYS fall back to _activeTabIndex — never clear it from resolve failures
|
|
773
786
|
if (!idx) idx = _activeTabIndex;
|
|
787
|
+
// Once this session owns a tab, never silently run on the user's current tab.
|
|
788
|
+
if (!idx && _hasOwnedTab && SAFARI_PROFILE) {
|
|
789
|
+
throw new Error('Tab tracking lost during runJS — refusing to target the user\'s current tab. Call safari_new_tab to reopen.');
|
|
790
|
+
}
|
|
774
791
|
const target = idx
|
|
775
792
|
? `tab ${idx} of ${getTargetWindowRef()}`
|
|
776
793
|
: getFallbackTarget();
|