safari-mcp 2.8.9 → 2.9.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.
@@ -285,6 +285,19 @@ async function handleCommand(type, payload) {
285
285
  return targetTab.title;
286
286
  }
287
287
 
288
+ // --- Reload Extension (hot-reload after code changes) ---
289
+ // Allows the MCP server to trigger the extension to reload its own code from disk,
290
+ // bypassing the need for manual Safari → Preferences → Extensions → toggle.
291
+ // The WebSocket will disconnect as a side effect; the extension auto-reconnects.
292
+ case "reload_extension": {
293
+ // Respond BEFORE reload so the MCP server sees a success result.
294
+ // Delay the actual reload by a tick so the response can flush over the wire.
295
+ setTimeout(() => {
296
+ try { browser.runtime.reload(); } catch (_) { chrome.runtime.reload(); }
297
+ }, 50);
298
+ return { reloaded: true, version: browser.runtime.getManifest().version };
299
+ }
300
+
288
301
  case "read_page": {
289
302
  return await execInTab((sel, maxLen) => {
290
303
  if (sel) {
@@ -1287,7 +1300,53 @@ async function handleCommand(type, payload) {
1287
1300
  }
1288
1301
  }
1289
1302
  if (!root) return "Element not found: " + rootSelector;
1290
- let tree = walk(root, 0);
1303
+
1304
+ // TOP-LAYER MODAL DETECTION: when a modal/dialog is open, user intent is almost always
1305
+ // to interact with it, not the page behind. Pages like Google Business Profile, Drive,
1306
+ // Airtable rich dialogs open top-layer content that standard DOM walks miss or bury.
1307
+ // Detect visible modals and, if found, walk them FIRST so their refs appear at the top.
1308
+ let topLayerTree = "";
1309
+ if (!rootSelector) {
1310
+ const seenModals = new Set();
1311
+ const modalSelectors = [
1312
+ 'dialog[open]',
1313
+ '[role="dialog"]:not([aria-hidden="true"])',
1314
+ '[role="alertdialog"]:not([aria-hidden="true"])',
1315
+ '[aria-modal="true"]',
1316
+ '[data-radix-dialog-content]',
1317
+ '[data-headlessui-state*="open"][role="dialog"]',
1318
+ '.MuiDialog-container',
1319
+ // Google overlays: Search/GBP/Drive editors use these markers
1320
+ '[jscontroller][aria-modal]',
1321
+ '[jscontroller][role="dialog"]',
1322
+ 'c-wiz[role="dialog"]',
1323
+ 'c-wiz[aria-modal]'
1324
+ ];
1325
+ for (const sel of modalSelectors) {
1326
+ let nodes;
1327
+ try { nodes = document.querySelectorAll(sel); } catch (_) { continue; }
1328
+ for (const m of nodes) {
1329
+ if (seenModals.has(m)) continue;
1330
+ seenModals.add(m);
1331
+ const cs = window.getComputedStyle(m);
1332
+ if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0") continue;
1333
+ const r = m.getBoundingClientRect();
1334
+ // A real modal covers a meaningful area
1335
+ if (r.width < 150 || r.height < 100) continue;
1336
+ // Tombstones: modals at 0,0 with 0 size are placeholders
1337
+ if (r.width === 0 || r.height === 0) continue;
1338
+ // Nested modals: skip if we already included a parent
1339
+ let isNested = false;
1340
+ for (const other of seenModals) {
1341
+ if (other !== m && other.contains(m)) { isNested = true; break; }
1342
+ }
1343
+ if (isNested) continue;
1344
+ topLayerTree += walk(m, 0);
1345
+ }
1346
+ }
1347
+ }
1348
+
1349
+ let tree = topLayerTree + walk(root, 0);
1291
1350
  // Shadow roots are now walked INLINE inside walk() — no separate walkShadows needed.
1292
1351
  // Walk same-origin iframes
1293
1352
  const iframes = document.querySelectorAll("iframe");
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Safari MCP Bridge",
4
4
  "description": "Connects AI agents to Safari — fast JS execution, screenshots, clicks with your real cookies/logins",
5
- "version": "2.7.2",
5
+ "version": "2.9.1",
6
6
  "icons": {
7
7
  "48": "images/icon-48.png",
8
8
  "96": "images/icon-96.png",
package/index.js CHANGED
@@ -666,6 +666,8 @@ const _nullMeansFailure = new Set([
666
666
  const _noOwnershipCheck = new Set([
667
667
  // Tab management
668
668
  "new_tab", "list_tabs", "close_tab", "switch_tab",
669
+ // Extension self-management (doesn't touch tabs)
670
+ "reload_extension",
669
671
  // Read-only — don't modify the page
670
672
  "read_page", "get_source", "snapshot", "accessibility_snapshot",
671
673
  "get_element", "query_all", "screenshot", "screenshot_element",
@@ -1256,6 +1258,19 @@ server.tool(
1256
1258
  }
1257
1259
  );
1258
1260
 
1261
+ server.tool(
1262
+ "safari_reload_extension",
1263
+ "Hot-reload the Safari MCP Bridge extension — forces it to reload its own code from disk without requiring manual Safari Preferences → Extensions → toggle. Use after editing extension/background.js or extension/content.js in the safari-mcp repo. The extension briefly disconnects during reload and auto-reconnects within ~2 seconds. NOTE: this tool itself requires the extension version already installed to support the `reload_extension` command (added in v2.9.1+). If your extension is older, trigger a manual reload once to pick up this feature.",
1264
+ {},
1265
+ async () => {
1266
+ const result = await extensionOrFallback(
1267
+ "reload_extension", {},
1268
+ async () => "Extension fallback not available — this command requires the Safari MCP Bridge extension."
1269
+ );
1270
+ return { content: [{ type: "text", text: typeof result === 'string' ? result : JSON.stringify(result) }] };
1271
+ }
1272
+ );
1273
+
1259
1274
  server.tool(
1260
1275
  "safari_new_tab",
1261
1276
  "Open a new tab, optionally with a URL",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.8.9",
3
+ "version": "2.9.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",