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.
- package/README.md +7 -5
- package/index.js +23 -13
- package/package.json +1 -1
- 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
|
|
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
|
|
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 |
|
|
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"
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
1184
|
-
// explicitly
|
|
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
|
-
|
|
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
|
}
|