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.
- package/extension/background.js +60 -1
- package/extension/manifest.json +1 -1
- package/index.js +15 -0
- package/package.json +1 -1
package/extension/background.js
CHANGED
|
@@ -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
|
-
|
|
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");
|
package/extension/manifest.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|