safari-mcp 2.15.3 → 2.15.5
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 +39 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.5",
|
|
4
4
|
"mcpName": "io.github.achiya-automation/safari-mcp",
|
|
5
5
|
"description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 96 tools via AppleScript + JavaScript.",
|
|
6
6
|
"type": "module",
|
package/safari.js
CHANGED
|
@@ -670,7 +670,7 @@ async function resolveActiveTab() {
|
|
|
670
670
|
// Parse result — can be "N" (found) or "0:tabCount" (not found)
|
|
671
671
|
const resultStr = String(result);
|
|
672
672
|
if (resultStr.includes(':')) {
|
|
673
|
-
// Not found —
|
|
673
|
+
// Not found — every exit below fails closed (drops the index); nothing here guesses.
|
|
674
674
|
const tabCount = Number(resultStr.split(':')[1]) || 1;
|
|
675
675
|
_st().lastTabCount = tabCount;
|
|
676
676
|
_st().activeTabURL = null;
|
|
@@ -682,8 +682,16 @@ async function resolveActiveTab() {
|
|
|
682
682
|
return null;
|
|
683
683
|
}
|
|
684
684
|
if (_st().activeTabIndex && _st().activeTabIndex > tabCount) {
|
|
685
|
-
|
|
686
|
-
|
|
685
|
+
// Our index is past the end of the window: the user closed a tab or tore one
|
|
686
|
+
// into its own window, so the index no longer names any tab — least of all ours.
|
|
687
|
+
// This used to clamp to `tabCount` ("tab ghost proactive fix"), which silently
|
|
688
|
+
// retargeted us at the LAST tab in the window — the user's. That predates the
|
|
689
|
+
// identity marker (clamp: Mar 31, marker: v2.8.3 Apr 14) and was the one exit
|
|
690
|
+
// here that still guessed. Fail closed like the two branches above; callers
|
|
691
|
+
// re-anchor via _assertNotFallingBackToUserTab's "re-run safari_new_tab" error.
|
|
692
|
+
console.error(`[Safari MCP] Tab identity lost (index ${_st().activeTabIndex} > tabCount ${tabCount}) — clearing index to avoid targeting the user's tab`);
|
|
693
|
+
_st().activeTabIndex = null;
|
|
694
|
+
return null;
|
|
687
695
|
}
|
|
688
696
|
return _st().activeTabIndex;
|
|
689
697
|
}
|
|
@@ -1169,6 +1177,18 @@ async function _withTargetTabFronted(fn) {
|
|
|
1169
1177
|
}
|
|
1170
1178
|
}
|
|
1171
1179
|
|
|
1180
|
+
// Atomic tab-identity guard, as a JS prefix. Belongs here and not at a call site: it is the
|
|
1181
|
+
// only check that is independent of how `idx` was resolved, so every path that builds a
|
|
1182
|
+
// targeted `do JavaScript` needs it — most of all the retry paths, which run precisely when
|
|
1183
|
+
// the index has already proven untrustworthy. Empty string when the caller named a tab
|
|
1184
|
+
// explicitly, or when this session owns no marked tab (front-document fallback is then
|
|
1185
|
+
// intentional). See #64.
|
|
1186
|
+
function _tabIdentityGuard(explicitTabIndex) {
|
|
1187
|
+
const marker = _st().activeTabMarker;
|
|
1188
|
+
if (!marker || explicitTabIndex) return '';
|
|
1189
|
+
return `if(window.name!=='${marker}'&&window.__mcpTabMarker!=='${marker}'){throw new Error('MCP_WRONG_TAB')};`;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1172
1192
|
// Run JavaScript in Safari — fastest path, no focus stealing
|
|
1173
1193
|
// Uses osascriptFast (persistent process, ~5ms) for short scripts,
|
|
1174
1194
|
// falls back to osascript (~80ms) for long scripts that exceed stdin limits
|
|
@@ -1215,9 +1235,7 @@ async function runJS(js, { tabIndex, timeout = 15000 } = {}) {
|
|
|
1215
1235
|
// does not carry OUR marker. Makes the op fail-closed at EXECUTION time regardless of how
|
|
1216
1236
|
// `idx` resolved — a drift to the user's tab (shared profile window) throws instead of
|
|
1217
1237
|
// running on their page. Skipped when the caller passed an explicit tabIndex.
|
|
1218
|
-
const _guard = (
|
|
1219
|
-
? `if(window.name!=='${_st().activeTabMarker}'&&window.__mcpTabMarker!=='${_st().activeTabMarker}'){throw new Error('MCP_WRONG_TAB')};`
|
|
1220
|
-
: '';
|
|
1238
|
+
const _guard = _tabIdentityGuard(tabIndex);
|
|
1221
1239
|
const script = `tell application "Safari" to do JavaScript "${_guard}${escaped}" in ${target}`;
|
|
1222
1240
|
try {
|
|
1223
1241
|
if (script.length < 50000) {
|
|
@@ -1254,7 +1272,10 @@ async function runJS(js, { tabIndex, timeout = 15000 } = {}) {
|
|
|
1254
1272
|
if (newIdx && newIdx !== idx) {
|
|
1255
1273
|
console.error(`[Safari MCP] Tab ghost resolved: ${idx} → ${newIdx}`);
|
|
1256
1274
|
const newTarget = `tab ${newIdx} of ${getTargetWindowRef()}`;
|
|
1257
|
-
|
|
1275
|
+
// Keep the guard on the retry. `newIdx` came from a re-resolve triggered by the
|
|
1276
|
+
// index being wrong; dropping the check here made the least trustworthy path the
|
|
1277
|
+
// only unchecked one (#64).
|
|
1278
|
+
const retryScript = `tell application "Safari" to do JavaScript "${_guard}${escaped}" in ${newTarget}`;
|
|
1258
1279
|
if (retryScript.length < 50000) return osascriptFast(retryScript, { timeout });
|
|
1259
1280
|
return osascript(retryScript, { timeout });
|
|
1260
1281
|
}
|
|
@@ -1313,7 +1334,10 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
|
|
|
1313
1334
|
.replace(/\n/g, " ")
|
|
1314
1335
|
.replace(/\r/g, "")
|
|
1315
1336
|
.replace(/\t/g, " ");
|
|
1316
|
-
|
|
1337
|
+
// Same execution-time identity check runJS gets. These are the upload / paste-image ops —
|
|
1338
|
+
// the largest blast radius in the toolset — and they were the one targeted path with no
|
|
1339
|
+
// guard at all (#64).
|
|
1340
|
+
const appleScript = `tell application "Safari" to do JavaScript "${_tabIdentityGuard(tabIndex)}${escaped}" in ${target}`;
|
|
1317
1341
|
const tmpFile = join(tmpdir(), `safari-mcp-${Date.now()}.scpt`);
|
|
1318
1342
|
await writeFile(tmpFile, appleScript, "utf8");
|
|
1319
1343
|
// Save frontmost app via daemon before subprocess execution
|
|
@@ -1326,6 +1350,13 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
|
|
|
1326
1350
|
maxBuffer: 10 * 1024 * 1024,
|
|
1327
1351
|
});
|
|
1328
1352
|
return stdout.trim();
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
// No retry here, unlike runJS: these payloads carry file data, and re-running one on a
|
|
1355
|
+
// freshly resolved tab is exactly the guess this guard exists to prevent.
|
|
1356
|
+
if ((err.message || '').includes('MCP_WRONG_TAB')) {
|
|
1357
|
+
throw new Error('Tab tracking lost during runJSLarge — target tab does not carry this session\'s marker (atomic guard fail-closed). Call safari_new_tab to reopen.', { cause: err });
|
|
1358
|
+
}
|
|
1359
|
+
throw err;
|
|
1329
1360
|
} finally {
|
|
1330
1361
|
unlink(tmpFile).catch(() => {});
|
|
1331
1362
|
// Awaited restore — caller must not return to user-space while Safari is still frontmost.
|