safari-mcp 2.7.8 → 2.7.9
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 +27 -4
- package/extension/popup/popup.html +1 -37
- package/extension/popup/popup.js +34 -0
- package/index.js +1 -1
- package/package.json +5 -1
- package/safari-helper +0 -0
- package/safari-helper.swift +4 -21
package/README.md
CHANGED
|
@@ -426,13 +426,22 @@ The extension requires a one-time build with Xcode (free, included with macOS).
|
|
|
426
426
|
# 1. Clone the repo (the npm package does not include the Xcode project)
|
|
427
427
|
git clone https://github.com/achiya-automation/safari-mcp.git
|
|
428
428
|
cd safari-mcp
|
|
429
|
+
|
|
430
|
+
# 2. Build the extension
|
|
429
431
|
xcodebuild -project "xcode/Safari MCP/Safari MCP.xcodeproj" \
|
|
430
432
|
-scheme "Safari MCP (macOS)" -configuration Release build
|
|
431
433
|
|
|
432
|
-
#
|
|
433
|
-
|
|
434
|
+
# 3. Ad-hoc sign the built app so Safari will load it
|
|
435
|
+
# (xcodebuild without a signing identity produces a bundle Safari silently rejects)
|
|
436
|
+
APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData/Safari_MCP-*/Build/Products/Release -name "Safari MCP.app" -maxdepth 2 | head -1)
|
|
437
|
+
codesign --sign - --force --deep "$APP_PATH"
|
|
438
|
+
|
|
439
|
+
# 4. Open the app (needed once so Safari registers the extension)
|
|
440
|
+
open "$APP_PATH"
|
|
434
441
|
```
|
|
435
442
|
|
|
443
|
+
Alternatively, open `xcode/Safari MCP/Safari MCP.xcodeproj` directly in Xcode, select your Apple ID under Signing & Capabilities, and click Run. A free personal Apple Developer account is sufficient for local use.
|
|
444
|
+
|
|
436
445
|
Then in Safari:
|
|
437
446
|
1. Safari → Settings → Advanced → enable **Show features for web developers**
|
|
438
447
|
2. Safari → Develop → **Allow Unsigned Extensions** (required each Safari restart)
|
|
@@ -456,8 +465,21 @@ Safari MCP needs these one-time permissions:
|
|
|
456
465
|
| Permission | Where | Why |
|
|
457
466
|
|-----------|-------|-----|
|
|
458
467
|
| JavaScript from Apple Events | Safari → Develop menu | Required for `do JavaScript` |
|
|
459
|
-
|
|
|
460
|
-
|
|
|
468
|
+
| Automation → Safari | System Settings → Privacy & Security → Automation | Required for all AppleScript-backed tools |
|
|
469
|
+
| Screen Recording | System Settings → Privacy & Security → Screen Recording | Required for `safari_screenshot` |
|
|
470
|
+
| Accessibility | System Settings → Privacy & Security → Accessibility | Required for `safari_save_pdf` only |
|
|
471
|
+
|
|
472
|
+
### Granting Automation → Safari (important for IDE users)
|
|
473
|
+
|
|
474
|
+
macOS TCC grants Automation permission to the **parent process** that spawns the MCP server, not to `safari-mcp` itself. So you need to grant **Automation → Safari** to the app that runs Claude Code / Cursor / Windsurf — typically **Visual Studio Code** or **Terminal**.
|
|
475
|
+
|
|
476
|
+
If the permission dialog never appears automatically, run this command once from a Terminal that already has Automation permission:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
osascript -e 'tell application "Safari" to get URL of current tab of window 1'
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
That call registers the Terminal app in the Automation database and then triggers the prompt for Safari. After you approve it, subsequent MCP calls from any child process chain will work.
|
|
461
483
|
|
|
462
484
|
---
|
|
463
485
|
|
|
@@ -466,6 +488,7 @@ Safari MCP needs these one-time permissions:
|
|
|
466
488
|
| Issue | Fix |
|
|
467
489
|
|-------|-----|
|
|
468
490
|
| "AppleScript error" | Enable "Allow JavaScript from Apple Events" in Safari → Develop |
|
|
491
|
+
| "Not authorized to send Apple events to Safari" | Grant Automation → Safari to your IDE (see above) |
|
|
469
492
|
| Screenshots empty | Grant Screen Recording permission to Terminal/VS Code |
|
|
470
493
|
| Tab not found | Call `safari_list_tabs` to refresh tab indices |
|
|
471
494
|
| Hebrew keyboard issues | All typing uses JS events — immune to keyboard layout |
|
|
@@ -37,42 +37,6 @@
|
|
|
37
37
|
<span class="slider"></span>
|
|
38
38
|
</label>
|
|
39
39
|
</div>
|
|
40
|
-
<script>
|
|
41
|
-
// Inline script — cannot be cached separately by Safari
|
|
42
|
-
function setStatus(c, t, i) {
|
|
43
|
-
document.getElementById("statusDot").className = "dot " + c;
|
|
44
|
-
document.getElementById("statusText").textContent = t;
|
|
45
|
-
document.getElementById("info").textContent = i;
|
|
46
|
-
}
|
|
47
|
-
async function getVal(k, d) {
|
|
48
|
-
try { const r = await browser.storage.local.get(k); return r[k] !== undefined ? r[k] : d; }
|
|
49
|
-
catch { return d; }
|
|
50
|
-
}
|
|
51
|
-
async function check() {
|
|
52
|
-
const on = await getVal("mcpEnabled", true);
|
|
53
|
-
document.getElementById("enableToggle").checked = on;
|
|
54
|
-
if (!on) { setStatus("paused", "Paused", "Toggle to resume"); return; }
|
|
55
|
-
// Ask background script for real-time status first
|
|
56
|
-
try {
|
|
57
|
-
const resp = await browser.runtime.sendMessage({ action: "getStatus" });
|
|
58
|
-
if (resp) {
|
|
59
|
-
if (resp.connected) { setStatus("connected", "Connected", "Port 9224"); return; }
|
|
60
|
-
if (!resp.enabled) { setStatus("paused", "Paused", "Toggle to resume"); return; }
|
|
61
|
-
}
|
|
62
|
-
} catch {}
|
|
63
|
-
// Fallback: read from storage
|
|
64
|
-
const s = await getVal("mcpStatus", null);
|
|
65
|
-
if (s === "connected") setStatus("connected", "Connected", "Port 9224");
|
|
66
|
-
else if (s === "paused") setStatus("paused", "Paused", "Toggle to resume");
|
|
67
|
-
else if (s === "checking") setStatus("checking", "Connecting...", "Trying port 9224...");
|
|
68
|
-
else setStatus("disconnected", "Not connected", "Start the MCP server to connect");
|
|
69
|
-
}
|
|
70
|
-
document.getElementById("enableToggle").addEventListener("change", async (e) => {
|
|
71
|
-
await browser.storage.local.set({ mcpEnabled: e.target.checked });
|
|
72
|
-
try { browser.runtime.sendMessage({ action: "setEnabled", enabled: e.target.checked }); } catch {}
|
|
73
|
-
setTimeout(check, 500);
|
|
74
|
-
});
|
|
75
|
-
check();
|
|
76
|
-
</script>
|
|
40
|
+
<script src="popup.js"></script>
|
|
77
41
|
</body>
|
|
78
42
|
</html>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function setStatus(c, t, i) {
|
|
2
|
+
document.getElementById("statusDot").className = "dot " + c;
|
|
3
|
+
document.getElementById("statusText").textContent = t;
|
|
4
|
+
document.getElementById("info").textContent = i;
|
|
5
|
+
}
|
|
6
|
+
async function getVal(k, d) {
|
|
7
|
+
try { const r = await browser.storage.local.get(k); return r[k] !== undefined ? r[k] : d; }
|
|
8
|
+
catch { return d; }
|
|
9
|
+
}
|
|
10
|
+
async function check() {
|
|
11
|
+
const on = await getVal("mcpEnabled", true);
|
|
12
|
+
document.getElementById("enableToggle").checked = on;
|
|
13
|
+
if (!on) { setStatus("paused", "Paused", "Toggle to resume"); return; }
|
|
14
|
+
// Ask background script for real-time status first
|
|
15
|
+
try {
|
|
16
|
+
const resp = await browser.runtime.sendMessage({ action: "getStatus" });
|
|
17
|
+
if (resp) {
|
|
18
|
+
if (resp.connected) { setStatus("connected", "Connected", "Port 9224"); return; }
|
|
19
|
+
if (!resp.enabled) { setStatus("paused", "Paused", "Toggle to resume"); return; }
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
// Fallback: read from storage
|
|
23
|
+
const s = await getVal("mcpStatus", null);
|
|
24
|
+
if (s === "connected") setStatus("connected", "Connected", "Port 9224");
|
|
25
|
+
else if (s === "paused") setStatus("paused", "Paused", "Toggle to resume");
|
|
26
|
+
else if (s === "checking") setStatus("checking", "Connecting...", "Trying port 9224...");
|
|
27
|
+
else setStatus("disconnected", "Not connected", "Start the MCP server to connect");
|
|
28
|
+
}
|
|
29
|
+
document.getElementById("enableToggle").addEventListener("change", async (e) => {
|
|
30
|
+
await browser.storage.local.set({ mcpEnabled: e.target.checked });
|
|
31
|
+
try { browser.runtime.sendMessage({ action: "setEnabled", enabled: e.target.checked }); } catch {}
|
|
32
|
+
setTimeout(check, 500);
|
|
33
|
+
});
|
|
34
|
+
check();
|
package/index.js
CHANGED
|
@@ -147,7 +147,7 @@ let _lastMemoryWarningTime = 0;
|
|
|
147
147
|
|
|
148
148
|
function _getWebKitMemoryMB() {
|
|
149
149
|
try {
|
|
150
|
-
const pids = execFileSync("pgrep", ["-
|
|
150
|
+
const pids = execFileSync("pgrep", ["-f", "WebKit|WebContent"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
151
151
|
if (!pids) return 0;
|
|
152
152
|
const pidList = pids.split("\n").join(",");
|
|
153
153
|
const psOut = execFileSync("ps", ["-p", pidList, "-o", "rss="], { encoding: "utf8" }).trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.9",
|
|
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",
|
|
@@ -71,5 +71,9 @@
|
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
73
73
|
"ws": "^8.19.0"
|
|
74
|
+
},
|
|
75
|
+
"overrides": {
|
|
76
|
+
"hono": "^4.12.12",
|
|
77
|
+
"@hono/node-server": "^1.19.13"
|
|
74
78
|
}
|
|
75
79
|
}
|
package/safari-helper
CHANGED
|
Binary file
|
package/safari-helper.swift
CHANGED
|
@@ -324,30 +324,13 @@ while let line = readLine(strippingNewline: true) {
|
|
|
324
324
|
continue
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// for 10-30+ seconds, preventing ALL subsequent commands from being read.
|
|
330
|
-
let semaphore = DispatchSemaphore(value: 0)
|
|
331
|
-
var scriptResult: NSAppleEventDescriptor?
|
|
332
|
-
var scriptError: NSDictionary?
|
|
333
|
-
|
|
334
|
-
DispatchQueue.global(qos: .userInitiated).async {
|
|
335
|
-
var errorDict: NSDictionary?
|
|
336
|
-
scriptResult = nsScript.executeAndReturnError(&errorDict)
|
|
337
|
-
scriptError = errorDict
|
|
338
|
-
semaphore.signal()
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Wait up to 30 seconds for the script to complete.
|
|
342
|
-
// If it times out, respond with error but don't block the loop forever.
|
|
343
|
-
let waitResult = semaphore.wait(timeout: .now() + 30.0)
|
|
327
|
+
var errorDict: NSDictionary?
|
|
328
|
+
let result = nsScript.executeAndReturnError(&errorDict)
|
|
344
329
|
|
|
345
|
-
if
|
|
346
|
-
respond(["error": "AppleScript execution timed out (30s)"])
|
|
347
|
-
} else if let error = scriptError {
|
|
330
|
+
if let error = errorDict {
|
|
348
331
|
let msg = (error["NSAppleScriptErrorMessage"] as? String) ?? "AppleScript error"
|
|
349
332
|
respond(["error": msg])
|
|
350
333
|
} else {
|
|
351
|
-
respond(["result":
|
|
334
|
+
respond(["result": result.stringValue ?? ""])
|
|
352
335
|
}
|
|
353
336
|
}
|