safari-mcp 2.10.0 → 2.10.1
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/extension/background.js +8 -2
- package/extension/content.js +21 -4
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/safari.js +35 -0
package/extension/background.js
CHANGED
|
@@ -342,9 +342,15 @@ async function handleCommand(type, payload) {
|
|
|
342
342
|
window[id] = { done: false };
|
|
343
343
|
const s = document.createElement("script");
|
|
344
344
|
const code = "try{var __r=(function(){" + script + "})();if(__r&&typeof __r.then==='function'){__r.then(function(v){window['" + id + "']={done:true,v:v};}).catch(function(e){window['" + id + "']={done:true,e:e.message};});}else{window['" + id + "']={done:true,v:__r};}}catch(e){window['" + id + "']={done:true,e:e.message};}";
|
|
345
|
-
|
|
345
|
+
// Prefer the policy pre-registered by content.js at document_start —
|
|
346
|
+
// pages that block new policy creation post-load (GSC, modern Google admin)
|
|
347
|
+
// still accept ours because it was grandfathered in before their CSP applied.
|
|
348
|
+
if (window.__mcpTrustedPolicy && typeof window.__mcpTrustedPolicy.createScript === "function") {
|
|
349
|
+
try { s.textContent = window.__mcpTrustedPolicy.createScript(code); }
|
|
350
|
+
catch (_) { s.textContent = code; }
|
|
351
|
+
} else if (window.trustedTypes && window.trustedTypes.createPolicy) {
|
|
346
352
|
try {
|
|
347
|
-
const policy = window.trustedTypes.createPolicy("
|
|
353
|
+
const policy = window.trustedTypes.createPolicy("mcpEval_" + Date.now(), { createScript: (s) => s });
|
|
348
354
|
s.textContent = policy.createScript(code);
|
|
349
355
|
} catch (_) { s.textContent = code; }
|
|
350
356
|
} else {
|
package/extension/content.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
// Content script — runs at document_start in MAIN world (before page scripts)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Content script — runs at document_start in MAIN world (before page scripts).
|
|
2
|
+
// Two responsibilities:
|
|
3
|
+
// 1. Monkey-patch attachShadow to capture CLOSED shadow roots (Reddit, etc.).
|
|
4
|
+
// 2. Pre-register a Trusted Types policy named "mcpBridge" BEFORE the page sets
|
|
5
|
+
// its own require-trusted-types-for directive. Our policy is then grandfathered
|
|
6
|
+
// and survives even on pages (Google Search Console, Google admin, modern banks)
|
|
7
|
+
// that block new policy creation after page load. MCP evaluate strategies
|
|
8
|
+
// consult `window.__mcpTrustedPolicy` first.
|
|
5
9
|
// Runs in MAIN world via manifest "world": "MAIN" — no script injection needed,
|
|
6
10
|
// so CSP cannot block it.
|
|
7
11
|
|
|
@@ -21,3 +25,16 @@ if (!window.__mcpShadowPatched) {
|
|
|
21
25
|
return el.shadowRoot || _closedRoots.get(el) || null;
|
|
22
26
|
};
|
|
23
27
|
}
|
|
28
|
+
|
|
29
|
+
if (!window.__mcpTrustedPolicy && window.trustedTypes && typeof window.trustedTypes.createPolicy === "function") {
|
|
30
|
+
try {
|
|
31
|
+
window.__mcpTrustedPolicy = window.trustedTypes.createPolicy("mcpBridge", {
|
|
32
|
+
createScript: function (s) { return s; },
|
|
33
|
+
createScriptURL: function (s) { return s; },
|
|
34
|
+
createHTML: function (s) { return s; }
|
|
35
|
+
});
|
|
36
|
+
} catch (_e) {
|
|
37
|
+
// Page already restricts policies — rare since content script runs at document_start
|
|
38
|
+
// before page scripts. Leave undefined; evaluate fallbacks will probe other paths.
|
|
39
|
+
}
|
|
40
|
+
}
|
package/extension/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Safari MCP Bridge",
|
|
4
4
|
"description": "Connects AI agents to Safari — fast JS execution, screenshots, clicks with your real cookies/logins",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.10.1",
|
|
6
6
|
"icons": {
|
|
7
7
|
"48": "images/icon-48.png",
|
|
8
8
|
"96": "images/icon-96.png",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.1",
|
|
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
|
@@ -158,6 +158,14 @@ async function _restoreClipboard(savedContent) {
|
|
|
158
158
|
// to the current index.
|
|
159
159
|
let _activeTabIndex = null; // null = use front document (default)
|
|
160
160
|
let _activeTabURL = null; // URL-based tracking (stable even when tabs shift)
|
|
161
|
+
// Timestamp of most recent new_tab — within this grace window we MUST NOT fall
|
|
162
|
+
// back to "current tab of window" (which is the USER'S active tab). New tabs
|
|
163
|
+
// often start at about:blank if their URL fails to load (file://, blocked, etc.),
|
|
164
|
+
// and the URL-based marker resolution can fail until the page actually loads.
|
|
165
|
+
// During the grace window, navigate/click/fill operations either use the cached
|
|
166
|
+
// _activeTabIndex or fail loudly — never silently target the user's tab.
|
|
167
|
+
let _lastNewTabAt = 0;
|
|
168
|
+
const NEW_TAB_GRACE_MS = 30000;
|
|
161
169
|
let _lastResolveTime = 0; // Cache: skip resolve if verified recently
|
|
162
170
|
let _lastTabCount = null; // Track tab count for smart cache invalidation
|
|
163
171
|
let _activeTabMarker = null; // window.__mcpTabMarker — survives same-tab navigation, bulletproof tab identity
|
|
@@ -272,6 +280,20 @@ function isStaleWindowError(err) {
|
|
|
272
280
|
|
|
273
281
|
// Safe fallback target: when no tab index is known, use the profile window's current tab
|
|
274
282
|
// instead of "front document" which can target the user's personal profile window
|
|
283
|
+
// Throw if a write operation is about to fall back to the user's active tab
|
|
284
|
+
// during the new-tab grace window. Without this, navigate/fill/click silently
|
|
285
|
+
// target whatever tab the user is looking at when our cached index is lost.
|
|
286
|
+
function _assertNotFallingBackToUserTab(opName) {
|
|
287
|
+
if (_activeTabIndex) return; // we have a tracked index — fine
|
|
288
|
+
const sinceNew = Date.now() - _lastNewTabAt;
|
|
289
|
+
if (sinceNew < NEW_TAB_GRACE_MS) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Tab tracking lost within ${Math.round(sinceNew/1000)}s of safari_new_tab — refusing to ${opName} via fallback (would target the user's active tab). ` +
|
|
292
|
+
`Re-run safari_new_tab and retry.`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
275
297
|
function getFallbackTarget() {
|
|
276
298
|
return SAFARI_PROFILE ? `current tab of ${getTargetWindowRef()}` : "front document";
|
|
277
299
|
}
|
|
@@ -826,6 +848,7 @@ export async function navigate(url) {
|
|
|
826
848
|
const safeUrl = targetUrl.replace(/"/g, '\\"');
|
|
827
849
|
// Resolve tab by URL first (in case indices shifted)
|
|
828
850
|
if (_activeTabURL) await resolveActiveTab();
|
|
851
|
+
_assertNotFallingBackToUserTab('navigate');
|
|
829
852
|
const navTarget = _activeTabIndex
|
|
830
853
|
? `tab ${_activeTabIndex} of ${getTargetWindowRef()}`
|
|
831
854
|
: getFallbackTarget();
|
|
@@ -904,6 +927,17 @@ export async function navigate(url) {
|
|
|
904
927
|
}
|
|
905
928
|
_lastResolveTime = Date.now();
|
|
906
929
|
|
|
930
|
+
// Re-inject the tab marker — navigation creates a new JS context which wipes the
|
|
931
|
+
// window.__mcpTabMarker we set in newTab. Without re-injection, resolveActiveTab's
|
|
932
|
+
// marker-based search fails on the very next call and falls back to URL search,
|
|
933
|
+
// which can mis-target if the tab is still loading or shows an error page.
|
|
934
|
+
if (_activeTabIndex && _activeTabMarker) {
|
|
935
|
+
const safeMarker = _activeTabMarker.replace(/'/g, "\\'");
|
|
936
|
+
await osascriptFast(
|
|
937
|
+
`tell application "Safari" to do JavaScript "window.__mcpTabMarker='${safeMarker}'" in tab ${_activeTabIndex} of ${getTargetWindowRef()}`
|
|
938
|
+
).catch(() => {});
|
|
939
|
+
}
|
|
940
|
+
|
|
907
941
|
return result;
|
|
908
942
|
}
|
|
909
943
|
|
|
@@ -2543,6 +2577,7 @@ export async function newTab(url = "") {
|
|
|
2543
2577
|
_activeTabURL = url || null;
|
|
2544
2578
|
_lastResolveTime = Date.now();
|
|
2545
2579
|
_lastTabCount = Number(tabCount); // Update tab count cache
|
|
2580
|
+
_lastNewTabAt = Date.now(); // Start grace window — block fallback to user's tab
|
|
2546
2581
|
// Set bulletproof tab marker — survives same-tab navigation, redirects, query changes (v2.8.3)
|
|
2547
2582
|
_activeTabMarker = `MCP_${SESSION_ID}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2548
2583
|
await osascriptFast(
|