safari-mcp 2.22.12 → 2.22.14

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.
@@ -125,7 +125,10 @@ function _safeTabUrl(rawUrl) {
125
125
 
126
126
  function _receiptOrigin(rawUrl) {
127
127
  const raw = String(rawUrl || "");
128
- if (raw === "about:blank") return raw;
128
+ // Safari leaves `url` off a blank tab (measured 7.10.26). Read as no origin at all, the
129
+ // about:blank receipt of a fresh safari_new_tab() was refused on its first command, and
130
+ // getReceipt could not rotate it: the tab could be neither used nor closed.
131
+ if (!raw || raw === "about:blank") return "about:blank";
129
132
  try {
130
133
  const parsed = new URL(raw);
131
134
  return /^https?:$/.test(parsed.protocol) ? parsed.origin : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.22.12",
3
+ "version": "2.22.14",
4
4
  "mcpName": "io.github.achiya-automation/safari-mcp",
5
5
  "description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 98 tools via AppleScript + JavaScript.",
6
6
  "type": "module",
package/safari.js CHANGED
@@ -8,7 +8,7 @@ import { promisify } from "node:util";
8
8
  import { tmpdir, homedir } from "node:os";
9
9
  import { join, dirname, resolve as resolvePath } from "node:path";
10
10
  import { readFile, writeFile, unlink, appendFile, mkdir } from "node:fs/promises";
11
- import { readFileSync, realpathSync } from "node:fs";
11
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { randomUUID } from "node:crypto";
14
14
  import { VIEWPORT_SCRIPT, SAFE_AREA_SCRIPT, PWA_SCRIPT, WEBKIT_COMPAT_SCRIPT, ALL_SHEETS_FN } from "./injected-validators.js";
@@ -1426,9 +1426,16 @@ export const SCREEN_LOCKED_MSG =
1426
1426
  "safari_type_text / safari_press_key, and safari_upload_file without forceNative.";
1427
1427
 
1428
1428
  export async function isScreenLocked() {
1429
- return execFileAsync("/bin/sh", ["-c",
1430
- "ioreg -n Root -d1 -r -a 2>/dev/null | grep -c CGSSessionScreenIsLocked || true"])
1431
- .then((r) => String(r.stdout).trim() !== "0").catch(() => false);
1429
+ try {
1430
+ const { stdout } = await execFileAsync("/usr/sbin/ioreg", ["-n", "Root", "-d1", "-r", "-a"], { timeout: 2000 });
1431
+ const matches = [...String(stdout).matchAll(/<key>\s*(?:CGSSessionScreenIsLocked|IOConsoleLocked)\s*<\/key>\s*<(true|false)\s*\/>/g)];
1432
+ if (matches.length) return matches.some(match => match[1] === "true");
1433
+ // ioreg omits the key on an unlocked session. A present but unreadable value or
1434
+ // a broken probe must fail toward blocked input, never toward CGEvent.
1435
+ return /<key>\s*(?:CGSSessionScreenIsLocked|IOConsoleLocked)\s*<\/key>/.test(String(stdout));
1436
+ } catch {
1437
+ return true;
1438
+ }
1432
1439
  }
1433
1440
 
1434
1441
  export async function assertScreenUnlocked(probe = isScreenLocked) {
@@ -6610,6 +6617,17 @@ export function macosCompatNote(productVersion) {
6610
6617
  return { version: raw, major, risky, line };
6611
6618
  }
6612
6619
 
6620
+ // A Homebrew upgrade deletes the node binary a long-running server still executes, and System
6621
+ // Settings cannot grant anything to a file that is gone. Seen on 9.10.26: node 26.9.0 was replaced
6622
+ // by 26.11.0 under a LaunchAgent daemon started two days earlier, and doctor went from 6/6 to 3/6
6623
+ // (Apple Events -1743, both helper permissions false). Only a restart onto the current node helps.
6624
+ export function staleNodeNote(execPath = process.execPath, exists = existsSync) {
6625
+ if (exists(execPath)) return null;
6626
+ return `⚠ This server still runs node from ${execPath}, which no longer exists: node was upgraded or removed while the server kept running. ` +
6627
+ "macOS permission checks can fail for a binary that is gone, and System Settings cannot grant anything to it. " +
6628
+ "Restart the server on the current node, then grant the permissions to the new binary if macOS asks.";
6629
+ }
6630
+
6613
6631
  export async function doctor() {
6614
6632
  const checks = [];
6615
6633
  const add = (ok, label, detail, fix) => checks.push({ ok, label, detail, fix: ok ? null : fix });
@@ -6657,7 +6675,7 @@ export async function doctor() {
6657
6675
  // Homebrew node upgrade changes that path, so the grant has to follow the new binary.
6658
6676
  add(!!pf && pf.screenRecording === true, "Screen Recording (screenshots)",
6659
6677
  pf ? (pf.screenRecording ? "permitted" : "NOT permitted — screenshots taken through AppleScript and safari_save_pdf fail (extension screenshots don't need it)") : "unknown (helper not responding)",
6660
- `System Settings > Privacy & Security > Screen & System Audio Recording → enable ${process.ppid === 1 ? `node at ${process.execPath} (this server runs under launchd)` : "the terminal/IDE that launched this server"}.`);
6678
+ `System Settings > Privacy & Security > Screen & System Audio Recording → enable ${process.ppid !== 1 ? "the terminal/IDE that launched this server" : staleNodeNote() ? "the node this server runs on after a restart (see the warning above)" : `node at ${process.execPath} (this server runs under launchd)`}.`);
6661
6679
 
6662
6680
  // 6. Helper codesign identity — a stale/ad-hoc id breaks the Accessibility grant on reinstall
6663
6681
  let idOk = false, idDetail = "";
@@ -6685,6 +6703,8 @@ export async function doctor() {
6685
6703
  const passed = checks.filter((c) => c.ok).length;
6686
6704
  const lines = [`Safari MCP doctor — ${passed}/${checks.length} checks passed`, ""];
6687
6705
  if (osLine) lines.push(osLine, "");
6706
+ const nodeNote = staleNodeNote();
6707
+ if (nodeNote) lines.push(nodeNote, "");
6688
6708
  // Not a pass/fail check — a state the user has to be able to see here rather than dig out
6689
6709
  // of the host's env, so "why did it touch my tab" has an answer in the same report (#92).
6690
6710
  lines.push(