safari-mcp 2.16.1 → 2.16.2

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
@@ -64,12 +64,27 @@ Native WebKit. ~60% less CPU. Background operation. 97 tools. One `npx` command.
64
64
 
65
65
  ---
66
66
 
67
+ ## In users' own words
68
+
69
+ Not solicited testimonials — quotes lifted from the public issue tracker, each linked to the thread it came from.
70
+
71
+ > "I run multiple Pi sessions/subagents against my normal Safari profile in parallel. **Sharing its cookies and logins is intentional.**"
72
+ > — [@maxim](https://github.com/achiya-automation/safari-mcp/issues/76), on running concurrent agents against a real browser
73
+
74
+ > "The server has a deliberate tab-ownership model … **the code is careful about this, and for the default case that's the right safety posture.**"
75
+ > — [@turner-moore](https://github.com/achiya-automation/safari-mcp/issues/92), on why the guards refuse to touch your tabs
76
+
77
+ > "Direct local validation from the package: **Safari MCP doctor 6/6.**"
78
+ > — [@jrepp](https://github.com/achiya-automation/safari-mcp/pull/53), who found and fixed a queue-alignment bug in the focus helper
79
+
80
+ ---
81
+
67
82
  ## Quick Start
68
83
 
69
84
  ### Prerequisites
70
85
 
71
86
  - macOS (any version with Safari)
72
- - Node.js 18+
87
+ - Node.js 20+
73
88
  - Safari → Settings → Advanced → **Show features for web developers** ✓
74
89
  - Safari → Develop → **Allow JavaScript from Apple Events** ✓
75
90
 
@@ -869,7 +884,7 @@ If Safari MCP saves you from Chrome overhead, **a star helps others discover it:
869
884
 
870
885
  [![Star this repo](https://img.shields.io/github/stars/achiya-automation/safari-mcp?style=social)](https://github.com/achiya-automation/safari-mcp)
871
886
 
872
- [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.%2096%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/)
887
+ [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.%2097%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/)
873
888
 
874
889
  [![Star History Chart](https://api.star-history.com/svg?repos=achiya-automation/safari-mcp&type=Date)](https://star-history.com/#achiya-automation/safari-mcp&Date)
875
890
 
@@ -1941,6 +1941,10 @@ browser.tabs.onRemoved.addListener((tabId) => {
1941
1941
  async function _verifyProfileMatch(expectedProfile) {
1942
1942
  try {
1943
1943
  const allWindows = await browser.windows.getAll({ populate: true });
1944
+ // No window in this profile → it cannot be the profile the server drives, and probing
1945
+ // would have to CREATE one: browser.tabs.create() with no open window opens a fresh window
1946
+ // that jumps to the front. That is the personal profile popping up every reconnect.
1947
+ if (!allWindows.length) return false;
1944
1948
  // Check if any window's tab titles contain the profile name pattern "ProfileName —"
1945
1949
  // Safari profile windows show: "ProfileName — Tab Title" in window name
1946
1950
  // But the extension only sees its OWN profile's windows, so we check if tabs exist at all.
package/glama.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://glama.ai/mcp/schemas/server.json",
3
3
  "name": "Safari MCP Server",
4
- "description": "Native Safari browser automation for AI agents. 96 tools via AppleScript — zero overhead, keeps logins, runs silently in background.",
4
+ "description": "Native Safari browser automation for AI agents. 97 tools via AppleScript — zero overhead, keeps logins, runs silently in background.",
5
5
  "repository": "https://github.com/achiya-automation/safari-mcp",
6
6
  "homepage": "https://github.com/achiya-automation/safari-mcp",
7
7
  "author": "achiya-automation",
package/index.js CHANGED
@@ -403,7 +403,7 @@ try {
403
403
  const execFileAsync = pfy(execFileCb);
404
404
  // Don't launch Safari if it's not running
405
405
  try {
406
- await execFileAsync("pgrep", ["-x", "Safari"], { timeout: 2000 });
406
+ await execFileAsync("pgrep", ["-x", "Safari"], { timeout: 1000 });
407
407
  } catch {
408
408
  res.writeHead(200, { "Content-Type": "application/json" });
409
409
  res.end(JSON.stringify({ match: false, error: "Safari is not running" }));
@@ -423,7 +423,11 @@ try {
423
423
  end repeat
424
424
  return "notfound"
425
425
  end tell`;
426
- const { stdout } = await execFileAsync("osascript", ["-e", script], { timeout: 5000 });
426
+ // Must finish well inside the extension's 5s fetch timeout: if the extension aborts
427
+ // first it never caches the verdict, so it re-probes every few seconds — and each probe
428
+ // opens a *new window* in any profile that has none (the personal profile popping up).
429
+ // 1s pgrep + 2.5s osascript = 3.5s worst case, and the catch below still answers 200.
430
+ const { stdout } = await execFileAsync("osascript", ["-e", script], { timeout: 2500 });
427
431
  const out = stdout.trim();
428
432
  if (out === "match") {
429
433
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -2397,7 +2401,10 @@ try {
2397
2401
  let version = "?";
2398
2402
  try { version = JSON.parse(readFileSync(pkgPath, "utf8")).version; } catch {}
2399
2403
  console.error("");
2400
- console.error(`[Safari MCP] 🦁 v${version} ready — 96 tools, native WebKit, zero Chrome.`);
2404
+ // Count tools at runtime — a hardcoded number here drifted to 96 while the code had 97.
2405
+ const toolCount = Object.keys(server._registeredTools ?? {}).length;
2406
+ const toolsPhrase = toolCount ? `${toolCount} tools, ` : "";
2407
+ console.error(`[Safari MCP] 🦁 v${version} ready — ${toolsPhrase}native WebKit, zero Chrome.`);
2401
2408
  console.error(`[Safari MCP] ⭐ Like it? Star: https://github.com/achiya-automation/safari-mcp`);
2402
2409
  console.error("");
2403
2410
  try { writeFileSync(bannerStateFile, String(Date.now()), { mode: 0o600 }); } catch {}
@@ -7,9 +7,19 @@
7
7
  // ORDER MATTERS: backslash is escaped BEFORE the quote, or the backslash inserted in front
8
8
  // of the quote gets doubled and the string breaks out. Locked by test/escaping.test.mjs
9
9
  // and test/injection-safety.test.mjs.
10
+ // NOTE: \r and \n are escaped to \\r/\\n so that multiline values round-trip as escape
11
+ // sequences inside the single-quoted JS literal rather than being silently space-flattened
12
+ // by runJS. U+2028/U+2029 are ECMAScript LineTerminators and must also be spelled as
13
+ // escape sequences -- raw U+2028/U+2029 inside a regex literal is a SyntaxError.
10
14
 
11
15
  export function escJsSingleQuote(s) {
12
- return String(s).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
16
+ return String(s)
17
+ .replace(/\\/g, "\\\\")
18
+ .replace(/'/g, "\\'")
19
+ .replace(/\r/g, "\\r")
20
+ .replace(/\n/g, "\\n")
21
+ .replace(/\u2028/g, "\\u2028")
22
+ .replace(/\u2029/g, "\\u2029");
13
23
  }
14
24
 
15
25
  // Also strips CR/LF: a raw newline would close the AppleScript string and allow injection.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.16.1",
3
+ "version": "2.16.2",
4
4
  "mcpName": "io.github.achiya-automation/safari-mcp",
5
- "description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 96 tools via AppleScript + JavaScript.",
5
+ "description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 97 tools via AppleScript + JavaScript.",
6
6
  "type": "module",
7
7
  "main": "index.js",
8
8
  "bin": {
@@ -3,7 +3,7 @@
3
3
  // Skipped silently in CI and when stdout is not a TTY (npm install in scripts).
4
4
 
5
5
  const path = require("path");
6
- const { execSync } = require("child_process");
6
+ const { execFileSync, spawnSync } = require("child_process");
7
7
  const fs = require("fs");
8
8
 
9
9
  const c = {
@@ -31,7 +31,10 @@ function readHelperId(helper) {
31
31
  // NOTE: the Identifier= line is only emitted with --verbose. Plain `codesign -d`
32
32
  // prints nothing matchable, so the old grep always failed → the early-return never
33
33
  // fired and any verify read came back empty (a false "re-sign failed" signal).
34
- return execSync(`codesign -d --verbose=2 -- "${helper}" 2>&1 | grep "^Identifier="`, { encoding: "utf8" }).trim();
34
+ const result = spawnSync("codesign", ["-d", "--verbose=2", "--", helper], { encoding: "utf8" });
35
+ const output = (result.stdout || "") + (result.stderr || "");
36
+ const match = output.split("\n").find((l) => l.startsWith("Identifier="));
37
+ return match ? match.trim() : "";
35
38
  } catch { return ""; }
36
39
  }
37
40
  function ensureCodesign() {
@@ -43,9 +46,11 @@ function ensureCodesign() {
43
46
  const current = readHelperId(helper);
44
47
  if (current.includes(STABLE_ID)) return;
45
48
  const entitlements = path.join(__dirname, "..", "safari-helper.entitlements");
46
- const entFlag = fs.existsSync(entitlements) ? `--entitlements "${entitlements}"` : "";
49
+ const codesignArgs = ["-s", "-", "-f", "--identifier", STABLE_ID];
50
+ if (fs.existsSync(entitlements)) codesignArgs.push("--entitlements", entitlements);
51
+ codesignArgs.push(helper);
47
52
  // NOTE: do NOT swallow codesign's stderr (the old `2>/dev/null` hid real failures).
48
- execSync(`codesign -s - -f --identifier ${STABLE_ID} ${entFlag} "${helper}"`, { stdio: ["ignore", "ignore", "pipe"] });
53
+ execFileSync("codesign", codesignArgs, { stdio: ["ignore", "ignore", "pipe"] });
49
54
  // Verify the re-sign actually took. A SILENT failure here is the #1 cause of native
50
55
  // clicks "succeeding" while never reaching the page — the Accessibility grant is keyed
51
56
  // to the identifier, so a wrong/unstable identifier breaks it invisibly. Make it loud.