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.
- package/package.json +1 -1
- package/safari.js +45 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.8.
|
|
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
|
-
|
|
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 {
|