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.
- package/README.md +0 -12
- package/index.js +67 -31
- 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
|
-
[](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
|
-
[](https://star-history.com/#achiya-automation/safari-mcp&Date)
|
|
1005
|
-
|
|
1006
|
-
---
|
|
1007
|
-
|
|
1008
996
|
## Listed On
|
|
1009
997
|
|
|
1010
998
|
[](https://glama.ai/mcp/servers/achiya-automation/safari-mcp) [](https://registry.modelcontextprotocol.io/) [](https://mcpscoreboard.com/server/05977769-8762-4e89-aff3-a0c5776843bb/) [](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
|
-
|
|
2558
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|