safari-mcp 2.15.2 → 2.15.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <img src="social-preview.png" alt="Safari MCP Server — 96 native browser automation tools for AI agents on macOS" width="100%">
3
+ <img src="social-preview.png" alt="Safari MCP Server — 97 native browser automation tools for AI agents on macOS" width="100%">
4
4
 
5
5
  <br/>
6
6
 
@@ -24,9 +24,9 @@
24
24
  <a href="https://insiders.vscode.dev/redirect?url=vscode-insiders:mcp/install?%7B%22safari-mcp%22%3A%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22safari-mcp%22%5D%7D%7D"><img src="https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?logo=visual-studio-code&logoColor=white" alt="Install in VS Code Insiders"></a>
25
25
  <a href="cursor://anysphere.cursor-deeplink/mcp/install?name=safari-mcp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22safari-mcp%22%5D%7D"><img src="https://img.shields.io/badge/Cursor-Install_Server-f97316?logo=cursor&logoColor=white" alt="Install in Cursor"></a>
26
26
 
27
- **96 tools** · **No Chrome/Puppeteer/Playwright needed** · **~5ms per command** · **60% less CPU than Chrome**
27
+ **97 tools** · **No Chrome/Puppeteer/Playwright needed** · **~5ms per command** · **60% less CPU than Chrome**
28
28
 
29
- [Quick Start](#quick-start) · [All 96 Tools](#tools-96) · [Examples](examples/) · [Why Safari MCP?](#safari-mcp-vs-alternatives) · [Architecture](#architecture) · [Changelog](CHANGELOG.md)
29
+ [Quick Start](#quick-start) · [All 97 Tools](#tools-97) · [Examples](examples/) · [Why Safari MCP?](#safari-mcp-vs-alternatives) · [Architecture](#architecture) · [Changelog](CHANGELOG.md)
30
30
 
31
31
  ![Safari MCP Demo](https://github.com/achiya-automation/safari-mcp/raw/main/assets/safari-mcp-promo.gif)
32
32
 
@@ -44,17 +44,17 @@ Your AI agent needs to browse. So it either:
44
44
 
45
45
  Your AI drives the **Safari you're already logged into** — Gmail, GitHub, Ahrefs, Slack, banking.
46
46
 
47
- Native WebKit. ~60% less CPU. Background operation. 96 tools. One `npx` command. macOS only.
47
+ Native WebKit. ~60% less CPU. Background operation. 97 tools. One `npx` command. macOS only.
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 96 tools. See the full comparison below.
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.
52
52
 
53
53
  ---
54
54
 
55
55
  ## Highlights
56
56
 
57
- - **96 tools** — navigation, clicks, forms, screenshots, network, storage, accessibility, and more
57
+ - **97 tools** — navigation, clicks, forms, screenshots, network, storage, accessibility, and more
58
58
  - **Zero heat** — native WebKit on Apple Silicon, ~60% less CPU than Chrome
59
59
  - **Your real browser** — keeps all logins, cookies, sessions (Gmail, GitHub, Ahrefs, etc.)
60
60
  - **Background operation** — Safari stays in the background, no window stealing
@@ -304,7 +304,7 @@ The recommended pattern for AI agents using Safari MCP:
304
304
 
305
305
  ---
306
306
 
307
- ## Tools (96)
307
+ ## Tools (97)
308
308
 
309
309
  <details>
310
310
  <summary><b>Click to expand the full tool list — organized by category</b></summary>
@@ -382,6 +382,7 @@ The recommended pattern for AI agents using Safari MCP:
382
382
  | Tool | Description |
383
383
  |------|-------------|
384
384
  | `safari_evaluate` | Execute arbitrary JavaScript, return result |
385
+ | `safari_eval_file` | Execute JavaScript read from a file path (avoids huge inline scripts) |
385
386
 
386
387
  ### Element Inspection (4)
387
388
  | Tool | Description |
@@ -531,7 +532,7 @@ Safari MCP runs locally on your Mac with minimal attack surface:
531
532
  | Your logins | ✅ Yes | ✅ Yes | ❌ No |
532
533
  | macOS native | ✅ WebKit | ❌ Chromium | ❌ Chromium/WebKit |
533
534
  | Browser dependencies | None | Chrome + debug port | Playwright runtime |
534
- | Tools | 96 | ~30 | ~25 |
535
+ | Tools | 97 | ~30 | ~25 |
535
536
  | File upload | JS (no dialog) | CDP | Playwright API |
536
537
  | Image paste | JS (no clipboard) | CDP | Playwright API |
537
538
  | Focus steal | ❌ Background | ❌ Background | ❌ Headless |
@@ -550,7 +551,7 @@ In Safari Technology Preview 247 (July 2026), Apple shipped an **official** Safa
550
551
  | **Your real logins / cookies** | ✅ Your actual Safari | ⚠️ Isolated automation session — no access to AutoFill or browsing activity |
551
552
  | **Runs on** | ✅ Stable Safari, every Mac | ❌ Safari Technology Preview 247 only |
552
553
  | **Background (no focus steal)** | ✅ Yes | ❌ Dedicated window with a "controlled by automation" banner |
553
- | **Tools** | **96** | ~17 |
554
+ | **Tools** | **97** | ~17 |
554
555
  | **Storage** (cookies, localStorage, IndexedDB) | ✅ 10 tools | ❌ |
555
556
  | **Network mocking + throttling** | ✅ Yes | ❌ Read-only network inspection |
556
557
  | **Device emulation** (iPhone, iPad) | ✅ Yes | ⚠️ Viewport + media type only |
@@ -565,7 +566,7 @@ There are several "safari-mcp" projects floating around. Here's how they compare
565
566
 
566
567
  | Feature | **🦁 safari-mcp** *(this repo)* | [lxman/safari-mcp-server](https://github.com/lxman/safari-mcp-server) | [Epistates/MCPSafari](https://github.com/Epistates/MCPSafari) | [HayoDev/safari-devtools-mcp](https://github.com/HayoDev/safari-devtools-mcp) |
567
568
  |---------|:------------------------------:|:----------------------:|:------------:|:----------------------:|
568
- | **Tools** | **96** | ~10 | 23 | ~15 |
569
+ | **Tools** | **97** | ~10 | 23 | ~15 |
569
570
  | **Install** | `npx safari-mcp` | Manual | Binary | `npx` |
570
571
  | **Engine** | **Dual** (Extension + AppleScript) | WebDriver | Extension only | DevTools Protocol |
571
572
  | **Keeps your real Safari logins** | ✅ Yes | ⚠️ Limited | ✅ Yes | ❌ Debug session |
@@ -847,3 +848,7 @@ If Safari MCP saves you from Chrome overhead, **a star helps others discover it:
847
848
  ## License
848
849
 
849
850
  MIT — use it however you want.
851
+
852
+ ---
853
+
854
+ Built by **[Achiya Automation](https://achiya-automation.com)** — automation & AI agents for business.
package/mcp-helpers.js CHANGED
@@ -144,28 +144,42 @@ if (window.__mcpVersion !== 6) {
144
144
  var beforeUrl = location.href;
145
145
  var anchor = target.closest ? target.closest('a[href]') : null;
146
146
  var href = anchor && anchor.href && !anchor.href.startsWith('javascript:') ? anchor.href : '';
147
+ // Dispatch from the LEAF under the click point, not from the actionable ancestor.
148
+ // A real click's `event.target` is the topmost node at that pixel — the inner label —
149
+ // while `currentTarget` is the <a>/<button>. Handlers that gate on e.target (Facebook's
150
+ // rel="dialog" links, most SPA routers) see a target they never expected when the event
151
+ // is fired straight at the <a>, bail out, and the click silently degrades into a plain
152
+ // navigation. Firing from the leaf reproduces the real bubble path exactly.
153
+ var from = window.mcpElementFromPoint(x, y);
154
+ if (!from || !(target === from || target.contains(from))) from = target;
147
155
  var s = { bubbles: true, cancelable: true, composed: true, view: window, clientX: x, clientY: y, button: 0, detail: 1 };
148
156
  var p = { ...s, pointerId: 1, pointerType: 'mouse', isPrimary: true, width: 1, height: 1, pressure: 0.5 };
149
- target.dispatchEvent(new PointerEvent('pointerover', { ...p, buttons: 0 }));
150
- target.dispatchEvent(new MouseEvent('mouseover', { ...s, buttons: 0 }));
151
- target.dispatchEvent(new PointerEvent('pointerenter', { ...p, buttons: 0 }));
152
- target.dispatchEvent(new MouseEvent('mouseenter', { ...s, buttons: 0 }));
153
- target.dispatchEvent(new PointerEvent('pointermove', { ...p, buttons: 0 }));
154
- target.dispatchEvent(new MouseEvent('mousemove', { ...s, buttons: 0 }));
155
- target.dispatchEvent(new PointerEvent('pointerdown', { ...p, buttons: 1 }));
156
- target.dispatchEvent(new MouseEvent('mousedown', { ...s, buttons: 1 }));
157
+ from.dispatchEvent(new PointerEvent('pointerover', { ...p, buttons: 0 }));
158
+ from.dispatchEvent(new MouseEvent('mouseover', { ...s, buttons: 0 }));
159
+ from.dispatchEvent(new PointerEvent('pointerenter', { ...p, buttons: 0 }));
160
+ from.dispatchEvent(new MouseEvent('mouseenter', { ...s, buttons: 0 }));
161
+ from.dispatchEvent(new PointerEvent('pointermove', { ...p, buttons: 0 }));
162
+ from.dispatchEvent(new MouseEvent('mousemove', { ...s, buttons: 0 }));
163
+ from.dispatchEvent(new PointerEvent('pointerdown', { ...p, buttons: 1 }));
164
+ from.dispatchEvent(new MouseEvent('mousedown', { ...s, buttons: 1 }));
165
+ // Focus the actionable element, not the leaf — a <span> inside a button isn't focusable.
157
166
  if (target.focus) { try { target.focus({ preventScroll: true }); } catch (e) { try { target.focus(); } catch (_) {} } }
158
- target.dispatchEvent(new PointerEvent('pointerup', { ...p, buttons: 0, pressure: 0 }));
159
- target.dispatchEvent(new MouseEvent('mouseup', { ...s, buttons: 0 }));
167
+ from.dispatchEvent(new PointerEvent('pointerup', { ...p, buttons: 0, pressure: 0 }));
168
+ from.dispatchEvent(new MouseEvent('mouseup', { ...s, buttons: 0 }));
169
+ // .click() on an anchor (or a node inside one) triggers the browser's own navigation
170
+ // with target === the anchor, which is the exact shape the gate rejects. For anchors,
171
+ // let the dispatched event below carry the default action instead.
160
172
  try {
161
- if (typeof target.click === 'function') {
162
- target.click();
163
- if (href && href !== beforeUrl) {
164
- location.href = href;
165
- }
166
- }
173
+ if (!href && typeof from.click === 'function') from.click();
167
174
  } catch (e) {}
168
- target.dispatchEvent(new MouseEvent('click', { ...s, buttons: 0 }));
175
+ var clickEv = new MouseEvent('click', { ...s, buttons: 0 });
176
+ var notPrevented = from.dispatchEvent(clickEv);
177
+ // Navigate only if nothing claimed the click. A handler that called preventDefault owns
178
+ // it — it just opened a dialog or pushed an SPA route, and forcing location.href here
179
+ // would tear that down. (dispatchEvent returns false iff preventDefault was called.)
180
+ if (href && notPrevented && location.href === beforeUrl) {
181
+ location.href = href;
182
+ }
169
183
  // Vue v-model / Lit @change / framework-agnostic toggle sync — for checkbox/radio,
170
184
  // Vue 3 listens for `change` via v-model; some Vue components subscribe to `input` instead.
171
185
  // Native target.click() fires both, but in production-stripped Vue apps (no fiber/proxy
@@ -178,9 +192,9 @@ if (window.__mcpVersion !== 6) {
178
192
  target.dispatchEvent(new Event('input', toggleOpts));
179
193
  target.dispatchEvent(new Event('change', toggleOpts));
180
194
  }
181
- if (href && href !== beforeUrl) {
182
- location.href = href;
183
- }
195
+ // (Navigation is handled above, gated on notPrevented. A second unconditional
196
+ // `location.href = href` used to live here and fired even when a handler had called
197
+ // preventDefault — tearing down the dialog/route it had just opened.)
184
198
  var form = target.closest ? target.closest('form') : null;
185
199
  if (form && (target.type === 'submit' || (target.tagName === 'BUTTON' && target.type !== 'button' && target.type !== 'reset'))) {
186
200
  try { form.requestSubmit ? form.requestSubmit(target.type === 'submit' ? target : undefined) : form.submit(); } catch (e) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.15.2",
3
+ "version": "2.15.4",
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",
@@ -66,6 +66,7 @@
66
66
  "transport.js",
67
67
  "response.js",
68
68
  "safari.js",
69
+ "session-context.js",
69
70
  "ownership-match.js",
70
71
  "ownership-state.js",
71
72
  "mcp-helpers.js",
@@ -95,6 +96,6 @@
95
96
  "globals": "^17.6.0",
96
97
  "jsdom": "^29.1.1",
97
98
  "prettier": "^3.8.4",
98
- "typescript": "^6.0.3"
99
+ "typescript": "^7.0.2"
99
100
  }
100
101
  }