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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/safari.js +28 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.10.9",
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 result = await osascriptFast(
220
- `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`
221
- ).catch(() => '0|');
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
- if (!_targetWindowRef || !_targetWindowId) return;
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 && _activeTabIndex) {
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
- const matchAtCached = await osascriptFast(
383
- `tell application "Safari" to do JavaScript "${checkScript}" in tab ${_activeTabIndex} of ${getTargetWindowRef()}`
384
- ).catch(() => '0');
385
- if (String(matchAtCached).trim() === '1') return _activeTabIndex;
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 && _activeTabURL !== '') {
768
- // Resolve by URL — verify index is still correct
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();