safari-mcp 2.15.4 โ†’ 2.15.6

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 (3) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/safari.js +57 -5
package/README.md CHANGED
@@ -48,7 +48,7 @@ Native WebKit. ~60% less CPU. Background operation. 97 tools. One `npx` command.
48
48
 
49
49
  > ๐Ÿ“ฐ **Featured on freeCodeCamp:** [How to Connect Your AI Coding Agent to a Browser on macOS](https://www.freecodecamp.org/news/how-to-connect-your-ai-coding-agent-to-a-browser-on-macos/) ยท [HackerNoon: Reverse-Engineering React, Shadow DOM, and CSP](https://hackernoon.com/i-had-to-reverse-engineer-react-shadow-dom-and-csp-to-automate-safari-without-chrome)
50
50
 
51
- > ๐ŸŽ **Apple shipped an official Safari MCP** (Safari Technology Preview 247, July 2026). It's built on `safaridriver` for isolated debugging sessions. safari-mcp drives the **real Safari you're already logged into** โ€” on stable Safari, with 97 tools. See the full comparison below.
51
+ > ๐ŸŽ **Apple shipped an official Safari MCP** (July 2026 โ€” Safari Technology Preview 247+ and the Safari 27 beta). It's built on `safaridriver` for isolated debugging sessions. safari-mcp drives the **real Safari you're already logged into** โ€” on the stable Safari that ships with macOS today, with 97 tools. See the full comparison below.
52
52
 
53
53
  ---
54
54
 
@@ -544,21 +544,23 @@ Safari MCP runs locally on your Mac with minimal attack surface:
544
544
 
545
545
  ### vs Apple's Official Safari MCP (safaridriver)
546
546
 
547
- In Safari Technology Preview 247 (July 2026), Apple shipped an **official** Safari MCP server built on `safaridriver`. That's great validation for the category โ€” and it's built for a different job. Apple's server drives an **isolated WebDriver automation session** for debugging; safari-mcp drives the **real Safari you're already logged into**.
547
+ In July 2026 Apple shipped an **official** Safari MCP server built on `safaridriver` โ€” first in Safari Technology Preview 247, and now also in the Safari 27 beta. That's great validation for the category โ€” and it's built for a different job. Apple's server drives an **isolated WebDriver automation session** for debugging; safari-mcp drives the **real Safari you're already logged into**.
548
+
549
+ It has not reached a stable Safari release yet: on macOS 26.5.2 with Safari 26.5.2, `safaridriver --help` lists `--port`, `--bidi`, `--enable` and `--diagnose`, and no `--mcp` (verified 2026-07-23). Check your own machine with `safaridriver --help | grep mcp` before assuming either way.
548
550
 
549
551
  | | ๐Ÿฆ safari-mcp *(this repo)* | Apple `safaridriver --mcp` |
550
552
  |---|:---:|:---:|
551
553
  | **Your real logins / cookies** | โœ… Your actual Safari | โš ๏ธ Isolated automation session โ€” no access to AutoFill or browsing activity |
552
- | **Runs on** | โœ… Stable Safari, every Mac | โŒ Safari Technology Preview 247 only |
554
+ | **Runs on** | โœ… Stable Safari, every Mac | โš ๏ธ Safari Technology Preview 247+ or the Safari 27 beta โ€” not in stable Safari 26.5 |
553
555
  | **Background (no focus steal)** | โœ… Yes | โŒ Dedicated window with a "controlled by automation" banner |
554
556
  | **Tools** | **97** | ~17 |
555
557
  | **Storage** (cookies, localStorage, IndexedDB) | โœ… 10 tools | โŒ |
556
558
  | **Network mocking + throttling** | โœ… Yes | โŒ Read-only network inspection |
557
559
  | **Device emulation** (iPhone, iPad) | โœ… Yes | โš ๏ธ Viewport + media type only |
558
- | **Setup** | `npx safari-mcp` | Enable "remote automation and external agents" in STP |
560
+ | **Setup** | `npx safari-mcp` | Enable "remote automation and external agents", then point your client at that build's `safaridriver --mcp` |
559
561
  | **Official Apple support** | โŒ Community (MIT) | โœ… Apple, WebDriver-standard |
560
562
 
561
- > **When Apple's server is the right pick:** you specifically want a clean-room, WebDriver-standard session for compatibility debugging and you already run STP. **For everything else โ€” daily automation on the browser you're already signed into, on stable Safari โ€” safari-mcp is built for exactly that.**
563
+ > **When Apple's server is the right pick:** you specifically want a clean-room, WebDriver-standard session for compatibility debugging and you already run a preview or beta build. **For everything else โ€” daily automation on the browser you're already signed into, on the Safari that shipped with your Mac โ€” safari-mcp is built for exactly that.**
562
564
 
563
565
  ### Why Safari MCP and Not the Other Safari MCP Projects?
564
566
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.15.4",
3
+ "version": "2.15.6",
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,45 @@ 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 only when the caller named a tab
1184
+ // explicitly. See #64.
1185
+ function _tabIdentityGuard(explicitTabIndex) {
1186
+ if (explicitTabIndex) return '';
1187
+ const marker = _st().activeTabMarker;
1188
+ // No marker โ†’ this session owns no tab, so the target is the front document: the tab the
1189
+ // user is looking at. That fallback is intentional for a session that genuinely never
1190
+ // opened one โ€” but "owns no tab" is also what a *re-initialised* session reports. In
1191
+ // HTTP-daemon mode a dropped transport makes the client re-init, which mints a new
1192
+ // MCP session id, and `_st()` hands it a fresh empty state: `hasOwnedTab` false,
1193
+ // `activeTabMarker` null. Every fail-closed branch keys on `hasOwnedTab`, so they all read
1194
+ // false at exactly the moment an agent is mid-task and already owns a tab โ€” and the next
1195
+ // op runs, unguarded, on the user's current page.
1196
+ //
1197
+ // The session state died; the marker on the tab did not. A front document carrying any
1198
+ // `MCP_` marker is a tab some MCP session opened and this one does not own, so it is
1199
+ // never a legitimate implicit target โ€” refuse it. An unmarked front document stays
1200
+ // reachable, which keeps "read the page I'm looking at" working for real fresh sessions.
1201
+ if (!marker) {
1202
+ return `if(typeof window.name==='string'&&window.name.indexOf('MCP_')===0){throw new Error('MCP_FOREIGN_TAB')};`;
1203
+ }
1204
+ return `if(window.name!=='${marker}'&&window.__mcpTabMarker!=='${marker}'){throw new Error('MCP_WRONG_TAB')};`;
1205
+ }
1206
+
1207
+ // A tripped foreign-tab guard is terminal: with no marker of our own there is nothing to
1208
+ // re-resolve to, and retrying is the guess the guard just refused.
1209
+ function _foreignTabError(where, cause) {
1210
+ return new Error(
1211
+ `Tab tracking lost during ${where} โ€” the target is a tab opened by another MCP session ` +
1212
+ `that this session does not own (atomic guard fail-closed). This session holds no tab ` +
1213
+ `marker, which is also what a session re-initialised after a transport drop reports. ` +
1214
+ `Call safari_new_tab to open a tab this session owns.`,
1215
+ cause ? { cause } : undefined
1216
+ );
1217
+ }
1218
+
1180
1219
  // Run JavaScript in Safari โ€” fastest path, no focus stealing
1181
1220
  // Uses osascriptFast (persistent process, ~5ms) for short scripts,
1182
1221
  // falls back to osascript (~80ms) for long scripts that exceed stdin limits
@@ -1223,9 +1262,7 @@ async function runJS(js, { tabIndex, timeout = 15000 } = {}) {
1223
1262
  // does not carry OUR marker. Makes the op fail-closed at EXECUTION time regardless of how
1224
1263
  // `idx` resolved โ€” a drift to the user's tab (shared profile window) throws instead of
1225
1264
  // 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
- : '';
1265
+ const _guard = _tabIdentityGuard(tabIndex);
1229
1266
  const script = `tell application "Safari" to do JavaScript "${_guard}${escaped}" in ${target}`;
1230
1267
  try {
1231
1268
  if (script.length < 50000) {
@@ -1237,6 +1274,7 @@ async function runJS(js, { tabIndex, timeout = 15000 } = {}) {
1237
1274
  // the shared profile window). Re-resolve via a full marker scan and retry once on the
1238
1275
  // correct tab; if unresolved, fail closed rather than touch the user's tab.
1239
1276
  const _gmsg = err.message || '';
1277
+ if (_gmsg.includes('MCP_FOREIGN_TAB')) throw _foreignTabError('runJS', err);
1240
1278
  if (_guard && _gmsg.includes('MCP_WRONG_TAB')) {
1241
1279
  console.error('[Safari MCP] Atomic guard tripped โ€” re-resolving marked tab via full scan');
1242
1280
  _st().lastResolveTime = 0; _st().activeTabIndex = null;
@@ -1262,7 +1300,10 @@ async function runJS(js, { tabIndex, timeout = 15000 } = {}) {
1262
1300
  if (newIdx && newIdx !== idx) {
1263
1301
  console.error(`[Safari MCP] Tab ghost resolved: ${idx} โ†’ ${newIdx}`);
1264
1302
  const newTarget = `tab ${newIdx} of ${getTargetWindowRef()}`;
1265
- const retryScript = `tell application "Safari" to do JavaScript "${escaped}" in ${newTarget}`;
1303
+ // Keep the guard on the retry. `newIdx` came from a re-resolve triggered by the
1304
+ // index being wrong; dropping the check here made the least trustworthy path the
1305
+ // only unchecked one (#64).
1306
+ const retryScript = `tell application "Safari" to do JavaScript "${_guard}${escaped}" in ${newTarget}`;
1266
1307
  if (retryScript.length < 50000) return osascriptFast(retryScript, { timeout });
1267
1308
  return osascript(retryScript, { timeout });
1268
1309
  }
@@ -1321,7 +1362,10 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
1321
1362
  .replace(/\n/g, " ")
1322
1363
  .replace(/\r/g, "")
1323
1364
  .replace(/\t/g, " ");
1324
- const appleScript = `tell application "Safari" to do JavaScript "${escaped}" in ${target}`;
1365
+ // Same execution-time identity check runJS gets. These are the upload / paste-image ops โ€”
1366
+ // the largest blast radius in the toolset โ€” and they were the one targeted path with no
1367
+ // guard at all (#64).
1368
+ const appleScript = `tell application "Safari" to do JavaScript "${_tabIdentityGuard(tabIndex)}${escaped}" in ${target}`;
1325
1369
  const tmpFile = join(tmpdir(), `safari-mcp-${Date.now()}.scpt`);
1326
1370
  await writeFile(tmpFile, appleScript, "utf8");
1327
1371
  // Save frontmost app via daemon before subprocess execution
@@ -1334,6 +1378,14 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
1334
1378
  maxBuffer: 10 * 1024 * 1024,
1335
1379
  });
1336
1380
  return stdout.trim();
1381
+ } catch (err) {
1382
+ // No retry here, unlike runJS: these payloads carry file data, and re-running one on a
1383
+ // freshly resolved tab is exactly the guess this guard exists to prevent.
1384
+ if ((err.message || '').includes('MCP_FOREIGN_TAB')) throw _foreignTabError('runJSLarge', err);
1385
+ if ((err.message || '').includes('MCP_WRONG_TAB')) {
1386
+ 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 });
1387
+ }
1388
+ throw err;
1337
1389
  } finally {
1338
1390
  unlink(tmpFile).catch(() => {});
1339
1391
  // Awaited restore โ€” caller must not return to user-space while Safari is still frontmost.