safari-mcp 2.15.5 โ†’ 2.15.7

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 (4) hide show
  1. package/README.md +7 -5
  2. package/index.js +23 -13
  3. package/package.json +1 -1
  4. package/safari.js +33 -4
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/index.js CHANGED
@@ -210,19 +210,21 @@ function _startMemoryMonitor() {
210
210
 
211
211
  // Cleanup on exit
212
212
  let _cleaningUp = false;
213
+ async function _shutdown() {
214
+ if (_cleaningUp) return;
215
+ _cleaningUp = true;
216
+ // Restore the user's clipboard synchronously FIRST โ€” if a native paste is mid-flight, its
217
+ // 2s restore timer would never fire once we exit, leaving the tool's text on the clipboard.
218
+ try { safari.flushClipboardRestore(); } catch {}
219
+ // Cap tab cleanup โ€” if the daemon/Safari is wedged at shutdown (exactly when a SIGTERM
220
+ // tends to arrive), listTabs/closeTab can each block for their full timeout; never let
221
+ // that hang the exit past 3s.
222
+ await Promise.race([_cleanupTabs(), new Promise(r => setTimeout(r, 3000))]);
223
+ process.exit(0);
224
+ }
225
+
213
226
  for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
214
- process.on(sig, async () => {
215
- if (_cleaningUp) return; // Prevent double-exit on rapid signal repeat
216
- _cleaningUp = true;
217
- // Restore the user's clipboard synchronously FIRST โ€” if a native paste is mid-flight, its
218
- // 2s restore timer would never fire once we exit, leaving the tool's text on the clipboard.
219
- try { safari.flushClipboardRestore(); } catch {}
220
- // Cap tab cleanup โ€” if the daemon/Safari is wedged at shutdown (exactly when a SIGTERM
221
- // tends to arrive), listTabs/closeTab can each block for their full timeout; never let
222
- // that hang the exit past 3s.
223
- await Promise.race([_cleanupTabs(), new Promise(r => setTimeout(r, 3000))]);
224
- process.exit(0);
225
- });
227
+ process.on(sig, _shutdown);
226
228
  }
227
229
  process.on("exit", () => {
228
230
  if (_openedTabs.size > 0) {
@@ -2420,4 +2422,12 @@ _startMemoryMonitor();
2420
2422
 
2421
2423
  // Transport is chosen at runtime: default stdio (unchanged), or a shared HTTP instance when
2422
2424
  // SAFARI_MCP_HTTP=1 (many Claude sessions โ†’ one process). buildServer is the per-session factory.
2423
- await startTransport(buildServer, process.env);
2425
+ const transportHandle = await startTransport(buildServer, process.env);
2426
+
2427
+ if (transportHandle.kind === "stdio") {
2428
+ // A client can disappear without delivering a signal. Treat its pipe closing as the stdio
2429
+ // server's lifecycle boundary, while leaving the opt-in HTTP daemon independent of stdin.
2430
+ process.stdin.once("end", _shutdown);
2431
+ process.stdin.once("close", _shutdown);
2432
+ if (process.stdin.readableEnded || process.stdin.destroyed) void _shutdown();
2433
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.15.5",
3
+ "version": "2.15.7",
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
@@ -1180,15 +1180,42 @@ async function _withTargetTabFronted(fn) {
1180
1180
  // Atomic tab-identity guard, as a JS prefix. Belongs here and not at a call site: it is the
1181
1181
  // only check that is independent of how `idx` was resolved, so every path that builds a
1182
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.
1183
+ // the index has already proven untrustworthy. Empty only when the caller named a tab
1184
+ // explicitly. See #64.
1186
1185
  function _tabIdentityGuard(explicitTabIndex) {
1186
+ if (explicitTabIndex) return '';
1187
1187
  const marker = _st().activeTabMarker;
1188
- if (!marker || explicitTabIndex) return '';
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
+ }
1189
1204
  return `if(window.name!=='${marker}'&&window.__mcpTabMarker!=='${marker}'){throw new Error('MCP_WRONG_TAB')};`;
1190
1205
  }
1191
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
+
1192
1219
  // Run JavaScript in Safari โ€” fastest path, no focus stealing
1193
1220
  // Uses osascriptFast (persistent process, ~5ms) for short scripts,
1194
1221
  // falls back to osascript (~80ms) for long scripts that exceed stdin limits
@@ -1247,6 +1274,7 @@ async function runJS(js, { tabIndex, timeout = 15000 } = {}) {
1247
1274
  // the shared profile window). Re-resolve via a full marker scan and retry once on the
1248
1275
  // correct tab; if unresolved, fail closed rather than touch the user's tab.
1249
1276
  const _gmsg = err.message || '';
1277
+ if (_gmsg.includes('MCP_FOREIGN_TAB')) throw _foreignTabError('runJS', err);
1250
1278
  if (_guard && _gmsg.includes('MCP_WRONG_TAB')) {
1251
1279
  console.error('[Safari MCP] Atomic guard tripped โ€” re-resolving marked tab via full scan');
1252
1280
  _st().lastResolveTime = 0; _st().activeTabIndex = null;
@@ -1353,6 +1381,7 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
1353
1381
  } catch (err) {
1354
1382
  // No retry here, unlike runJS: these payloads carry file data, and re-running one on a
1355
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);
1356
1385
  if ((err.message || '').includes('MCP_WRONG_TAB')) {
1357
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 });
1358
1387
  }