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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/safari.js +28 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.15.4",
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 = (_st().activeTabMarker && !tabIndex)
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
- const retryScript = `tell application "Safari" to do JavaScript "${escaped}" in ${newTarget}`;
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
- const appleScript = `tell application "Safari" to do JavaScript "${escaped}" in ${target}`;
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.