safari-mcp 2.3.0 → 2.4.0
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 +16 -10
- package/index.js +28 -9
- package/mcp-helpers.js +2 -0
- package/package.json +1 -1
- package/safari.js +14 -306
package/extension/background.js
CHANGED
|
@@ -242,6 +242,7 @@ async function handleCommand(type, payload) {
|
|
|
242
242
|
await browser.tabs.goBack(tabId);
|
|
243
243
|
await waitForTabSettled(tabId, 3000);
|
|
244
244
|
const updated = await browser.tabs.get(tabId);
|
|
245
|
+
_setSessionTab(sessionId, updated.id, updated.url);
|
|
245
246
|
return { title: updated.title, url: updated.url };
|
|
246
247
|
}
|
|
247
248
|
|
|
@@ -249,6 +250,7 @@ async function handleCommand(type, payload) {
|
|
|
249
250
|
await browser.tabs.goForward(tabId);
|
|
250
251
|
await waitForTabSettled(tabId, 3000);
|
|
251
252
|
const updated = await browser.tabs.get(tabId);
|
|
253
|
+
_setSessionTab(sessionId, updated.id, updated.url);
|
|
252
254
|
return { title: updated.title, url: updated.url };
|
|
253
255
|
}
|
|
254
256
|
|
|
@@ -524,7 +526,7 @@ async function handleCommand(type, payload) {
|
|
|
524
526
|
|
|
525
527
|
// --- Visibility check ---
|
|
526
528
|
const cs = window.getComputedStyle(el);
|
|
527
|
-
if (cs.display === "none" || cs.visibility === "hidden" || cs.
|
|
529
|
+
if (cs.display === "none" || cs.visibility === "hidden" || cs.visibility === "collapse" || parseFloat(cs.opacity) === 0) {
|
|
528
530
|
return "Element not visible (display/visibility/opacity)";
|
|
529
531
|
}
|
|
530
532
|
|
|
@@ -1647,16 +1649,20 @@ async function _verifyProfileMatch(expectedProfile) {
|
|
|
1647
1649
|
await new Promise(r => setTimeout(r, 500));
|
|
1648
1650
|
|
|
1649
1651
|
// Ask the server to verify which profile has this nonce
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1652
|
+
let verifyRes;
|
|
1653
|
+
try {
|
|
1654
|
+
verifyRes = await fetch(`${HTTP_URL}/verify-profile`, {
|
|
1655
|
+
method: "POST",
|
|
1656
|
+
headers: { "Content-Type": "application/json" },
|
|
1657
|
+
body: JSON.stringify({ nonce, expectedProfile }),
|
|
1658
|
+
signal: AbortSignal.timeout(5000),
|
|
1659
|
+
});
|
|
1660
|
+
} finally {
|
|
1661
|
+
// Always clean up test tab — even if fetch fails
|
|
1662
|
+
await browser.tabs.remove(checkTab.id).catch(() => {});
|
|
1663
|
+
}
|
|
1658
1664
|
|
|
1659
|
-
if (verifyRes.ok) {
|
|
1665
|
+
if (verifyRes && verifyRes.ok) {
|
|
1660
1666
|
const result = await verifyRes.json();
|
|
1661
1667
|
if (result.match) {
|
|
1662
1668
|
await browser.storage.local.set({ mcpVerifiedProfile: expectedProfile });
|
package/index.js
CHANGED
|
@@ -17,6 +17,8 @@ import { readFileSync } from "node:fs";
|
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
|
|
20
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB cap on POST body — prevents DoS
|
|
21
|
+
|
|
20
22
|
// ========== SINGLETON: kill stale instances from previous sessions ==========
|
|
21
23
|
// NOTE: Claude Code VSCode may start 2 instances simultaneously (~40ms apart).
|
|
22
24
|
// Only kill instances running >10 seconds (truly stale), not fresh siblings.
|
|
@@ -94,7 +96,7 @@ function _startMemoryMonitor() {
|
|
|
94
96
|
// Use pgrep + ps directly — avoids spawning bash, grep, awk pipeline
|
|
95
97
|
let psOut = "0";
|
|
96
98
|
try {
|
|
97
|
-
const pids = execFileSync("pgrep", ["-
|
|
99
|
+
const pids = execFileSync("pgrep", ["-Ef", "WebKit|WebContent"], { encoding: "utf8" }).trim();
|
|
98
100
|
if (pids) {
|
|
99
101
|
const pidList = pids.split("\n").join(",");
|
|
100
102
|
psOut = execFileSync("ps", ["-p", pidList, "-o", "rss="], { encoding: "utf8" }).trim();
|
|
@@ -111,8 +113,17 @@ function _startMemoryMonitor() {
|
|
|
111
113
|
}
|
|
112
114
|
if (oldestIdx !== null) {
|
|
113
115
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
// Re-resolve by URL before closing (indices shift as tabs change)
|
|
117
|
+
const info = _openedTabs.get(oldestIdx);
|
|
118
|
+
if (info?.url) {
|
|
119
|
+
const tabs = await safari.listTabs();
|
|
120
|
+
const parsed = typeof tabs === 'string' ? JSON.parse(tabs) : tabs;
|
|
121
|
+
const match = parsed.find(t => t.url === info.url);
|
|
122
|
+
if (match) {
|
|
123
|
+
safari.setActiveTabIndex(match.index);
|
|
124
|
+
await safari.closeTab();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
116
127
|
} catch {}
|
|
117
128
|
_untrackTab(oldestIdx);
|
|
118
129
|
}
|
|
@@ -123,8 +134,11 @@ function _startMemoryMonitor() {
|
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
// Cleanup on exit
|
|
137
|
+
let _cleaningUp = false;
|
|
126
138
|
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
127
139
|
process.on(sig, async () => {
|
|
140
|
+
if (_cleaningUp) return; // Prevent double-exit on rapid signal repeat
|
|
141
|
+
_cleaningUp = true;
|
|
128
142
|
await _cleanupTabs();
|
|
129
143
|
process.exit(0);
|
|
130
144
|
});
|
|
@@ -250,7 +264,7 @@ try {
|
|
|
250
264
|
// POST /result — extension sends command result
|
|
251
265
|
if (req.method === "POST" && req.url === "/result") {
|
|
252
266
|
let body = "";
|
|
253
|
-
req.on("data", (chunk) =>
|
|
267
|
+
req.on("data", (chunk) => { body += chunk; if (body.length > MAX_BODY_SIZE) { res.writeHead(413); res.end("Payload too large"); req.destroy(); } });
|
|
254
268
|
req.on("end", () => {
|
|
255
269
|
try {
|
|
256
270
|
const msg = JSON.parse(body);
|
|
@@ -291,7 +305,7 @@ try {
|
|
|
291
305
|
// POST /verify-profile — extension asks server to check which profile has a nonce tab
|
|
292
306
|
if (req.method === "POST" && req.url === "/verify-profile") {
|
|
293
307
|
let body = "";
|
|
294
|
-
req.on("data", (chunk) =>
|
|
308
|
+
req.on("data", (chunk) => { body += chunk; if (body.length > MAX_BODY_SIZE) { res.writeHead(413); res.end("Payload too large"); req.destroy(); } });
|
|
295
309
|
req.on("end", async () => {
|
|
296
310
|
try {
|
|
297
311
|
const { nonce, expectedProfile } = JSON.parse(body);
|
|
@@ -299,9 +313,9 @@ try {
|
|
|
299
313
|
const safeNonce = String(nonce).replace(/[^0-9]/g, ''); // nonce is numeric only
|
|
300
314
|
const safeProfile = (expectedProfile || "").replace(/[^\p{L}\p{N}\s\-_]/gu, ''); // whitelist: letters, numbers, spaces, hyphens, underscores
|
|
301
315
|
// Check via AppleScript — look for the nonce in the profile window
|
|
302
|
-
const { execFile } = await import("node:child_process");
|
|
303
|
-
const { promisify } = await import("node:util");
|
|
304
|
-
const execFileAsync =
|
|
316
|
+
const { execFile: execFileCb } = await import("node:child_process");
|
|
317
|
+
const { promisify: pfy } = await import("node:util");
|
|
318
|
+
const execFileAsync = pfy(execFileCb);
|
|
305
319
|
const script = `tell application "Safari"
|
|
306
320
|
repeat with w in every window
|
|
307
321
|
repeat with t in every tab of w
|
|
@@ -343,7 +357,7 @@ try {
|
|
|
343
357
|
// POST /proxy-command — secondary instances send commands through primary
|
|
344
358
|
if (req.method === "POST" && req.url === "/proxy-command") {
|
|
345
359
|
let body = "";
|
|
346
|
-
req.on("data", (chunk) =>
|
|
360
|
+
req.on("data", (chunk) => { body += chunk; if (body.length > MAX_BODY_SIZE) { res.writeHead(413); res.end("Payload too large"); req.destroy(); } });
|
|
347
361
|
req.on("end", async () => {
|
|
348
362
|
try {
|
|
349
363
|
const { type, payload } = JSON.parse(body);
|
|
@@ -391,6 +405,7 @@ setTimeout(() => {
|
|
|
391
405
|
}, 2000);
|
|
392
406
|
|
|
393
407
|
async function _checkPrimaryExtension() {
|
|
408
|
+
if (_isExtensionHost) return; // Already hosting — stop polling
|
|
394
409
|
try {
|
|
395
410
|
const res = await fetch(`http://127.0.0.1:${HTTP_PORT}/proxy-check`, {
|
|
396
411
|
method: "GET",
|
|
@@ -1126,12 +1141,16 @@ server.tool(
|
|
|
1126
1141
|
if (resolved && resolved.url !== 'about:blank') {
|
|
1127
1142
|
if (urlContains && !resolved.url.includes(urlContains)) continue;
|
|
1128
1143
|
await extensionOrFallback("switch_tab", { index: resolved.index }, () => safari.switchTab(resolved.index));
|
|
1144
|
+
safari.setActiveTabIndex(resolved.index);
|
|
1145
|
+
safari.setActiveTabURL(resolved.url);
|
|
1129
1146
|
return { content: [{ type: "text", text: `Found new tab: ${resolved.title} (${resolved.url})` }] };
|
|
1130
1147
|
}
|
|
1131
1148
|
continue;
|
|
1132
1149
|
}
|
|
1133
1150
|
if (urlContains && !tab.url.includes(urlContains)) continue;
|
|
1134
1151
|
await extensionOrFallback("switch_tab", { index: tab.index }, () => safari.switchTab(tab.index));
|
|
1152
|
+
safari.setActiveTabIndex(tab.index);
|
|
1153
|
+
safari.setActiveTabURL(tab.url);
|
|
1135
1154
|
return { content: [{ type: "text", text: `Found new tab: ${tab.title} (${tab.url})` }] };
|
|
1136
1155
|
}
|
|
1137
1156
|
}
|
package/mcp-helpers.js
CHANGED
|
@@ -75,6 +75,8 @@ if (window.__mcpVersion !== 5) {
|
|
|
75
75
|
if (!window.mcpIsVisible(el)) return false;
|
|
76
76
|
if (el.disabled || el.getAttribute('aria-disabled') === 'true') return false;
|
|
77
77
|
if (el.matches && el.matches('input[type="hidden"]')) return false;
|
|
78
|
+
var cs = window.getComputedStyle(el);
|
|
79
|
+
if (cs && cs.pointerEvents === 'none') return false;
|
|
78
80
|
return true;
|
|
79
81
|
};
|
|
80
82
|
window.mcpPickActionable = function(el) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
|
-
import { join, dirname } from "node:path";
|
|
9
|
+
import { join, dirname, resolve as resolvePath } from "node:path";
|
|
10
10
|
import { readFile, writeFile, unlink, appendFile } from "node:fs/promises";
|
|
11
11
|
import { readFileSync } from "node:fs";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
@@ -84,11 +84,9 @@ function cleanupHelper() {
|
|
|
84
84
|
_helperProc = null;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
// Signal handlers (SIGINT/SIGTERM/SIGHUP) are registered in index.js only.
|
|
88
|
+
// cleanupHelper runs via process.on("exit"), which fires when index.js calls process.exit().
|
|
87
89
|
process.on("exit", cleanupHelper);
|
|
88
|
-
process.on("SIGINT", () => { cleanupHelper(); process.exit(0); });
|
|
89
|
-
process.on("SIGTERM", () => { cleanupHelper(); process.exit(0); });
|
|
90
|
-
// SIGHUP = terminal closed, uncaught = crash — still clean up
|
|
91
|
-
process.on("SIGHUP", () => { cleanupHelper(); process.exit(0); });
|
|
92
90
|
process.on("uncaughtException", (err) => { console.error("Uncaught:", err); cleanupHelper(); process.exit(1); });
|
|
93
91
|
|
|
94
92
|
// ========== ACTIVE TAB TRACKING ==========
|
|
@@ -388,7 +386,7 @@ function _osascriptFastHelper(script, timeout) {
|
|
|
388
386
|
function _helperNativeClick(x, y, doubleClick = false, windowId = 0, timeout = 5000) {
|
|
389
387
|
return new Promise((resolve, reject) => {
|
|
390
388
|
if (!_helperProc) startHelper();
|
|
391
|
-
if (!_helperProc || !_helperProc.stdin) {
|
|
389
|
+
if (!_helperProc || !_helperProc.stdin || !_helperProc.stdin.writable) {
|
|
392
390
|
reject(new Error("safari-helper not available for native click"));
|
|
393
391
|
return;
|
|
394
392
|
}
|
|
@@ -698,301 +696,8 @@ export async function getPageSource({ maxLength = 200000 } = {}) {
|
|
|
698
696
|
// Loaded from mcp-helpers.js at startup — enables syntax highlighting, linting, and easier maintenance
|
|
699
697
|
const INJECT_MCP_HELPERS = readFileSync(join(__dirname, 'mcp-helpers.js'), 'utf8');
|
|
700
698
|
|
|
701
|
-
// NOTE:
|
|
702
|
-
|
|
703
|
-
const _LEGACY_INLINE_START = `if(window.__mcpVersion!==5){window.__mcpVersion=5;
|
|
704
|
-
window.__mcpRefs=window.__mcpRefs||{};
|
|
705
|
-
window.__mcpCachedRoots=null;
|
|
706
|
-
window.__mcpRootsDirty=true;
|
|
707
|
-
if(!window.__mcpRootsObserver){
|
|
708
|
-
window.__mcpRootsObserver=new MutationObserver(function(){window.__mcpRootsDirty=true;});
|
|
709
|
-
window.__mcpRootsObserver.observe(document.documentElement,{childList:true,subtree:true});
|
|
710
|
-
}
|
|
711
|
-
window.mcpCollectRoots=function(){
|
|
712
|
-
if(!window.__mcpRootsDirty&&window.__mcpCachedRoots)return window.__mcpCachedRoots;
|
|
713
|
-
var roots=[];
|
|
714
|
-
function collect(root){
|
|
715
|
-
roots.push(root);
|
|
716
|
-
var all=root.querySelectorAll('*');
|
|
717
|
-
for(var i=0;i<all.length;i++){
|
|
718
|
-
if(all[i].shadowRoot)collect(all[i].shadowRoot);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
collect(document);
|
|
722
|
-
var iframes=document.querySelectorAll('iframe');
|
|
723
|
-
for(var i=0;i<iframes.length;i++){
|
|
724
|
-
try{var doc=iframes[i].contentDocument;if(doc)collect(doc);}catch(e){}
|
|
725
|
-
}
|
|
726
|
-
window.__mcpCachedRoots=roots;
|
|
727
|
-
window.__mcpRootsDirty=false;
|
|
728
|
-
return roots;
|
|
729
|
-
};
|
|
730
|
-
window.mcpQuerySelectorDeep=function(selector){
|
|
731
|
-
try{
|
|
732
|
-
var direct=document.querySelector(selector);
|
|
733
|
-
if(direct)return direct;
|
|
734
|
-
}catch(e){return null}
|
|
735
|
-
var roots=window.mcpCollectRoots();
|
|
736
|
-
for(var i=1;i<roots.length;i++){
|
|
737
|
-
try{
|
|
738
|
-
var found=roots[i].querySelector(selector);
|
|
739
|
-
if(found)return found;
|
|
740
|
-
}catch(e){}
|
|
741
|
-
}
|
|
742
|
-
var iframes=document.querySelectorAll('iframe');
|
|
743
|
-
for(var i=0;i<iframes.length;i++){
|
|
744
|
-
try{
|
|
745
|
-
var doc=iframes[i].contentDocument;
|
|
746
|
-
if(doc){var found=doc.querySelector(selector);if(found)return found;}
|
|
747
|
-
}catch(e){}
|
|
748
|
-
}
|
|
749
|
-
return null;
|
|
750
|
-
};
|
|
751
|
-
window.mcpElementFromPoint=function(x,y){
|
|
752
|
-
var el=document.elementFromPoint(x,y);
|
|
753
|
-
while(el&&el.shadowRoot){
|
|
754
|
-
try{
|
|
755
|
-
if(typeof el.shadowRoot.elementFromPoint!=='function')break;
|
|
756
|
-
var inner=el.shadowRoot.elementFromPoint(x,y);
|
|
757
|
-
if(!inner||inner===el)break;
|
|
758
|
-
el=inner;
|
|
759
|
-
}catch(e){break}
|
|
760
|
-
}
|
|
761
|
-
return el;
|
|
762
|
-
};
|
|
763
|
-
window.mcpIsVisible=function(el){
|
|
764
|
-
if(!el||el.nodeType!==1||!el.isConnected)return false;
|
|
765
|
-
var cs=window.getComputedStyle(el);
|
|
766
|
-
if(!cs||cs.display==='none'||cs.visibility==='hidden'||cs.visibility==='collapse'||cs.pointerEvents==='none'||cs.opacity==='0')return false;
|
|
767
|
-
var r=el.getBoundingClientRect();
|
|
768
|
-
return r.width>0&&r.height>0;
|
|
769
|
-
};
|
|
770
|
-
window.mcpIsActionable=function(el){
|
|
771
|
-
if(!window.mcpIsVisible(el))return false;
|
|
772
|
-
if(el.disabled||el.getAttribute('aria-disabled')==='true')return false;
|
|
773
|
-
if(el.matches&&el.matches('input[type="hidden"]'))return false;
|
|
774
|
-
return true;
|
|
775
|
-
};
|
|
776
|
-
window.mcpPickActionable=function(el){
|
|
777
|
-
var node=el&&el.nodeType===1?el:(el&&el.parentElement)||null;
|
|
778
|
-
while(node){
|
|
779
|
-
if(window.mcpIsActionable(node)&&node.matches&&node.matches('a[href],button,input:not([type="hidden"]),textarea,select,summary,label,option,[role],[onclick],[tabindex],[contenteditable=""],[contenteditable="true"]'))return node;
|
|
780
|
-
node=node.parentElement;
|
|
781
|
-
}
|
|
782
|
-
return el&&el.nodeType===1?el:null;
|
|
783
|
-
};
|
|
784
|
-
window.mcpFindByAttr=function(attr,value,selector){
|
|
785
|
-
if(!value)return null;
|
|
786
|
-
var roots=window.mcpCollectRoots();
|
|
787
|
-
var sel=selector||('['+attr+']');
|
|
788
|
-
for(var i=0;i<roots.length;i++){
|
|
789
|
-
var els=roots[i].querySelectorAll(sel);
|
|
790
|
-
for(var j=0;j<els.length;j++){
|
|
791
|
-
var current=attr==='href'?els[j].href:els[j].getAttribute(attr);
|
|
792
|
-
if(current===value&&window.mcpIsVisible(els[j]))return els[j];
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return null;
|
|
796
|
-
};
|
|
797
|
-
window.mcpNormalizeText=function(value){
|
|
798
|
-
return String(value||'').replace(/\\s+/g,' ').trim();
|
|
799
|
-
};
|
|
800
|
-
window.mcpResolveTarget=function(el){
|
|
801
|
-
var base=window.mcpPickActionable(el)||el;
|
|
802
|
-
if(!base)return null;
|
|
803
|
-
try{base.scrollIntoView({block:'center',inline:'center'});}catch(e){}
|
|
804
|
-
var r=base.getBoundingClientRect();
|
|
805
|
-
var dx=Math.min(12,Math.max(4,r.width/4));
|
|
806
|
-
var dy=Math.min(12,Math.max(4,r.height/4));
|
|
807
|
-
var pts=[
|
|
808
|
-
[r.left+r.width/2,r.top+r.height/2],
|
|
809
|
-
[r.left+dx,r.top+r.height/2],
|
|
810
|
-
[r.right-dx,r.top+r.height/2],
|
|
811
|
-
[r.left+r.width/2,r.top+dy],
|
|
812
|
-
[r.left+r.width/2,r.bottom-dy]
|
|
813
|
-
];
|
|
814
|
-
for(var i=0;i<pts.length;i++){
|
|
815
|
-
var x=pts[i][0],y=pts[i][1];
|
|
816
|
-
if(x<0||y<0||x>window.innerWidth||y>window.innerHeight)continue;
|
|
817
|
-
var hit=window.mcpElementFromPoint(x,y);
|
|
818
|
-
if(!hit)continue;
|
|
819
|
-
if(base===hit||base.contains(hit)||hit.contains(base))return window.mcpPickActionable(hit)||hit;
|
|
820
|
-
// Overlay pattern: transparent button/link overlays a text label (e.g. dropdown items).
|
|
821
|
-
// If the hit is an interactive element sharing a nearby common ancestor, prefer it.
|
|
822
|
-
var hitAction=window.mcpPickActionable(hit);
|
|
823
|
-
if(hitAction&&hitAction!==base&&hitAction.matches&&hitAction.matches('button,a[href],[role="button"],[role="option"],[role="menuitem"]')){
|
|
824
|
-
var p=base.parentElement;
|
|
825
|
-
for(var k=0;k<4&&p;k++){
|
|
826
|
-
if(p.contains(hitAction))return hitAction;
|
|
827
|
-
p=p.parentElement;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
return base;
|
|
832
|
-
};
|
|
833
|
-
window.mcpClick=function(el){
|
|
834
|
-
var target=window.mcpResolveTarget(el);
|
|
835
|
-
if(!window.mcpIsActionable(target))return false;
|
|
836
|
-
var r=target.getBoundingClientRect();
|
|
837
|
-
var x=r.left+r.width/2,y=r.top+r.height/2;
|
|
838
|
-
var beforeUrl=location.href;
|
|
839
|
-
var anchor=target.closest?target.closest('a[href]'):null;
|
|
840
|
-
var href=anchor&&anchor.href&&!anchor.href.startsWith('javascript:')?anchor.href:'';
|
|
841
|
-
var s={bubbles:true,cancelable:true,composed:true,view:window,clientX:x,clientY:y,button:0,detail:1};
|
|
842
|
-
var p={...s,pointerId:1,pointerType:'mouse',isPrimary:true,width:1,height:1,pressure:0.5};
|
|
843
|
-
target.dispatchEvent(new PointerEvent('pointerover',{...p,buttons:0}));
|
|
844
|
-
target.dispatchEvent(new MouseEvent('mouseover',{...s,buttons:0}));
|
|
845
|
-
target.dispatchEvent(new PointerEvent('pointerenter',{...p,buttons:0}));
|
|
846
|
-
target.dispatchEvent(new MouseEvent('mouseenter',{...s,buttons:0}));
|
|
847
|
-
target.dispatchEvent(new PointerEvent('pointermove',{...p,buttons:0}));
|
|
848
|
-
target.dispatchEvent(new MouseEvent('mousemove',{...s,buttons:0}));
|
|
849
|
-
target.dispatchEvent(new PointerEvent('pointerdown',{...p,buttons:1}));
|
|
850
|
-
target.dispatchEvent(new MouseEvent('mousedown',{...s,buttons:1}));
|
|
851
|
-
if(target.focus){try{target.focus({preventScroll:true});}catch(e){try{target.focus();}catch(_){}}}
|
|
852
|
-
target.dispatchEvent(new PointerEvent('pointerup',{...p,buttons:0,pressure:0}));
|
|
853
|
-
target.dispatchEvent(new MouseEvent('mouseup',{...s,buttons:0}));
|
|
854
|
-
try{
|
|
855
|
-
if(typeof target.click==='function'){
|
|
856
|
-
target.click();
|
|
857
|
-
if(href&&href!==beforeUrl){
|
|
858
|
-
location.href=href;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}catch(e){}
|
|
862
|
-
target.dispatchEvent(new MouseEvent('click',{...s,buttons:0}));
|
|
863
|
-
if(href&&href!==beforeUrl){
|
|
864
|
-
location.href=href;
|
|
865
|
-
}
|
|
866
|
-
var form=target.closest?target.closest('form'):null;
|
|
867
|
-
if(form&&(target.type==='submit'||(target.tagName==='BUTTON'&&target.type!=='button'&&target.type!=='reset'))){
|
|
868
|
-
try{form.requestSubmit?form.requestSubmit(target.type==='submit'?target:undefined):form.submit();}catch(e){}
|
|
869
|
-
}
|
|
870
|
-
return true;
|
|
871
|
-
};
|
|
872
|
-
window.mcpReactClick=function(el){
|
|
873
|
-
var startNode=window.mcpResolveTarget(el)||el;
|
|
874
|
-
if(!startNode)return false;
|
|
875
|
-
function makeSynth(targetNode, type){
|
|
876
|
-
var r=targetNode.getBoundingClientRect();
|
|
877
|
-
return{type:type||'click',target:targetNode,currentTarget:targetNode,
|
|
878
|
-
clientX:r.left+r.width/2,clientY:r.top+r.height/2,pageX:r.left+r.width/2+window.scrollX,pageY:r.top+r.height/2+window.scrollY,
|
|
879
|
-
preventDefault:function(){},stopPropagation:function(){},nativeEvent:new MouseEvent(type||'click'),persist:function(){},bubbles:true,cancelable:true};
|
|
880
|
-
}
|
|
881
|
-
// For React radio/checkbox inputs, onChange is the primary handler (not onClick)
|
|
882
|
-
var isToggle=startNode.tagName==='INPUT'&&(startNode.type==='radio'||startNode.type==='checkbox');
|
|
883
|
-
function tryOnChange(propsObj,targetNode){
|
|
884
|
-
if(!isToggle||!propsObj.onChange)return false;
|
|
885
|
-
var newChecked=startNode.type==='radio'?true:!startNode.checked;
|
|
886
|
-
propsObj.onChange({target:{value:startNode.value,checked:newChecked,type:startNode.type,name:startNode.name,id:startNode.id,tagName:'INPUT'},
|
|
887
|
-
currentTarget:targetNode,preventDefault:function(){},stopPropagation:function(){},nativeEvent:new Event('change'),persist:function(){},bubbles:true,cancelable:true,type:'change'});
|
|
888
|
-
return true;
|
|
889
|
-
}
|
|
890
|
-
var node=startNode;
|
|
891
|
-
for(var depth=0;depth<15&&node;depth++){
|
|
892
|
-
var pk=Object.keys(node).find(function(k){return k.startsWith('__reactProps$')});
|
|
893
|
-
if(pk&&node[pk]){
|
|
894
|
-
var props=node[pk];
|
|
895
|
-
if(tryOnChange(props,node))return true;
|
|
896
|
-
if(props.onClick){props.onClick(makeSynth(node,'click'));return true}
|
|
897
|
-
if(props.onMouseDown){props.onMouseDown(makeSynth(node,'mousedown'));return true}
|
|
898
|
-
if(props.onPointerDown){props.onPointerDown(makeSynth(node,'pointerdown'));return true}
|
|
899
|
-
}
|
|
900
|
-
var fk=Object.keys(node).find(function(k){return k.startsWith('__reactFiber$')||k.startsWith('__reactInternalInstance$')});
|
|
901
|
-
if(fk){
|
|
902
|
-
var f=node[fk];
|
|
903
|
-
while(f){
|
|
904
|
-
if(f.memoizedProps){
|
|
905
|
-
if(tryOnChange(f.memoizedProps,node))return true;
|
|
906
|
-
if(f.memoizedProps.onClick){f.memoizedProps.onClick(makeSynth(node,'click'));return true}
|
|
907
|
-
if(f.memoizedProps.onMouseDown){f.memoizedProps.onMouseDown(makeSynth(node,'mousedown'));return true}
|
|
908
|
-
if(f.memoizedProps.onPointerDown){f.memoizedProps.onPointerDown(makeSynth(node,'pointerdown'));return true}
|
|
909
|
-
}
|
|
910
|
-
f=f.return;
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
node=node.parentElement;
|
|
914
|
-
}
|
|
915
|
-
return false;
|
|
916
|
-
};
|
|
917
|
-
window.mcpClickWithReact=function(el){
|
|
918
|
-
var target=window.mcpResolveTarget(el)||el;
|
|
919
|
-
var reactFired=false;
|
|
920
|
-
try{reactFired=window.mcpReactClick(target);}catch(e){}
|
|
921
|
-
var anchor=target&&target.closest?target.closest('a[href]'):null;
|
|
922
|
-
if(!reactFired||anchor)window.mcpClick(target);
|
|
923
|
-
return target;
|
|
924
|
-
};
|
|
925
|
-
window.mcpFindRef=function(ref){
|
|
926
|
-
var el=window.mcpQuerySelectorDeep('[data-mcp-ref="'+ref+'"]');
|
|
927
|
-
if(el)return el;
|
|
928
|
-
if(!window.__mcpRefs||!window.__mcpRefs[ref])return null;
|
|
929
|
-
var m=window.__mcpRefs[ref];
|
|
930
|
-
if(m.id){el=window.mcpFindByAttr('id',m.id);if(el)return el}
|
|
931
|
-
if(m.testid){el=window.mcpFindByAttr('data-testid',m.testid);if(el)return el}
|
|
932
|
-
if(m.nameAttr){el=window.mcpFindByAttr('name',m.nameAttr);if(el)return el}
|
|
933
|
-
if(m.href){el=window.mcpFindByAttr('href',m.href,'a[href]');if(el)return el}
|
|
934
|
-
if(m.al){el=window.mcpFindByAttr('aria-label',m.al);if(el)return el}
|
|
935
|
-
if(m.ph){el=window.mcpFindByAttr('placeholder',m.ph);if(el)return el}
|
|
936
|
-
if(m.text){
|
|
937
|
-
el=window.mcpFindText(m.text,true)||window.mcpFindText(m.text,false);
|
|
938
|
-
if(el)return el;
|
|
939
|
-
}
|
|
940
|
-
if(m.cx!==undefined&&m.cy!==undefined){
|
|
941
|
-
try{
|
|
942
|
-
window.scrollTo(window.scrollX,Math.max(0,m.cy-window.innerHeight/2));
|
|
943
|
-
}catch(e){}
|
|
944
|
-
el=window.mcpElementFromPoint(m.cx-window.scrollX,m.cy-window.scrollY);
|
|
945
|
-
if(el)return window.mcpPickActionable(el)||el;
|
|
946
|
-
}
|
|
947
|
-
return null;
|
|
948
|
-
};
|
|
949
|
-
window.mcpFindText=function(text,exact){
|
|
950
|
-
var needle=window.mcpNormalizeText(text);
|
|
951
|
-
var best=null,bestScore=Infinity;
|
|
952
|
-
function consider(node){
|
|
953
|
-
var target=window.mcpPickActionable(node)||node;
|
|
954
|
-
if(!window.mcpIsVisible(target))return;
|
|
955
|
-
var r=target.getBoundingClientRect();
|
|
956
|
-
var area=r.width*r.height;
|
|
957
|
-
var interactive=target.matches&&target.matches('a[href],button,input:not([type="hidden"]),textarea,select,summary,label,[role=button],[role=link],[role=tab],[onclick],[tabindex]');
|
|
958
|
-
var score=area+(interactive?0:1000000);
|
|
959
|
-
if(score<bestScore){best=target;bestScore=score}
|
|
960
|
-
}
|
|
961
|
-
var roots=window.mcpCollectRoots();
|
|
962
|
-
for(var i=0;i<roots.length;i++){
|
|
963
|
-
var attrEls=roots[i].querySelectorAll('[aria-label],[placeholder],[title],[data-testid],[alt]');
|
|
964
|
-
for(var j=0;j<attrEls.length;j++){
|
|
965
|
-
var a=attrEls[j];
|
|
966
|
-
var vals=[a.getAttribute('aria-label'),a.getAttribute('placeholder'),a.getAttribute('title'),a.getAttribute('data-testid'),a.getAttribute('alt')];
|
|
967
|
-
for(var k=0;k<vals.length;k++){
|
|
968
|
-
var val=vals[k];
|
|
969
|
-
if(!val)continue;
|
|
970
|
-
var normalized=window.mcpNormalizeText(val);
|
|
971
|
-
if(exact?normalized===needle:normalized.includes(needle)){consider(a);break}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
var tw=document.createTreeWalker(roots[i],NodeFilter.SHOW_TEXT,null);
|
|
975
|
-
while(tw.nextNode()){
|
|
976
|
-
var n=tw.currentNode;
|
|
977
|
-
var t=window.mcpNormalizeText(n.textContent);
|
|
978
|
-
if(!t)continue;
|
|
979
|
-
if(exact?(t!==needle):!t.includes(needle))continue;
|
|
980
|
-
if(n.parentElement)consider(n.parentElement);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
if(!best){
|
|
984
|
-
var allEls=document.querySelectorAll('*');
|
|
985
|
-
for(var i=0;i<allEls.length;i++){
|
|
986
|
-
var el=allEls[i];
|
|
987
|
-
var it=window.mcpNormalizeText(el.innerText);
|
|
988
|
-
if(!it)continue;
|
|
989
|
-
if(exact?(it!==needle):!it.includes(needle))continue;
|
|
990
|
-
consider(el);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
return best;
|
|
994
|
-
};
|
|
995
|
-
}`;
|
|
699
|
+
// NOTE: ~300 lines of legacy inline helpers were here — now in mcp-helpers.js (deleted in v2.4.0)
|
|
700
|
+
|
|
996
701
|
|
|
997
702
|
// Precomputed escaped helper string — avoids re-escaping ~4KB on every injection call
|
|
998
703
|
const _HELPERS_ESCAPED = INJECT_MCP_HELPERS
|
|
@@ -1221,6 +926,7 @@ export async function nativeClick({ selector, text, x, y, ref, doubleClick = fal
|
|
|
1221
926
|
|
|
1222
927
|
export async function fill({ selector, value, ref }) {
|
|
1223
928
|
if (ref) selector = refSelector(ref);
|
|
929
|
+
if (!selector) throw new Error("fill requires selector or ref");
|
|
1224
930
|
const sel = selector.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1225
931
|
// Proper escaping order: backslashes first, then quotes
|
|
1226
932
|
const val = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
|
|
@@ -1680,7 +1386,7 @@ export async function screenshot({ fullPage = false } = {}) {
|
|
|
1680
1386
|
}
|
|
1681
1387
|
// Compress: convert PNG to JPEG (50% quality) + resize to max 1200px width
|
|
1682
1388
|
// Cuts ~600KB PNG → ~60KB JPEG — critical for staying under 20MB context limit
|
|
1683
|
-
const jpgFile = tmpFile.replace(
|
|
1389
|
+
const jpgFile = tmpFile.replace(/\.png$/, '.jpg');
|
|
1684
1390
|
try {
|
|
1685
1391
|
await execFileAsync("sips", [
|
|
1686
1392
|
"-s", "format", "jpeg",
|
|
@@ -2170,8 +1876,7 @@ export async function drag({ sourceSelector, targetSelector, sourceX, sourceY, t
|
|
|
2170
1876
|
// ========== FILE PATH SAFETY ==========
|
|
2171
1877
|
// Prevent reading sensitive system files via upload/paste tools
|
|
2172
1878
|
function _validateFilePath(filePath) {
|
|
2173
|
-
const
|
|
2174
|
-
const resolved = resolve(filePath);
|
|
1879
|
+
const resolved = resolvePath(filePath);
|
|
2175
1880
|
if (resolved.includes('..')) throw new Error("Path traversal not allowed: " + filePath);
|
|
2176
1881
|
const blocked = ['.ssh', '.gnupg', '.aws', '.config/gcloud', 'credentials', '.env', '.npmrc', '.netrc', 'id_rsa', 'id_ed25519', '.keychain'];
|
|
2177
1882
|
const lower = resolved.toLowerCase();
|
|
@@ -2198,10 +1903,10 @@ export async function uploadFile({ selector, filePath }) {
|
|
|
2198
1903
|
repeat with w in every window
|
|
2199
1904
|
if exists sheet 1 of w then
|
|
2200
1905
|
try
|
|
2201
|
-
click button "
|
|
1906
|
+
click button "Cancel" of sheet 1 of w
|
|
2202
1907
|
on error
|
|
2203
1908
|
try
|
|
2204
|
-
click button "
|
|
1909
|
+
click button "ביטול" of sheet 1 of w
|
|
2205
1910
|
on error
|
|
2206
1911
|
key code 53
|
|
2207
1912
|
end try
|
|
@@ -2659,6 +2364,7 @@ export async function takeSnapshot({ selector, _gen } = {}) {
|
|
|
2659
2364
|
}
|
|
2660
2365
|
|
|
2661
2366
|
window.__mcpRefs = {};
|
|
2367
|
+
window.__mcpRefsTime = Date.now();
|
|
2662
2368
|
var root = ${root};
|
|
2663
2369
|
if (!root) return 'Element not found';
|
|
2664
2370
|
walk(root, 0);
|
|
@@ -3435,6 +3141,8 @@ export async function scrollToElement({ selector, text, block = "center", timeou
|
|
|
3435
3141
|
// Navigate + wait + read — the most common 3-step workflow
|
|
3436
3142
|
export async function navigateAndRead(url, { maxLength = 50000 } = {}) {
|
|
3437
3143
|
await refreshTargetWindow();
|
|
3144
|
+
// Suppress onbeforeunload dialogs (same as navigate())
|
|
3145
|
+
await runJS("window.onbeforeunload=null", { timeout: 2000 }).catch(() => {});
|
|
3438
3146
|
let targetUrl = url;
|
|
3439
3147
|
if (!/^https?:\/\//i.test(targetUrl)) targetUrl = "https://" + targetUrl;
|
|
3440
3148
|
const safeUrl = targetUrl.replace(/"/g, '\\"');
|