safari-mcp 2.8.2 → 2.8.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/safari.js +45 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
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
@@ -10,10 +10,14 @@ import { join, dirname, resolve as resolvePath } from "node:path";
10
10
  import { readFile, writeFile, unlink, appendFile } from "node:fs/promises";
11
11
  import { readFileSync } from "node:fs";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { randomUUID } from "node:crypto";
13
14
  // Extension bridge is handled by index.js (WebSocket server on port 9223)
14
15
 
15
16
  const execFileAsync = promisify(execFile);
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ // Local session ID — kept in sync with index.js SESSION_ID for tab marker generation
19
+ // Both files need their own const because they're separate ES modules; the marker only needs to be unique per process
20
+ const SESSION_ID = randomUUID().slice(0, 8);
17
21
 
18
22
  // ========== SWIFT HELPER DAEMON ==========
19
23
  // Persistent process — no subprocess spawn overhead (~5ms vs ~90ms)
@@ -156,7 +160,8 @@ let _activeTabIndex = null; // null = use front document (default)
156
160
  let _activeTabURL = null; // URL-based tracking (stable even when tabs shift)
157
161
  let _lastResolveTime = 0; // Cache: skip resolve if verified recently
158
162
  let _lastTabCount = null; // Track tab count for smart cache invalidation
159
- const RESOLVE_CACHE_MS = 500; // Brief cache — invalidated on tab count change
163
+ let _activeTabMarker = null; // window.__mcpTabMarker — survives same-tab navigation, bulletproof tab identity
164
+ const RESOLVE_CACHE_MS = 100; // Brief cache — was 500, reduced to catch tabs added by user/popups (v2.8.3 fix)
160
165
 
161
166
  // ========== DIAGNOSTIC LOG ==========
162
167
  // File-based log for profile/focus issues — survives MCP restart, visible to user
@@ -331,7 +336,41 @@ export function setActiveTabURL(url) { _activeTabURL = url; _lastResolveTime = D
331
336
 
332
337
  // Resolve our tracked URL to current tab index — single combined osascript call
333
338
  async function resolveActiveTab() {
339
+ if (!_activeTabURL && !_activeTabMarker) return _activeTabIndex;
340
+
341
+ // Strategy 1: window.__mcpTabMarker (bulletproof — survives navigation, redirects, query changes)
342
+ // Only attempted if we have a marker AND a cached index hint
343
+ if (_activeTabMarker && _activeTabIndex) {
344
+ try {
345
+ const safeMarker = _activeTabMarker.replace(/'/g, "\\'");
346
+ // Single JS call: check cached index first, then walk all tabs to find marker
347
+ const checkScript = `(function(){return window.__mcpTabMarker==='${safeMarker}'?'1':'0'})()`;
348
+ const matchAtCached = await osascriptFast(
349
+ `tell application "Safari" to do JavaScript "${checkScript}" in tab ${_activeTabIndex} of ${getTargetWindowRef()}`
350
+ ).catch(() => '0');
351
+ if (String(matchAtCached).trim() === '1') return _activeTabIndex;
352
+ // Cached index doesn't match — scan all tabs for marker
353
+ const tabCountStr = await osascriptFast(
354
+ `tell application "Safari" to return count of tabs of ${getTargetWindowRef()}`
355
+ ).catch(() => '0');
356
+ const tabCount = Number(tabCountStr) || 0;
357
+ for (let i = tabCount; i >= 1; i--) {
358
+ const m = await osascriptFast(
359
+ `tell application "Safari" to do JavaScript "${checkScript}" in tab ${i} of ${getTargetWindowRef()}`
360
+ ).catch(() => '0');
361
+ if (String(m).trim() === '1') {
362
+ _activeTabIndex = i;
363
+ _lastTabCount = tabCount;
364
+ return i;
365
+ }
366
+ }
367
+ // Marker not found anywhere — page reloaded or tab closed
368
+ _activeTabMarker = null;
369
+ } catch { /* fall through to URL strategy */ }
370
+ }
371
+
334
372
  if (!_activeTabURL) return _activeTabIndex;
373
+
335
374
  try {
336
375
  const safeUrl = _activeTabURL.replace(/"/g, '\\"');
337
376
  const domain = _activeTabURL.replace(/^https?:\/\//, '').split('/')[0].replace(/"/g, '\\"');
@@ -2112,6 +2151,11 @@ export async function newTab(url = "") {
2112
2151
  _activeTabURL = url || null;
2113
2152
  _lastResolveTime = Date.now();
2114
2153
  _lastTabCount = Number(tabCount); // Update tab count cache
2154
+ // Set bulletproof tab marker — survives same-tab navigation, redirects, query changes (v2.8.3)
2155
+ _activeTabMarker = `MCP_${SESSION_ID}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
2156
+ await osascriptFast(
2157
+ `tell application "Safari" to do JavaScript "window.__mcpTabMarker='${_activeTabMarker}'" in tab ${_activeTabIndex} of ${getTargetWindowRef()}`
2158
+ ).catch(() => {});
2115
2159
  // Wait for page load if URL given
2116
2160
  if (url) {
2117
2161
  try {