safari-mcp 2.11.0 → 2.11.1

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/index.js +19 -2
  2. package/package.json +1 -1
  3. package/safari.js +9 -0
package/index.js CHANGED
@@ -127,6 +127,20 @@ function _isURLOwned(url) {
127
127
  return false;
128
128
  }
129
129
 
130
+ // Sentinel persisted when a blank tab (about:blank) is opened by this session.
131
+ // A blank tab has no unique URL to own, but ownership must still survive an MCP
132
+ // process restart (_openedTabs is in-memory only) — otherwise reopening blank
133
+ // tabs falsely trips the "no tabs opened yet" guard. The sentinel is never a
134
+ // real tab URL, so it cannot falsely match a user's page in _isURLOwned().
135
+ const BLANK_TAB_SENTINEL = "__mcp-blank-tab__";
136
+
137
+ function _markBlankTabOpened() {
138
+ if (!_ownedTabURLs.has(BLANK_TAB_SENTINEL)) {
139
+ _ownedTabURLs.add(BLANK_TAB_SENTINEL);
140
+ _saveOwnershipFile(_ownedTabURLs);
141
+ }
142
+ }
143
+
130
144
  function _addOwnedURL(url) {
131
145
  if (url && url !== 'about:blank' && url !== 'favorites://') {
132
146
  _ownedTabURLs.add(url);
@@ -697,7 +711,7 @@ async function extensionOrFallback(extensionType, extensionPayload, fallbackFn)
697
711
  throw new Error(msg);
698
712
  } else if (currentUrl && !_isURLOwned(currentUrl)) {
699
713
  // about:blank tabs are owned if we have any tracked tabs (new_tab creates them at about:blank)
700
- const isBlankOwned = (currentUrl === 'about:blank' || currentUrl === 'missing value') && _openedTabs.size > 0;
714
+ const isBlankOwned = (currentUrl === 'about:blank' || currentUrl === 'missing value') && (_openedTabs.size > 0 || _ownedTabURLs.has(BLANK_TAB_SENTINEL));
701
715
  if (!isBlankOwned) {
702
716
  const msg = `⚠️ Tab safety: refusing "${extensionType}" — current tab (${currentUrl}) was not opened by this MCP session. Use safari_new_tab or safari_switch_tab to target your own tab.`;
703
717
  console.error(`[Safari MCP] ${msg}`);
@@ -1354,6 +1368,9 @@ server.tool(
1354
1368
  _addOwnedURL(trackUrl);
1355
1369
  if (url && url !== trackUrl) _addOwnedURL(url); // also own the requested URL (handles www redirects)
1356
1370
  }
1371
+ // Blank tab (no URL requested): persist a restart-surviving ownership marker.
1372
+ const _effectiveNewURL = (result?.url && result.url !== 'about:blank' && result.url !== 'missing value') ? result.url : url;
1373
+ if (!_effectiveNewURL) _markBlankTabOpened();
1357
1374
  return { content: [{ type: "text", text: typeof rawResult === 'string' ? rawResult : JSON.stringify(result) }] };
1358
1375
  }
1359
1376
  );
@@ -1384,7 +1401,7 @@ server.tool(
1384
1401
  const target = parsed.find(t => t.index === index);
1385
1402
  if (target && target.url && !_isURLOwned(target.url)) {
1386
1403
  // about:blank / missing value tabs are owned if tracked in _openedTabs
1387
- const isBlankOwned = (target.url === 'about:blank' || target.url === 'missing value') && _openedTabs.has(index);
1404
+ const isBlankOwned = (target.url === 'about:blank' || target.url === 'missing value') && (_openedTabs.has(index) || _ownedTabURLs.has(BLANK_TAB_SENTINEL));
1388
1405
  if (!isBlankOwned) {
1389
1406
  const msg = `⚠️ Tab safety: refusing switch_tab to index ${index} (${target.url}) — not opened by this MCP session. Use safari_new_tab to open your own tab.`;
1390
1407
  console.error(`[Safari MCP] ${msg}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.11.0",
3
+ "version": "2.11.1",
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",
package/safari.js CHANGED
@@ -107,6 +107,15 @@ function cleanupHelper() {
107
107
  // cleanupHelper runs via process.on("exit"), which fires when index.js calls process.exit().
108
108
  process.on("exit", cleanupHelper);
109
109
  process.on("uncaughtException", (err) => { console.error("Uncaught:", err); cleanupHelper(); process.exit(1); });
110
+ // Unhandled promise rejections must NOT terminate the server. A single failed
111
+ // async operation — e.g. a proxy fetch to the primary instance while it is
112
+ // mid-restart, or an aborted fetch timeout — would otherwise bubble to the
113
+ // uncaughtException handler above (Node's default for unhandled rejections) and
114
+ // exit the whole MCP process, disconnecting every concurrent session. Log and
115
+ // continue: the failed operation is localized, the process itself is healthy.
116
+ process.on("unhandledRejection", (reason) => {
117
+ console.error("[Safari MCP] Unhandled rejection (non-fatal, continuing):", (reason && reason.stack) || reason);
118
+ });
110
119
 
111
120
  // ========== SAFARI RUNNING CHECK ==========
112
121
  // Prevent AppleScript from auto-launching Safari when it's closed