safari-mcp 2.22.6 → 2.22.8

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.
Files changed (3) hide show
  1. package/README.md +0 -12
  2. package/index.js +67 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -993,18 +993,6 @@ Other macOS MCP servers that complement Safari MCP:
993
993
 
994
994
  ---
995
995
 
996
- ## Like it? Give it a ⭐
997
-
998
- If Safari MCP saves you from Chrome overhead, **a star helps others discover it:**
999
-
1000
- [![Star this repo](https://img.shields.io/github/stars/achiya-automation/safari-mcp?style=social)](https://github.com/achiya-automation/safari-mcp)
1001
-
1002
- [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.%2098%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/)
1003
-
1004
- [![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)
1005
-
1006
- ---
1007
-
1008
996
  ## Listed On
1009
997
 
1010
998
  [![Glama](https://glama.ai/mcp/servers/achiya-automation/safari-mcp/badges/score.svg)](https://glama.ai/mcp/servers/achiya-automation/safari-mcp) [![MCP Registry](https://img.shields.io/badge/MCP-Registry-purple)](https://registry.modelcontextprotocol.io/) [![MCP Scoreboard](https://mcpscoreboard.com/badge/05977769-8762-4e89-aff3-a0c5776843bb.svg)](https://mcpscoreboard.com/server/05977769-8762-4e89-aff3-a0c5776843bb/) [![CI](https://github.com/achiya-automation/safari-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/achiya-automation/safari-mcp/actions/workflows/ci.yml)
package/index.js CHANGED
@@ -1861,10 +1861,16 @@ function _safeUrlForOutput(rawUrl) {
1861
1861
  // Receipts this daemon rotated on the caller's behalf (a cross-origin safari_navigate) stay
1862
1862
  // usable under their old name: callers routinely keep the first receipt they saw and ignore
1863
1863
  // the fresh one in the navigate result (nine consecutive "not valid for this origin" failures
1864
- // in one session on 4.9.26). Whoever held the old capability held the tab.
1864
+ // in one session on 4.9.26). Whoever held the old capability held the tab. Every session that
1865
+ // held it follows the alias, other clients of an HTTP daemon included (_getActiveReceipt), so an
1866
+ // alias may only join two receipts of one tab: each rotation names the receipt it replaces.
1865
1867
  const _receiptAliases = new Map(); // old token → newer token
1866
1868
  function _aliasReceipt(oldToken, newToken) {
1867
1869
  if (!oldToken || !newToken || oldToken === newToken) return;
1870
+ // Every older name of the tab points straight at its newest receipt. As a chain of one link per
1871
+ // rotation, a receipt stopped resolving after eight (_receiptToken's hop limit): a handful of
1872
+ // cross-origin navigations in one tab.
1873
+ for (const [older, newer] of _receiptAliases) if (newer === oldToken) _receiptAliases.set(older, newToken);
1868
1874
  _receiptAliases.set(oldToken, newToken);
1869
1875
  if (_receiptAliases.size > 500) _receiptAliases.delete(_receiptAliases.keys().next().value);
1870
1876
  }
@@ -1920,8 +1926,10 @@ const _activeReceipts = new Map();
1920
1926
  function _receiptSessionKey() {
1921
1927
  return `${SESSION_ID}:${currentSessionId()}`;
1922
1928
  }
1929
+ // Resolved on every read: another client of an HTTP daemon can rotate this session's receipt
1930
+ // after it was stored, and the extension then knows only the new one.
1923
1931
  function _getActiveReceipt() {
1924
- return _activeReceipts.get(_receiptSessionKey()) || "";
1932
+ return _receiptToken(_activeReceipts.get(_receiptSessionKey()));
1925
1933
  }
1926
1934
  function _setActiveReceipt(receipt) {
1927
1935
  _activeReceipts.set(_receiptSessionKey(), _receiptToken(receipt));
@@ -1975,6 +1983,39 @@ function _sanitizeTabResult(value) {
1975
1983
  };
1976
1984
  }
1977
1985
 
1986
+ // A receipt is bound to the origin it was minted on, so a cross-origin navigation the
1987
+ // caller itself asked for used to strand every following call ("not valid for this
1988
+ // origin", ~80×/week). The caller chose the destination, so rotating here is safe —
1989
+ // hand back the new receipt instead of letting the caller discover the trap. Every
1990
+ // navigation a caller asks for goes through here: safari_navigate, safari_navigate_and_read
1991
+ // and run_script's navigate and navigateAndRead steps. `usedReceipt` is the receipt the
1992
+ // navigation carried, read before it waited: a call alongside that names another tab makes
1993
+ // that tab the session's current one, and the rotation has to name the tab that navigated.
1994
+ // navigate_and_read answers with its page as a JSON string; a rotated result is an object.
1995
+ async function _rotateReceiptAfterNavigate(result, usedReceipt, oldUrl) {
1996
+ let page = result;
1997
+ if (typeof page === "string") {
1998
+ try { page = JSON.parse(page); } catch { return result; }
1999
+ }
2000
+ const landed = page && typeof page === "object" ? page.url : "";
2001
+ // The origin of the receipt the navigation carried: extensionOrFallback sends the newest one.
2002
+ if (!landed || !usedReceipt || _originOf(landed) === (_receiptOrigins.get(_receiptToken(usedReceipt)) || _originOf(oldUrl))) {
2003
+ return result;
2004
+ }
2005
+ try {
2006
+ // Named for the alias below, as run_script getReceipt names it (see _receiptAliases).
2007
+ const fresh = _sanitizeTabResult(await extensionOrFallback(
2008
+ "get_tab_receipt", { ..._explicitReceipt({ receipt: usedReceipt }) }, () => null
2009
+ ));
2010
+ if (fresh?.receipt) {
2011
+ _aliasReceipt(_receiptToken(usedReceipt), fresh.receipt); // the receipt extensionOrFallback sent
2012
+ _setActiveReceipt(fresh.receipt);
2013
+ return { ...page, receipt: fresh.receipt };
2014
+ }
2015
+ } catch { /* the navigation itself succeeded — a failed rotation must not fail the tool */ }
2016
+ return result;
2017
+ }
2018
+
1978
2019
  function _isBatchSemanticFailure(result) {
1979
2020
  if (Array.isArray(result)) return result.some(_isBatchSemanticFailure);
1980
2021
  if (result && typeof result === "object") return result.ok === false;
@@ -2089,7 +2130,7 @@ async function _runExtensionBatchAction(action, args = {}) {
2089
2130
  if (safeValue?.receipt) {
2090
2131
  // Rotation retires the old token. Keep it resolving to the new one, as navigate does,
2091
2132
  // so the tab stays reachable — and closable by the tab cap — by the name it was opened with.
2092
- _aliasReceipt(previous, safeValue.receipt);
2133
+ _aliasReceipt(_receiptToken(previous), safeValue.receipt); // the receipt extensionOrFallback sent
2093
2134
  _setActiveReceipt(safeValue.receipt);
2094
2135
  }
2095
2136
  return safeValue;
@@ -2129,22 +2170,26 @@ async function _runExtensionBatchAction(action, args = {}) {
2129
2170
  case "navigate": {
2130
2171
  const url = String(args.url || "");
2131
2172
  if (!url) throw new Error("navigate requires url");
2173
+ const oldUrl = safari.getActiveTabURL();
2174
+ const usedReceipt = _getActiveReceipt();
2132
2175
  const raw = await extensionOrFallback(
2133
- "navigate", { url, timeout: args.timeout },
2176
+ "navigate", { url, timeout: args.timeout, ..._explicitReceipt({ receipt: usedReceipt }) },
2134
2177
  () => safari.navigate(url)
2135
2178
  );
2136
- return syncResultUrl(raw, url);
2179
+ return _rotateReceiptAfterNavigate(syncResultUrl(raw, url), usedReceipt, oldUrl);
2137
2180
  }
2138
2181
 
2139
2182
  case "navigateAndRead": {
2140
2183
  const url = String(args.url || "");
2141
2184
  if (!url) throw new Error("navigateAndRead requires url");
2185
+ const oldUrl = safari.getActiveTabURL();
2186
+ const usedReceipt = _getActiveReceipt();
2142
2187
  const raw = await extensionOrFallback(
2143
- "navigate_and_read", { url, maxLength: args.maxLength, timeout: args.timeout },
2188
+ "navigate_and_read", { url, maxLength: args.maxLength, timeout: args.timeout, ..._explicitReceipt({ receipt: usedReceipt }) },
2144
2189
  async () => { await safari.navigate(url); return safari.readPage({ maxLength: args.maxLength }); }
2145
2190
  );
2146
2191
  safari.setActiveTabURL(url);
2147
- return normalize(raw);
2192
+ return _rotateReceiptAfterNavigate(normalize(raw), usedReceipt, oldUrl);
2148
2193
  }
2149
2194
 
2150
2195
  case "readPage":
@@ -2388,9 +2433,13 @@ async function extensionOrFallback(extensionType, extensionPayload, fallbackFn)
2388
2433
  const reloadHandoff = extensionType === "reload_extension" && _isExtensionHost
2389
2434
  ? _prepareReloadHttpWorkerHandoff()
2390
2435
  : null;
2436
+ const namedReceipt = _receiptToken(extensionPayload.receipt);
2391
2437
  const payload = {
2392
2438
  ...(attachActiveReceipt && activeReceipt ? { receipt: activeReceipt } : {}),
2393
2439
  ...extensionPayload,
2440
+ // A receipt named when the call started follows a rotation of its tab made while the call
2441
+ // waited above (by another client of an HTTP daemon): an alias joins receipts of one tab.
2442
+ ...(namedReceipt ? { receipt: namedReceipt } : {}),
2394
2443
  ...(reloadHandoff ? { reloadHandoff: reloadHandoff.token } : {}),
2395
2444
  sessionId: `${SESSION_ID}:${currentSessionId()}`,
2396
2445
  };
@@ -2554,31 +2603,16 @@ server.tool(
2554
2603
  }
2555
2604
  const oldUrl = safari.getActiveTabURL();
2556
2605
  // extensionOrFallback claims the destination for this session once its guard passes.
2557
- let result = await extensionOrFallback(
2558
- "navigate", { url, ..._explicitReceipt({ receipt }) },
2606
+ // Read once, before anything waits: a call alongside that names another tab makes that tab the
2607
+ // session's current one, and the rotation below has to name the tab this call navigated.
2608
+ const usedReceipt = _receiptToken(receipt || _getActiveReceipt());
2609
+ const result = await extensionOrFallback(
2610
+ "navigate", { url, ..._explicitReceipt({ receipt: usedReceipt }) },
2559
2611
  () => safari.navigate(url)
2560
2612
  );
2561
2613
  // Tab kept its identity, just changed URL — drop the stale old URL from ownership.
2562
2614
  if (oldUrl && oldUrl !== url && oldUrl !== 'about:blank') _removeOwnedURL(oldUrl);
2563
- // A receipt is bound to the origin it was minted on, so a cross-origin navigation the
2564
- // caller itself asked for used to strand every following call ("not valid for this
2565
- // origin", ~80×/week). The caller chose the destination, so rotating here is safe —
2566
- // hand back the new receipt instead of letting the caller discover the trap.
2567
- const landed = result && typeof result === "object" ? result.url : "";
2568
- const usedReceipt = _receiptToken(receipt || _getActiveReceipt());
2569
- if (landed && _originOf(landed) !== (_receiptOrigins.get(usedReceipt) || _originOf(oldUrl)) && usedReceipt) {
2570
- try {
2571
- const fresh = _sanitizeTabResult(await extensionOrFallback(
2572
- "get_tab_receipt", { ..._explicitReceipt({ receipt }) }, () => null
2573
- ));
2574
- if (fresh?.receipt) {
2575
- _aliasReceipt(usedReceipt, fresh.receipt);
2576
- _setActiveReceipt(fresh.receipt);
2577
- result = { ...result, receipt: fresh.receipt };
2578
- }
2579
- } catch { /* the navigation itself succeeded — a failed rotation must not fail the tool */ }
2580
- }
2581
- return textResult(result);
2615
+ return textResult(await _rotateReceiptAfterNavigate(result, usedReceipt, oldUrl));
2582
2616
  }
2583
2617
  );
2584
2618
 
@@ -2662,7 +2696,7 @@ server.tool(
2662
2696
 
2663
2697
  server.tool(
2664
2698
  "safari_navigate_and_read",
2665
- "Navigate to a URL and return the page content in one step — saves 1 full round-trip vs navigate+read_page. Use instead of safari_navigate + safari_read_page.",
2699
+ "Navigate to a URL and return the page content in one step — saves 1 full round-trip vs navigate+read_page. Use instead of safari_navigate + safari_read_page. Like safari_navigate, returns a fresh `receipt` when the origin changed; use that one from then on.",
2666
2700
  {
2667
2701
  receipt: z.string().optional().describe("Tab receipt from safari_new_tab — pins this call to that tab (survives reconnects/subagents)"),
2668
2702
  url: z.string().describe("URL to navigate to"),
@@ -2671,15 +2705,17 @@ server.tool(
2671
2705
  },
2672
2706
  async ({ url, maxLength, timeout, receipt }) => {
2673
2707
  const oldUrl = safari.getActiveTabURL();
2708
+ // Read before anything waits, as safari_navigate does: the rotation names the tab that navigated.
2709
+ const usedReceipt = _receiptToken(receipt || _getActiveReceipt());
2674
2710
  const result = await extensionOrFallback(
2675
- "navigate_and_read", { url, maxLength, timeout, ..._explicitReceipt({ receipt }) },
2711
+ "navigate_and_read", { url, maxLength, timeout, ..._explicitReceipt({ receipt: usedReceipt }) },
2676
2712
  async () => {
2677
2713
  await safari.navigate(url);
2678
2714
  return safari.readPage({ maxLength });
2679
2715
  }
2680
2716
  );
2681
2717
  if (oldUrl && oldUrl !== url && oldUrl !== 'about:blank') _removeOwnedURL(oldUrl);
2682
- return textResult(result);
2718
+ return textResult(await _rotateReceiptAfterNavigate(result, usedReceipt, oldUrl));
2683
2719
  }
2684
2720
  );
2685
2721
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.22.6",
3
+ "version": "2.22.8",
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",