safari-mcp 2.15.4 → 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 +28 -5
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
|
@@ -1177,6 +1177,18 @@ async function _withTargetTabFronted(fn) {
|
|
|
1177
1177
|
}
|
|
1178
1178
|
}
|
|
1179
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
|
+
|
|
1180
1192
|
// Run JavaScript in Safari — fastest path, no focus stealing
|
|
1181
1193
|
// Uses osascriptFast (persistent process, ~5ms) for short scripts,
|
|
1182
1194
|
// falls back to osascript (~80ms) for long scripts that exceed stdin limits
|
|
@@ -1223,9 +1235,7 @@ async function runJS(js, { tabIndex, timeout = 15000 } = {}) {
|
|
|
1223
1235
|
// does not carry OUR marker. Makes the op fail-closed at EXECUTION time regardless of how
|
|
1224
1236
|
// `idx` resolved — a drift to the user's tab (shared profile window) throws instead of
|
|
1225
1237
|
// running on their page. Skipped when the caller passed an explicit tabIndex.
|
|
1226
|
-
const _guard = (
|
|
1227
|
-
? `if(window.name!=='${_st().activeTabMarker}'&&window.__mcpTabMarker!=='${_st().activeTabMarker}'){throw new Error('MCP_WRONG_TAB')};`
|
|
1228
|
-
: '';
|
|
1238
|
+
const _guard = _tabIdentityGuard(tabIndex);
|
|
1229
1239
|
const script = `tell application "Safari" to do JavaScript "${_guard}${escaped}" in ${target}`;
|
|
1230
1240
|
try {
|
|
1231
1241
|
if (script.length < 50000) {
|
|
@@ -1262,7 +1272,10 @@ async function runJS(js, { tabIndex, timeout = 15000 } = {}) {
|
|
|
1262
1272
|
if (newIdx && newIdx !== idx) {
|
|
1263
1273
|
console.error(`[Safari MCP] Tab ghost resolved: ${idx} → ${newIdx}`);
|
|
1264
1274
|
const newTarget = `tab ${newIdx} of ${getTargetWindowRef()}`;
|
|
1265
|
-
|
|
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}`;
|
|
1266
1279
|
if (retryScript.length < 50000) return osascriptFast(retryScript, { timeout });
|
|
1267
1280
|
return osascript(retryScript, { timeout });
|
|
1268
1281
|
}
|
|
@@ -1321,7 +1334,10 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
|
|
|
1321
1334
|
.replace(/\n/g, " ")
|
|
1322
1335
|
.replace(/\r/g, "")
|
|
1323
1336
|
.replace(/\t/g, " ");
|
|
1324
|
-
|
|
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}`;
|
|
1325
1341
|
const tmpFile = join(tmpdir(), `safari-mcp-${Date.now()}.scpt`);
|
|
1326
1342
|
await writeFile(tmpFile, appleScript, "utf8");
|
|
1327
1343
|
// Save frontmost app via daemon before subprocess execution
|
|
@@ -1334,6 +1350,13 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
|
|
|
1334
1350
|
maxBuffer: 10 * 1024 * 1024,
|
|
1335
1351
|
});
|
|
1336
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;
|
|
1337
1360
|
} finally {
|
|
1338
1361
|
unlink(tmpFile).catch(() => {});
|
|
1339
1362
|
// Awaited restore — caller must not return to user-space while Safari is still frontmost.
|