safari-mcp 2.7.3 → 2.7.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.
- package/README.md +62 -74
- package/extension/manifest.json +10 -0
- package/index.d.ts +218 -0
- package/index.js +51 -28
- package/package.json +4 -2
- package/safari-helper +0 -0
- package/safari-helper.swift +36 -1
- package/safari.js +104 -13
package/README.md
CHANGED
|
@@ -8,22 +8,17 @@
|
|
|
8
8
|
|
|
9
9
|
**The only MCP server for Safari — native browser automation for AI agents.**
|
|
10
10
|
|
|
11
|
-
[](https://opensource.org/licenses/MIT)
|
|
12
|
-
[](https://github.com/achiya-automation/safari-mcp/releases)
|
|
13
11
|
[](https://www.npmjs.com/package/safari-mcp)
|
|
14
12
|
[](https://www.npmjs.com/package/safari-mcp)
|
|
15
|
-
[](https://nodejs.org/)
|
|
13
|
+
[](https://opensource.org/licenses/MIT)
|
|
17
14
|
[](https://www.apple.com/macos/)
|
|
18
|
-
[](https://modelcontextprotocol.io)
|
|
19
15
|
[](https://github.com/achiya-automation/safari-mcp/stargazers)
|
|
20
|
-
[](https://glama.ai/mcp/servers/achiya-automation/safari-mcp)
|
|
21
16
|
|
|
22
|
-
<a href="vscode: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-
|
|
23
|
-
<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-
|
|
24
|
-
<a href="
|
|
17
|
+
<a href="vscode: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-Install_Server-0078d4?logo=visual-studio-code&logoColor=white" alt="Install in VS Code"></a>
|
|
18
|
+
<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>
|
|
19
|
+
<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>
|
|
25
20
|
|
|
26
|
-
**80 tools** · **
|
|
21
|
+
**80 tools** · **No Chrome/Puppeteer/Playwright needed** · **~5ms per command** · **60% less CPU than Chrome**
|
|
27
22
|
|
|
28
23
|
[Quick Start](#quick-start) · [All 80 Tools](#tools-80) · [Examples](examples/) · [Why Safari MCP?](#safari-mcp-vs-alternatives) · [Architecture](#architecture) · [Changelog](CHANGELOG.md)
|
|
29
24
|
|
|
@@ -31,23 +26,18 @@
|
|
|
31
26
|
|
|
32
27
|
</div>
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
> **TL;DR:** Use your real Safari with all your logins, cookies, and sessions. No headless browsers, no Chrome, no Puppeteer. Just pure AppleScript + JavaScript running natively on macOS — 60% less CPU/heat on Apple Silicon.
|
|
29
|
+
> **TL;DR:** Use your real Safari with all your logins, cookies, and sessions. No headless browsers, no Chrome, no Puppeteer. Just native AppleScript + JavaScript on macOS — 60% less CPU/heat on Apple Silicon.
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
<summary><b>🤔 Why not just use Playwright or Chrome DevTools MCP?</b></summary>
|
|
31
|
+
### 🤔 Why not just use Playwright or Chrome DevTools MCP?
|
|
40
32
|
|
|
41
33
|
| Problem | Safari MCP Solution |
|
|
42
34
|
|---------|-------------------|
|
|
43
35
|
| Chrome DevTools MCP heats up your Mac | Native WebKit — ~60% less CPU |
|
|
44
36
|
| Playwright launches a new browser without your logins | Uses your **real Safari** with all sessions |
|
|
45
|
-
| Puppeteer requires Chrome + debug port |
|
|
37
|
+
| Puppeteer requires Chrome + debug port | Just AppleScript — no extra browser |
|
|
46
38
|
| Headless browsers can't access your authenticated sessions | Gmail, GitHub, Slack — already logged in |
|
|
47
39
|
| Browser automation steals window focus | Safari stays in background, never interrupts |
|
|
48
40
|
|
|
49
|
-
</details>
|
|
50
|
-
|
|
51
41
|
---
|
|
52
42
|
|
|
53
43
|
## Highlights
|
|
@@ -56,7 +46,7 @@
|
|
|
56
46
|
- **Zero heat** — native WebKit on Apple Silicon, ~60% less CPU than Chrome
|
|
57
47
|
- **Your real browser** — keeps all logins, cookies, sessions (Gmail, GitHub, Ahrefs, etc.)
|
|
58
48
|
- **Background operation** — Safari stays in the background, no window stealing
|
|
59
|
-
- **No dependencies** — no Puppeteer, no Playwright, no WebDriver, no Chrome
|
|
49
|
+
- **No browser dependencies** — no Puppeteer, no Playwright, no WebDriver, no Chrome
|
|
60
50
|
- **Persistent process** — reuses a single osascript process (~5ms per command vs ~80ms)
|
|
61
51
|
- **Framework-compatible** — React, Vue, Angular, Svelte form filling via native setters
|
|
62
52
|
|
|
@@ -71,86 +61,60 @@
|
|
|
71
61
|
- Safari → Settings → Advanced → **Show features for web developers** ✓
|
|
72
62
|
- Safari → Develop → **Allow JavaScript from Apple Events** ✓
|
|
73
63
|
|
|
74
|
-
### Install
|
|
64
|
+
### Install (one command)
|
|
75
65
|
|
|
76
|
-
**Option A — npm (recommended):**
|
|
77
66
|
```bash
|
|
78
|
-
|
|
67
|
+
npx safari-mcp
|
|
79
68
|
```
|
|
80
69
|
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
brew install achiya-automation/tap/safari-mcp
|
|
84
|
-
```
|
|
70
|
+
That's it — no global install needed. Or install permanently:
|
|
85
71
|
|
|
86
|
-
**Option C — from source:**
|
|
87
72
|
```bash
|
|
88
|
-
|
|
89
|
-
cd safari-mcp
|
|
90
|
-
npm install
|
|
73
|
+
npm install -g safari-mcp
|
|
91
74
|
```
|
|
92
75
|
|
|
93
|
-
### Configure
|
|
76
|
+
### Configure your MCP client
|
|
94
77
|
|
|
95
|
-
Add to your
|
|
96
|
-
|
|
97
|
-
<details>
|
|
98
|
-
<summary><b>Claude Code</b> (~/.mcp.json)</summary>
|
|
78
|
+
Add to your client's config file — all clients use the same JSON:
|
|
99
79
|
|
|
100
80
|
```json
|
|
101
81
|
{
|
|
102
82
|
"mcpServers": {
|
|
103
83
|
"safari": {
|
|
104
|
-
"command": "
|
|
105
|
-
"args": ["
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": ["safari-mcp"]
|
|
106
86
|
}
|
|
107
87
|
}
|
|
108
88
|
}
|
|
109
89
|
```
|
|
110
|
-
</details>
|
|
111
90
|
|
|
112
91
|
<details>
|
|
113
|
-
<summary><b>
|
|
92
|
+
<summary><b>Where does each client store this?</b></summary>
|
|
93
|
+
|
|
94
|
+
| Client | Config file |
|
|
95
|
+
|--------|------------|
|
|
96
|
+
| **Claude Code** | `~/.mcp.json` |
|
|
97
|
+
| **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
98
|
+
| **Cursor** | `.cursor/mcp.json` in your project |
|
|
99
|
+
| **Windsurf** | `.windsurf/mcp.json` in your project |
|
|
100
|
+
| **VS Code + Continue** | `.vscode/mcp.json` in your project |
|
|
114
101
|
|
|
115
|
-
```json
|
|
116
|
-
{
|
|
117
|
-
"mcpServers": {
|
|
118
|
-
"safari": {
|
|
119
|
-
"command": "node",
|
|
120
|
-
"args": ["/path/to/safari-mcp/index.js"]
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
102
|
</details>
|
|
126
103
|
|
|
127
104
|
<details>
|
|
128
|
-
<summary><b>
|
|
105
|
+
<summary><b>Alternative: Homebrew</b></summary>
|
|
129
106
|
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
"mcpServers": {
|
|
133
|
-
"safari": {
|
|
134
|
-
"command": "node",
|
|
135
|
-
"args": ["/path/to/safari-mcp/index.js"]
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
107
|
+
```bash
|
|
108
|
+
brew install achiya-automation/tap/safari-mcp
|
|
139
109
|
```
|
|
140
110
|
</details>
|
|
141
111
|
|
|
142
112
|
<details>
|
|
143
|
-
<summary><b>
|
|
113
|
+
<summary><b>Alternative: from source</b></summary>
|
|
144
114
|
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
"safari": {
|
|
149
|
-
"command": "node",
|
|
150
|
-
"args": ["/path/to/safari-mcp/index.js"]
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/achiya-automation/safari-mcp.git
|
|
117
|
+
cd safari-mcp && npm install
|
|
154
118
|
```
|
|
155
119
|
</details>
|
|
156
120
|
|
|
@@ -375,7 +339,7 @@ Safari MCP runs locally on your Mac with minimal attack surface:
|
|
|
375
339
|
| CPU/Heat | 🟢 Minimal | 🔴 High | 🟡 Medium |
|
|
376
340
|
| Your logins | ✅ Yes | ✅ Yes | ❌ No |
|
|
377
341
|
| macOS native | ✅ WebKit | ❌ Chromium | ❌ Chromium/WebKit |
|
|
378
|
-
|
|
|
342
|
+
| Browser dependencies | None | Chrome + debug port | Playwright runtime |
|
|
379
343
|
| Tools | 80 | ~30 | ~25 |
|
|
380
344
|
| File upload | JS (no dialog) | CDP | Playwright API |
|
|
381
345
|
| Image paste | JS (no clipboard) | CDP | Playwright API |
|
|
@@ -451,12 +415,16 @@ The Safari MCP Extension is **optional but recommended**. Without it, ~80% of fu
|
|
|
451
415
|
|
|
452
416
|
### Installing the Extension
|
|
453
417
|
|
|
454
|
-
The extension requires a one-time build with Xcode (free, included with macOS)
|
|
418
|
+
The extension requires a one-time build with Xcode (free, included with macOS).
|
|
419
|
+
|
|
420
|
+
> **Note for npm users:** The `xcode/` directory is not included in the npm package.
|
|
421
|
+
> Clone the [GitHub repository](https://github.com/achiya-automation/safari-mcp) to build from source.
|
|
455
422
|
|
|
456
423
|
**Prerequisites:** Xcode (install from App Store — free)
|
|
457
424
|
|
|
458
425
|
```bash
|
|
459
|
-
# 1.
|
|
426
|
+
# 1. Clone the repo (the npm package does not include the Xcode project)
|
|
427
|
+
git clone https://github.com/achiya-automation/safari-mcp.git
|
|
460
428
|
cd safari-mcp
|
|
461
429
|
xcodebuild -project "xcode/Safari MCP/Safari MCP.xcodeproj" \
|
|
462
430
|
-scheme "Safari MCP (macOS)" -configuration Release build
|
|
@@ -545,14 +513,34 @@ Your support funds:
|
|
|
545
513
|
|
|
546
514
|
---
|
|
547
515
|
|
|
548
|
-
##
|
|
516
|
+
## Community
|
|
517
|
+
|
|
518
|
+
**2,000+ weekly npm downloads** — developers are using Safari MCP to build AI agents on macOS.
|
|
519
|
+
|
|
520
|
+
- [GitHub Discussions](https://github.com/achiya-automation/safari-mcp/discussions) — ask questions, share use cases
|
|
521
|
+
- [Issues](https://github.com/achiya-automation/safari-mcp/issues) — bug reports and feature requests
|
|
522
|
+
- [](https://github.com/achiya-automation/safari-mcp/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) — start contributing
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Like it? Give it a ⭐
|
|
527
|
+
|
|
528
|
+
If Safari MCP saves you from Chrome overhead, **a star helps others discover it:**
|
|
549
529
|
|
|
550
|
-
|
|
530
|
+
[](https://github.com/achiya-automation/safari-mcp)
|
|
531
|
+
|
|
532
|
+
[Share on Twitter/X](https://twitter.com/intent/tweet?text=Safari%20MCP%20%E2%80%94%20Stop%20running%20Chrome%20just%20so%20your%20AI%20agent%20can%20browse.%2080%20tools%2C%20native%20Safari%2C%2060%25%20less%20CPU.&url=https%3A%2F%2Fgithub.com%2Fachiya-automation%2Fsafari-mcp) · [Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fgithub.com%2Fachiya-automation%2Fsafari-mcp) · [Write about it](https://dev.to/)
|
|
551
533
|
|
|
552
534
|
[](https://star-history.com/#achiya-automation/safari-mcp&Date)
|
|
553
535
|
|
|
554
536
|
---
|
|
555
537
|
|
|
538
|
+
## Listed On
|
|
539
|
+
|
|
540
|
+
[](https://glama.ai/mcp/servers/achiya-automation/safari-mcp) [](https://registry.modelcontextprotocol.io/) [](https://github.com/achiya-automation/safari-mcp/actions/workflows/ci.yml)
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
556
544
|
## License
|
|
557
545
|
|
|
558
546
|
MIT — use it however you want.
|
package/extension/manifest.json
CHANGED
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
"host_permissions": ["<all_urls>"],
|
|
21
21
|
"content_scripts": [{
|
|
22
22
|
"matches": ["<all_urls>"],
|
|
23
|
+
"exclude_matches": [
|
|
24
|
+
"*://*.linkedin.com/*",
|
|
25
|
+
"*://*.x.com/*",
|
|
26
|
+
"*://*.twitter.com/*",
|
|
27
|
+
"*://*.producthunt.com/*",
|
|
28
|
+
"*://mail.google.com/*",
|
|
29
|
+
"*://calendar.google.com/*",
|
|
30
|
+
"*://chat.google.com/*",
|
|
31
|
+
"*://meet.google.com/*"
|
|
32
|
+
],
|
|
23
33
|
"js": ["content.js"],
|
|
24
34
|
"run_at": "document_start",
|
|
25
35
|
"world": "MAIN"
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safari MCP — TypeScript type declarations
|
|
3
|
+
* Covers all ~83 registered tools and the MCP server export.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ========== CORE TYPES ==========
|
|
7
|
+
|
|
8
|
+
export type MCPContent =
|
|
9
|
+
| { type: "text"; text: string }
|
|
10
|
+
| { type: "image"; data: string; mimeType: string };
|
|
11
|
+
|
|
12
|
+
export interface MCPToolResult {
|
|
13
|
+
content: MCPContent[];
|
|
14
|
+
isError?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MCPToolInputSchema {
|
|
18
|
+
type: "object";
|
|
19
|
+
properties: Record<string, { type: string; description?: string; enum?: string[] }>;
|
|
20
|
+
required?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MCPTool<TInput extends Record<string, unknown> = Record<string, unknown>> {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
inputSchema: MCPToolInputSchema;
|
|
27
|
+
handler: (input: TInput) => Promise<MCPToolResult>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
// ========== TOOL INPUT TYPES ==========
|
|
32
|
+
|
|
33
|
+
// Navigation
|
|
34
|
+
export interface SafariNavigateInput { url: string }
|
|
35
|
+
export interface SafariGoBackInput {}
|
|
36
|
+
export interface SafariGoForwardInput {}
|
|
37
|
+
export interface SafariReloadInput { hard?: boolean }
|
|
38
|
+
export interface SafariNavigateAndReadInput { url: string; maxLength?: number; timeout?: number }
|
|
39
|
+
|
|
40
|
+
// Page Info
|
|
41
|
+
export interface SafariReadPageInput { selector?: string; maxLength?: number }
|
|
42
|
+
export interface SafariGetSourceInput { maxLength?: number }
|
|
43
|
+
export interface SafariSnapshotInput { selector?: string }
|
|
44
|
+
|
|
45
|
+
// Click
|
|
46
|
+
export interface SafariClickInput { ref?: string; selector?: string; text?: string; x?: number; y?: number }
|
|
47
|
+
export interface SafariClickAndReadInput { text?: string; selector?: string; x?: number; y?: number; wait?: number; maxLength?: number }
|
|
48
|
+
export interface SafariDoubleClickInput { selector?: string; x?: number; y?: number }
|
|
49
|
+
export interface SafariRightClickInput { selector?: string; x?: number; y?: number }
|
|
50
|
+
export interface SafariNativeClickInput { ref?: string; selector?: string; text?: string; x?: number; y?: number; doubleClick?: boolean }
|
|
51
|
+
|
|
52
|
+
// Form Input
|
|
53
|
+
export interface SafariFillInput { ref?: string; selector?: string; value: string }
|
|
54
|
+
export interface SafariClearFieldInput { selector: string }
|
|
55
|
+
export interface SafariSelectOptionInput { selector: string; value: string }
|
|
56
|
+
export interface SafariFillFormField { selector: string; value: string }
|
|
57
|
+
export interface SafariFillFormInput { fields: SafariFillFormField[] }
|
|
58
|
+
|
|
59
|
+
// Keyboard
|
|
60
|
+
export interface SafariPressKeyInput { key: string; modifiers?: string[] }
|
|
61
|
+
export interface SafariTypeTextInput { text: string; ref?: string; selector?: string }
|
|
62
|
+
|
|
63
|
+
// Code Editor
|
|
64
|
+
export interface SafariReplaceEditorInput { text: string }
|
|
65
|
+
|
|
66
|
+
// Screenshot
|
|
67
|
+
export interface SafariScreenshotInput { fullPage?: boolean }
|
|
68
|
+
export interface SafariScreenshotElementInput { selector: string }
|
|
69
|
+
|
|
70
|
+
// Scroll
|
|
71
|
+
export interface SafariScrollInput { direction?: "up" | "down"; amount?: number }
|
|
72
|
+
export interface SafariScrollToInput { x?: number; y?: number }
|
|
73
|
+
export interface SafariScrollToElementInput {
|
|
74
|
+
selector?: string;
|
|
75
|
+
text?: string;
|
|
76
|
+
block?: "start" | "center" | "end" | "nearest";
|
|
77
|
+
timeout?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Tab Management
|
|
81
|
+
export interface SafariListTabsInput {}
|
|
82
|
+
export interface SafariNewTabInput { url?: string }
|
|
83
|
+
export interface SafariCloseTabInput {}
|
|
84
|
+
export interface SafariSwitchTabInput { index: number }
|
|
85
|
+
export interface SafariWaitForNewTabInput { timeout?: number; urlContains?: string }
|
|
86
|
+
|
|
87
|
+
// Wait
|
|
88
|
+
export interface SafariWaitForInput { selector?: string; text?: string; timeout?: number }
|
|
89
|
+
export interface SafariWaitInput { ms: number }
|
|
90
|
+
|
|
91
|
+
// Evaluate
|
|
92
|
+
export interface SafariEvaluateInput { script: string }
|
|
93
|
+
|
|
94
|
+
// Element Info
|
|
95
|
+
export interface SafariGetElementInput { selector: string }
|
|
96
|
+
export interface SafariQueryAllInput { selector: string; limit?: number }
|
|
97
|
+
|
|
98
|
+
// Hover
|
|
99
|
+
export interface SafariHoverInput { ref?: string; selector?: string; x?: number; y?: number }
|
|
100
|
+
|
|
101
|
+
// Dialog
|
|
102
|
+
export interface SafariHandleDialogInput { action?: "accept" | "dismiss"; text?: string }
|
|
103
|
+
|
|
104
|
+
// Window
|
|
105
|
+
export interface SafariResizeInput { width: number; height: number }
|
|
106
|
+
|
|
107
|
+
// Drag
|
|
108
|
+
export interface SafariDragInput {
|
|
109
|
+
sourceSelector?: string;
|
|
110
|
+
targetSelector?: string;
|
|
111
|
+
sourceX?: number;
|
|
112
|
+
sourceY?: number;
|
|
113
|
+
targetX?: number;
|
|
114
|
+
targetY?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// File / Image
|
|
118
|
+
export interface SafariUploadFileInput { selector: string; filePath: string }
|
|
119
|
+
export interface SafariPasteImageInput { filePath: string }
|
|
120
|
+
|
|
121
|
+
// Emulation
|
|
122
|
+
export interface SafariEmulateInput { device?: string; width?: number; height?: number; userAgent?: string; scale?: number }
|
|
123
|
+
export interface SafariResetEmulationInput {}
|
|
124
|
+
|
|
125
|
+
// Cookies & Storage
|
|
126
|
+
export interface SafariGetCookiesInput {}
|
|
127
|
+
export interface SafariLocalStorageInput { key?: string }
|
|
128
|
+
export interface SafariSetCookieInput {
|
|
129
|
+
name: string;
|
|
130
|
+
value: string;
|
|
131
|
+
domain?: string;
|
|
132
|
+
path?: string;
|
|
133
|
+
expires?: string;
|
|
134
|
+
secure?: boolean;
|
|
135
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
136
|
+
}
|
|
137
|
+
export interface SafariDeleteCookiesInput { name?: string; all?: boolean }
|
|
138
|
+
export interface SafariSessionStorageInput { key?: string }
|
|
139
|
+
export interface SafariSetSessionStorageInput { key: string; value: string }
|
|
140
|
+
export interface SafariSetLocalStorageInput { key: string; value: string }
|
|
141
|
+
export interface SafariDeleteLocalStorageInput { key?: string }
|
|
142
|
+
export interface SafariDeleteSessionStorageInput { key?: string }
|
|
143
|
+
export interface SafariExportStorageInput {}
|
|
144
|
+
export interface SafariImportStorageInput { state: string }
|
|
145
|
+
|
|
146
|
+
// Clipboard
|
|
147
|
+
export interface SafariClipboardReadInput {}
|
|
148
|
+
export interface SafariClipboardWriteInput { text: string }
|
|
149
|
+
|
|
150
|
+
// Network
|
|
151
|
+
export interface SafariNetworkInput { limit?: number }
|
|
152
|
+
export interface SafariMockRouteResponse { status?: number; body?: string; contentType?: string }
|
|
153
|
+
export interface SafariMockRouteInput { urlPattern: string; response: SafariMockRouteResponse }
|
|
154
|
+
export interface SafariClearMocksInput {}
|
|
155
|
+
export interface SafariStartNetworkCaptureInput {}
|
|
156
|
+
export interface SafariNetworkDetailsInput { limit?: number; filter?: string }
|
|
157
|
+
export interface SafariClearNetworkInput {}
|
|
158
|
+
export interface SafariThrottleNetworkInput { profile?: string; latency?: number; downloadKbps?: number }
|
|
159
|
+
|
|
160
|
+
// Console
|
|
161
|
+
export interface SafariStartConsoleInput {}
|
|
162
|
+
export interface SafariGetConsoleInput {}
|
|
163
|
+
export interface SafariClearConsoleInput {}
|
|
164
|
+
export interface SafariConsoleFilterInput { level: "log" | "warn" | "error" | "info" }
|
|
165
|
+
|
|
166
|
+
// PDF
|
|
167
|
+
export interface SafariSavePDFInput { path: string }
|
|
168
|
+
|
|
169
|
+
// Accessibility
|
|
170
|
+
export interface SafariAccessibilitySnapshotInput { selector?: string; maxDepth?: number }
|
|
171
|
+
|
|
172
|
+
// Performance
|
|
173
|
+
export interface SafariPerformanceMetricsInput {}
|
|
174
|
+
export interface SafariCSSCoverageInput {}
|
|
175
|
+
|
|
176
|
+
// Data Extraction
|
|
177
|
+
export interface SafariExtractTablesInput { selector?: string; limit?: number }
|
|
178
|
+
export interface SafariExtractMetaInput {}
|
|
179
|
+
export interface SafariExtractImagesInput { limit?: number }
|
|
180
|
+
export interface SafariExtractLinksInput { limit?: number; filter?: string }
|
|
181
|
+
|
|
182
|
+
// Geolocation
|
|
183
|
+
export interface SafariOverrideGeolocationInput { latitude: number; longitude: number; accuracy?: number }
|
|
184
|
+
|
|
185
|
+
// Computed Styles
|
|
186
|
+
export interface SafariGetComputedStyleInput { selector: string; properties?: string[] }
|
|
187
|
+
|
|
188
|
+
// IndexedDB
|
|
189
|
+
export interface SafariListIndexedDBsInput {}
|
|
190
|
+
export interface SafariGetIndexedDBInput { dbName: string; storeName: string; limit?: number }
|
|
191
|
+
|
|
192
|
+
// Form Detection
|
|
193
|
+
export interface SafariDetectFormsInput {}
|
|
194
|
+
|
|
195
|
+
// Batch / Combo Tools
|
|
196
|
+
export interface SafariRunScriptStep { action: string; args?: Record<string, unknown> }
|
|
197
|
+
export interface SafariRunScriptInput { steps: SafariRunScriptStep[] }
|
|
198
|
+
export interface SafariClickAndWaitInput { selector?: string; text?: string; waitFor?: string; timeout?: number }
|
|
199
|
+
export interface SafariFillAndSubmitInput { fields: SafariFillFormField[]; submitSelector?: string }
|
|
200
|
+
export interface SafariAnalyzePageInput {}
|
|
201
|
+
|
|
202
|
+
// ========== TAB INFO ==========
|
|
203
|
+
|
|
204
|
+
export interface SafariTab {
|
|
205
|
+
index: number;
|
|
206
|
+
title: string;
|
|
207
|
+
url: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ========== MODULE DECLARATION ==========
|
|
211
|
+
export interface McpServer {
|
|
212
|
+
connect(transport: unknown): Promise<void>;
|
|
213
|
+
tool(name: string, description: string, schema: unknown, handler: (input: unknown) => Promise<MCPToolResult>): unknown;
|
|
214
|
+
close(): Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
declare const server: McpServer;
|
|
218
|
+
export default server;
|
package/index.js
CHANGED
|
@@ -526,11 +526,11 @@ async function _proxyToExtension(type, payload, timeoutMs = 30000) {
|
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
let _extensionLastPollTime = 0;
|
|
529
|
-
// Detect stale HTTP connection (no poll in
|
|
529
|
+
// Detect stale HTTP connection (no poll in 30s = disconnected)
|
|
530
530
|
// Only applies to primary instance (extension host) — not proxy mode
|
|
531
531
|
setInterval(() => {
|
|
532
532
|
if (_isExtensionHost && _extensionConnected && !_extensionWs && _extensionLastPollTime > 0) {
|
|
533
|
-
if (Date.now() - _extensionLastPollTime >
|
|
533
|
+
if (Date.now() - _extensionLastPollTime > 30000) {
|
|
534
534
|
_extensionConnected = false;
|
|
535
535
|
_drainOnDisconnect("HTTP poll timeout");
|
|
536
536
|
console.error("[Safari MCP] Extension disconnected (HTTP poll timeout)");
|
|
@@ -665,33 +665,53 @@ async function extensionOrFallback(extensionType, extensionPayload, fallbackFn)
|
|
|
665
665
|
}
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
668
|
+
|
|
669
|
+
// ========== FOCUS PRESERVATION ==========
|
|
670
|
+
// Safari AppleScript/extension can steal focus (bring Safari window to front).
|
|
671
|
+
// Save the frontmost app before the operation and restore it after if Safari stole focus.
|
|
672
|
+
// Set focusGuard flag so inner osascript/runJSLarge calls skip their own focus logic.
|
|
673
|
+
const savedApp = await safari.saveFrontmostApp();
|
|
674
|
+
safari.setFocusGuard(true);
|
|
675
|
+
|
|
676
|
+
let result;
|
|
677
|
+
let usedExtension = false;
|
|
678
|
+
try {
|
|
679
|
+
if (_extensionConnected && !_preferAppleScript) {
|
|
680
|
+
try {
|
|
681
|
+
const t0 = Date.now();
|
|
682
|
+
const tabUrl = safari.getActiveTabURL();
|
|
683
|
+
const payload = { ...extensionPayload, sessionId: SESSION_ID, ...(tabUrl ? { tabUrl } : {}) };
|
|
684
|
+
const timeout = _commandTimeouts[extensionType] || 30000;
|
|
685
|
+
result = await sendToExtension(extensionType, payload, timeout);
|
|
686
|
+
const isCspError = typeof result === 'string' && (result.includes('unsafe-eval') || result.includes('trusted-types') || result.includes('Trusted Type') || result.includes('Content Security Policy'));
|
|
687
|
+
const isPermissionDenied = typeof result === 'string' && result.includes('__SCREENSHOT_PERMISSION_DENIED__');
|
|
688
|
+
const isFailed = result === null || (typeof result === 'string' && result.startsWith('Element not found'));
|
|
689
|
+
if (isPermissionDenied) {
|
|
690
|
+
console.error(`[Safari MCP] ${extensionType} permission denied (${Date.now() - t0}ms) — falling back to AppleScript`);
|
|
691
|
+
} else if (isCspError) {
|
|
692
|
+
console.error(`[Safari MCP] ${extensionType} CSP blocked: ${result?.substring(0, 100)} (${Date.now() - t0}ms) — falling back to AppleScript`);
|
|
693
|
+
} else if (isFailed && _nullMeansFailure.has(extensionType)) {
|
|
694
|
+
console.error(`[Safari MCP] ${extensionType} extension failed: ${result} (${Date.now() - t0}ms) — falling back to AppleScript`);
|
|
695
|
+
} else {
|
|
696
|
+
console.error(`[Safari MCP] ${extensionType} via extension (${Date.now() - t0}ms)`);
|
|
697
|
+
usedExtension = true;
|
|
698
|
+
}
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.error(`[Safari MCP] ${extensionType} extension failed: ${err.message} — falling back to AppleScript`);
|
|
687
701
|
}
|
|
688
|
-
} catch (err) {
|
|
689
|
-
console.error(`[Safari MCP] ${extensionType} extension failed: ${err.message} — falling back to AppleScript`);
|
|
690
702
|
}
|
|
703
|
+
if (!usedExtension) {
|
|
704
|
+
const t0 = Date.now();
|
|
705
|
+
result = await fallbackFn();
|
|
706
|
+
console.error(`[Safari MCP] ${extensionType} via AppleScript (${Date.now() - t0}ms)`);
|
|
707
|
+
}
|
|
708
|
+
} finally {
|
|
709
|
+
safari.setFocusGuard(false);
|
|
691
710
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
711
|
+
|
|
712
|
+
// Restore focus if Safari stole it
|
|
713
|
+
await safari.restoreFocusIfStolen(savedApp);
|
|
714
|
+
|
|
695
715
|
return result;
|
|
696
716
|
}
|
|
697
717
|
|
|
@@ -1094,7 +1114,10 @@ server.tool(
|
|
|
1094
1114
|
"Take a screenshot of a specific element (by CSS selector). Returns base64 PNG image.",
|
|
1095
1115
|
{ selector: z.string().describe("CSS selector of the element to capture") },
|
|
1096
1116
|
async ({ selector }) => {
|
|
1097
|
-
const base64 = await
|
|
1117
|
+
const base64 = await extensionOrFallback(
|
|
1118
|
+
"screenshot_element", { selector },
|
|
1119
|
+
() => safari.screenshotElement({ selector })
|
|
1120
|
+
);
|
|
1098
1121
|
return {
|
|
1099
1122
|
content: [{ type: "image", data: base64, mimeType: "image/jpeg" }],
|
|
1100
1123
|
};
|
|
@@ -1582,7 +1605,7 @@ server.tool(
|
|
|
1582
1605
|
|
|
1583
1606
|
server.tool(
|
|
1584
1607
|
"safari_save_pdf",
|
|
1585
|
-
"Save the current page as a PDF file. Uses
|
|
1608
|
+
"Save the current page as a PDF file. Uses screencapture + PDF rendering (no Safari UI interaction needed).",
|
|
1586
1609
|
{ path: z.string().describe("Absolute file path to save the PDF (e.g. /Users/am/Downloads/page.pdf)") },
|
|
1587
1610
|
async (args) => {
|
|
1588
1611
|
const result = await safari.savePDF(args);
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.5",
|
|
4
4
|
"mcpName": "io.github.achiya-automation/safari-mcp",
|
|
5
5
|
"description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 80 tools via AppleScript + JavaScript.",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"types": "index.d.ts",
|
|
7
8
|
"main": "index.js",
|
|
8
9
|
"bin": {
|
|
9
10
|
"safari-mcp": "index.js"
|
|
@@ -64,7 +65,8 @@
|
|
|
64
65
|
"extension/",
|
|
65
66
|
"glama.json",
|
|
66
67
|
"LICENSE",
|
|
67
|
-
"README.md"
|
|
68
|
+
"README.md",
|
|
69
|
+
"index.d.ts"
|
|
68
70
|
],
|
|
69
71
|
"dependencies": {
|
|
70
72
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
package/safari-helper
CHANGED
|
Binary file
|
package/safari-helper.swift
CHANGED
|
@@ -278,12 +278,47 @@ while let line = readLine(strippingNewline: true) {
|
|
|
278
278
|
continue
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
// Handle getFrontApp — returns frontmost application bundle ID (native, ~0.1ms)
|
|
282
|
+
if json["getFrontApp"] != nil {
|
|
283
|
+
let frontApp = NSWorkspace.shared.frontmostApplication
|
|
284
|
+
respond(["result": frontApp?.localizedName ?? "", "bundleId": frontApp?.bundleIdentifier ?? ""])
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Handle activateApp — activate a specific app by bundle ID (native, ~1ms)
|
|
289
|
+
// Used to restore focus to previous app instead of hiding Safari (less jarring)
|
|
290
|
+
if let bundleId = json["activateApp"] as? String {
|
|
291
|
+
if let app = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId).first {
|
|
292
|
+
app.activate()
|
|
293
|
+
respond(["result": "activated"])
|
|
294
|
+
} else {
|
|
295
|
+
respond(["error": "app not found: \(bundleId)"])
|
|
296
|
+
}
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle hideSafari — hide Safari to prevent focus stealing (native, ~1ms)
|
|
301
|
+
if json["hideSafari"] != nil {
|
|
302
|
+
if let safariApp = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Safari").first {
|
|
303
|
+
safariApp.hide()
|
|
304
|
+
respond(["result": "hidden"])
|
|
305
|
+
} else {
|
|
306
|
+
respond(["error": "Safari not running"])
|
|
307
|
+
}
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
|
|
281
311
|
// Handle AppleScript command
|
|
282
312
|
guard let script = json["script"] as? String else {
|
|
283
|
-
respond(["error": "invalid input — expected 'script', 'click', or '
|
|
313
|
+
respond(["error": "invalid input — expected 'script', 'click', 'keyboard', 'getFrontApp', or 'hideSafari'"])
|
|
284
314
|
continue
|
|
285
315
|
}
|
|
286
316
|
|
|
317
|
+
// Focus preservation is handled by the Node.js layer (osascript() function
|
|
318
|
+
// and extensionOrFallback). The daemon must NOT manage focus — it runs every
|
|
319
|
+
// 3 seconds for background checks, and restoring focus here steals Safari
|
|
320
|
+
// from the user whenever they switch to it.
|
|
321
|
+
|
|
287
322
|
guard let nsScript = NSAppleScript(source: script) else {
|
|
288
323
|
respond(["error": "failed to compile AppleScript"])
|
|
289
324
|
continue
|
package/safari.js
CHANGED
|
@@ -177,6 +177,7 @@ let _targetWindowId = null; // Numeric window ID (for CGEvent window targeting)
|
|
|
177
177
|
let _targetWindowCacheTime = 0;
|
|
178
178
|
const TARGET_WINDOW_CACHE_MS = 1000; // Short cache — fast detection of window changes
|
|
179
179
|
let _profileWindowMissing = false; // True when profile window is not found
|
|
180
|
+
let _focusGuardActive = false; // True when an outer caller already handles focus save/restore
|
|
180
181
|
|
|
181
182
|
// Get the target window ref, falling back to 'front window' ONLY when no profile is configured
|
|
182
183
|
function getTargetWindowRef() {
|
|
@@ -273,6 +274,56 @@ function getFallbackTarget() {
|
|
|
273
274
|
// Quick JS execution — exposed for smart-wait checks in index.js
|
|
274
275
|
export async function runJSQuick(js) { return runJS(js); }
|
|
275
276
|
|
|
277
|
+
// ========== FOCUS PRESERVATION ==========
|
|
278
|
+
// Safari AppleScript can steal focus (bring Safari window to front).
|
|
279
|
+
// We hide Safari instead of activating the previous app — prevents ANY Safari window from flashing.
|
|
280
|
+
export async function saveFrontmostApp() {
|
|
281
|
+
const app = await _helperGetFrontApp();
|
|
282
|
+
return app?.bundleId || null;
|
|
283
|
+
}
|
|
284
|
+
export async function restoreFocusIfStolen(savedBundleId) {
|
|
285
|
+
if (!savedBundleId || savedBundleId === "com.apple.Safari") return;
|
|
286
|
+
const current = await _helperGetFrontApp();
|
|
287
|
+
if (current?.bundleId === "com.apple.Safari") {
|
|
288
|
+
await _helperActivateApp(savedBundleId);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function _helperHideSafari(timeout = 2000) {
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
if (!_helperProc || !_helperProc.stdin?.writable) { resolve(); return; }
|
|
295
|
+
let resolved = false;
|
|
296
|
+
const timer = setTimeout(() => { if (!resolved) { resolved = true; resolve(); } }, timeout);
|
|
297
|
+
function cb() {
|
|
298
|
+
if (resolved) return;
|
|
299
|
+
resolved = true;
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
resolve();
|
|
302
|
+
}
|
|
303
|
+
_helperQueue.push(cb);
|
|
304
|
+
try { _helperProc.stdin.write('{"hideSafari":true}\n'); }
|
|
305
|
+
catch { clearTimeout(timer); resolve(); }
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function _helperActivateApp(bundleId, timeout = 2000) {
|
|
310
|
+
return new Promise((resolve) => {
|
|
311
|
+
if (!_helperProc || !_helperProc.stdin?.writable) { resolve(); return; }
|
|
312
|
+
let resolved = false;
|
|
313
|
+
const timer = setTimeout(() => { if (!resolved) { resolved = true; resolve(); } }, timeout);
|
|
314
|
+
function cb() {
|
|
315
|
+
if (resolved) return;
|
|
316
|
+
resolved = true;
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
resolve();
|
|
319
|
+
}
|
|
320
|
+
_helperQueue.push(cb);
|
|
321
|
+
try { _helperProc.stdin.write(JSON.stringify({ activateApp: bundleId }) + '\n'); }
|
|
322
|
+
catch { clearTimeout(timer); resolve(); }
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function setFocusGuard(active) { _focusGuardActive = active; }
|
|
276
327
|
export function getActiveTabIndex() { return _activeTabIndex; }
|
|
277
328
|
export function setActiveTabIndex(idx) { _activeTabIndex = idx; }
|
|
278
329
|
export function getActiveTabURL() { return _activeTabURL; }
|
|
@@ -342,6 +393,10 @@ async function resolveActiveTab() {
|
|
|
342
393
|
// Run AppleScript — uses execFile (safe, isolated, for complex scripts)
|
|
343
394
|
async function osascript(script, { timeout = 10000 } = {}) {
|
|
344
395
|
if (!(await isSafariRunning())) throw safariNotRunningError();
|
|
396
|
+
// Save frontmost app via daemon BEFORE subprocess (~0.1ms)
|
|
397
|
+
// Skip if an outer caller (extensionOrFallback) already handles focus
|
|
398
|
+
const shouldGuardFocus = !_focusGuardActive;
|
|
399
|
+
const frontApp = shouldGuardFocus ? await _helperGetFrontApp() : null;
|
|
345
400
|
try {
|
|
346
401
|
const { stdout } = await execFileAsync("osascript", ["-e", script], {
|
|
347
402
|
timeout,
|
|
@@ -360,6 +415,14 @@ async function osascript(script, { timeout = 10000 } = {}) {
|
|
|
360
415
|
}
|
|
361
416
|
}
|
|
362
417
|
throw new Error(`AppleScript error: ${err.stderr || err.message}`);
|
|
418
|
+
} finally {
|
|
419
|
+
// Re-activate previous app if Safari stole focus (~1ms)
|
|
420
|
+
if (shouldGuardFocus && frontApp?.bundleId && frontApp.bundleId !== 'com.apple.Safari') {
|
|
421
|
+
const current = await _helperGetFrontApp();
|
|
422
|
+
if (current?.bundleId === 'com.apple.Safari') {
|
|
423
|
+
_helperActivateApp(frontApp.bundleId).catch(() => {});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
363
426
|
}
|
|
364
427
|
}
|
|
365
428
|
|
|
@@ -520,6 +583,26 @@ function _helperNativeKeyboard(keyCode, flags = [], windowId = 0, timeout = 5000
|
|
|
520
583
|
});
|
|
521
584
|
}
|
|
522
585
|
|
|
586
|
+
// ========== NATIVE FOCUS OPERATIONS VIA DAEMON ==========
|
|
587
|
+
// Uses NSRunningApplication — ~0.1ms for get, ~1ms for activate (vs ~90ms AppleScript)
|
|
588
|
+
|
|
589
|
+
function _helperGetFrontApp(timeout = 2000) {
|
|
590
|
+
return new Promise((resolve) => {
|
|
591
|
+
if (!_helperProc || !_helperProc.stdin?.writable) { resolve(null); return; }
|
|
592
|
+
let resolved = false;
|
|
593
|
+
const timer = setTimeout(() => { if (!resolved) { resolved = true; resolve(null); } }, timeout);
|
|
594
|
+
function cb(line) {
|
|
595
|
+
if (resolved) return;
|
|
596
|
+
resolved = true;
|
|
597
|
+
clearTimeout(timer);
|
|
598
|
+
try { resolve(JSON.parse(line)); } catch { resolve(null); }
|
|
599
|
+
}
|
|
600
|
+
_helperQueue.push(cb);
|
|
601
|
+
try { _helperProc.stdin.write('{"getFrontApp":true}\n'); }
|
|
602
|
+
catch { clearTimeout(timer); resolve(null); }
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
523
606
|
// Get Safari window bounds, toolbar height, and window ID for coordinate calculation
|
|
524
607
|
async function _getSafariWindowGeometry() {
|
|
525
608
|
await refreshTargetWindow();
|
|
@@ -632,6 +715,10 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
|
|
|
632
715
|
const appleScript = `tell application "Safari" to do JavaScript "${escaped}" in ${target}`;
|
|
633
716
|
const tmpFile = join(tmpdir(), `safari-mcp-${Date.now()}.scpt`);
|
|
634
717
|
await writeFile(tmpFile, appleScript, "utf8");
|
|
718
|
+
// Save frontmost app via daemon before subprocess execution
|
|
719
|
+
// Skip if outer caller already handles focus
|
|
720
|
+
const shouldGuard = !_focusGuardActive;
|
|
721
|
+
const frontApp = shouldGuard ? await _helperGetFrontApp() : null;
|
|
635
722
|
try {
|
|
636
723
|
const { stdout } = await execFileAsync("osascript", [tmpFile], {
|
|
637
724
|
timeout,
|
|
@@ -640,6 +727,13 @@ async function runJSLarge(js, { tabIndex, timeout = 30000 } = {}) {
|
|
|
640
727
|
return stdout.trim();
|
|
641
728
|
} finally {
|
|
642
729
|
unlink(tmpFile).catch(() => {});
|
|
730
|
+
// Re-activate previous app if Safari stole focus (~1ms)
|
|
731
|
+
if (shouldGuard && frontApp?.bundleId && frontApp.bundleId !== 'com.apple.Safari') {
|
|
732
|
+
const current = await _helperGetFrontApp();
|
|
733
|
+
if (current?.bundleId === 'com.apple.Safari') {
|
|
734
|
+
_helperActivateApp(frontApp.bundleId).catch(() => {});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
643
737
|
}
|
|
644
738
|
}
|
|
645
739
|
|
|
@@ -1504,15 +1598,11 @@ export async function screenshot({ fullPage = false } = {}) {
|
|
|
1504
1598
|
).catch(() => null) : null;
|
|
1505
1599
|
|
|
1506
1600
|
// On macOS Tahoe, screencapture -l may briefly steal focus.
|
|
1507
|
-
// Save frontmost app so we can
|
|
1508
|
-
let
|
|
1601
|
+
// Save frontmost app via daemon so we can hide Safari if it stole focus.
|
|
1602
|
+
let previousBundleId = null;
|
|
1509
1603
|
if (windowId) {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
previousApp = (await osascriptFast(
|
|
1513
|
-
`tell application "System Events" to return name of first application process whose frontmost is true`
|
|
1514
|
-
)).trim();
|
|
1515
|
-
} catch (_) {}
|
|
1604
|
+
const fa = await _helperGetFrontApp();
|
|
1605
|
+
previousBundleId = fa?.bundleId || null;
|
|
1516
1606
|
}
|
|
1517
1607
|
|
|
1518
1608
|
if (windowId) {
|
|
@@ -1553,11 +1643,12 @@ export async function screenshot({ fullPage = false } = {}) {
|
|
|
1553
1643
|
);
|
|
1554
1644
|
}
|
|
1555
1645
|
}
|
|
1556
|
-
//
|
|
1557
|
-
if (
|
|
1558
|
-
await
|
|
1559
|
-
|
|
1560
|
-
|
|
1646
|
+
// Re-activate previous app if screencapture stole focus (common on macOS Tahoe)
|
|
1647
|
+
if (previousBundleId && previousBundleId !== "com.apple.Safari") {
|
|
1648
|
+
const cur = await _helperGetFrontApp();
|
|
1649
|
+
if (cur?.bundleId === "com.apple.Safari") {
|
|
1650
|
+
await _helperActivateApp(previousBundleId).catch(() => {});
|
|
1651
|
+
}
|
|
1561
1652
|
}
|
|
1562
1653
|
// Compress: convert PNG to JPEG (50% quality) + resize to max 1200px width
|
|
1563
1654
|
// Cuts ~600KB PNG → ~60KB JPEG — critical for staying under 20MB context limit
|