safari-mcp 2.3.1 → 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 +3 -1
- package/index.js +8 -4
- package/mcp-helpers.js +2 -0
- package/package.json +1 -1
- package/safari.js +7 -297
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
|
|
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.
|
|
@@ -132,8 +134,11 @@ function _startMemoryMonitor() {
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
// Cleanup on exit
|
|
137
|
+
let _cleaningUp = false;
|
|
135
138
|
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
136
139
|
process.on(sig, async () => {
|
|
140
|
+
if (_cleaningUp) return; // Prevent double-exit on rapid signal repeat
|
|
141
|
+
_cleaningUp = true;
|
|
137
142
|
await _cleanupTabs();
|
|
138
143
|
process.exit(0);
|
|
139
144
|
});
|
|
@@ -257,7 +262,6 @@ try {
|
|
|
257
262
|
}
|
|
258
263
|
|
|
259
264
|
// POST /result — extension sends command result
|
|
260
|
-
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB cap — prevents DoS from large payloads
|
|
261
265
|
if (req.method === "POST" && req.url === "/result") {
|
|
262
266
|
let body = "";
|
|
263
267
|
req.on("data", (chunk) => { body += chunk; if (body.length > MAX_BODY_SIZE) { res.writeHead(413); res.end("Payload too large"); req.destroy(); } });
|
|
@@ -309,9 +313,9 @@ try {
|
|
|
309
313
|
const safeNonce = String(nonce).replace(/[^0-9]/g, ''); // nonce is numeric only
|
|
310
314
|
const safeProfile = (expectedProfile || "").replace(/[^\p{L}\p{N}\s\-_]/gu, ''); // whitelist: letters, numbers, spaces, hyphens, underscores
|
|
311
315
|
// Check via AppleScript — look for the nonce in the profile window
|
|
312
|
-
const { execFile } = await import("node:child_process");
|
|
313
|
-
const { promisify } = await import("node:util");
|
|
314
|
-
const execFileAsync =
|
|
316
|
+
const { execFile: execFileCb } = await import("node:child_process");
|
|
317
|
+
const { promisify: pfy } = await import("node:util");
|
|
318
|
+
const execFileAsync = pfy(execFileCb);
|
|
315
319
|
const script = `tell application "Safari"
|
|
316
320
|
repeat with w in every window
|
|
317
321
|
repeat with t in every tab of w
|
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
|
@@ -386,7 +386,7 @@ function _osascriptFastHelper(script, timeout) {
|
|
|
386
386
|
function _helperNativeClick(x, y, doubleClick = false, windowId = 0, timeout = 5000) {
|
|
387
387
|
return new Promise((resolve, reject) => {
|
|
388
388
|
if (!_helperProc) startHelper();
|
|
389
|
-
if (!_helperProc || !_helperProc.stdin) {
|
|
389
|
+
if (!_helperProc || !_helperProc.stdin || !_helperProc.stdin.writable) {
|
|
390
390
|
reject(new Error("safari-helper not available for native click"));
|
|
391
391
|
return;
|
|
392
392
|
}
|
|
@@ -696,301 +696,8 @@ export async function getPageSource({ maxLength = 200000 } = {}) {
|
|
|
696
696
|
// Loaded from mcp-helpers.js at startup — enables syntax highlighting, linting, and easier maintenance
|
|
697
697
|
const INJECT_MCP_HELPERS = readFileSync(join(__dirname, 'mcp-helpers.js'), 'utf8');
|
|
698
698
|
|
|
699
|
-
// NOTE:
|
|
700
|
-
|
|
701
|
-
const _LEGACY_INLINE_START = `if(window.__mcpVersion!==5){window.__mcpVersion=5;
|
|
702
|
-
window.__mcpRefs=window.__mcpRefs||{};
|
|
703
|
-
window.__mcpCachedRoots=null;
|
|
704
|
-
window.__mcpRootsDirty=true;
|
|
705
|
-
if(!window.__mcpRootsObserver){
|
|
706
|
-
window.__mcpRootsObserver=new MutationObserver(function(){window.__mcpRootsDirty=true;});
|
|
707
|
-
window.__mcpRootsObserver.observe(document.documentElement,{childList:true,subtree:true});
|
|
708
|
-
}
|
|
709
|
-
window.mcpCollectRoots=function(){
|
|
710
|
-
if(!window.__mcpRootsDirty&&window.__mcpCachedRoots)return window.__mcpCachedRoots;
|
|
711
|
-
var roots=[];
|
|
712
|
-
function collect(root){
|
|
713
|
-
roots.push(root);
|
|
714
|
-
var all=root.querySelectorAll('*');
|
|
715
|
-
for(var i=0;i<all.length;i++){
|
|
716
|
-
if(all[i].shadowRoot)collect(all[i].shadowRoot);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
collect(document);
|
|
720
|
-
var iframes=document.querySelectorAll('iframe');
|
|
721
|
-
for(var i=0;i<iframes.length;i++){
|
|
722
|
-
try{var doc=iframes[i].contentDocument;if(doc)collect(doc);}catch(e){}
|
|
723
|
-
}
|
|
724
|
-
window.__mcpCachedRoots=roots;
|
|
725
|
-
window.__mcpRootsDirty=false;
|
|
726
|
-
return roots;
|
|
727
|
-
};
|
|
728
|
-
window.mcpQuerySelectorDeep=function(selector){
|
|
729
|
-
try{
|
|
730
|
-
var direct=document.querySelector(selector);
|
|
731
|
-
if(direct)return direct;
|
|
732
|
-
}catch(e){return null}
|
|
733
|
-
var roots=window.mcpCollectRoots();
|
|
734
|
-
for(var i=1;i<roots.length;i++){
|
|
735
|
-
try{
|
|
736
|
-
var found=roots[i].querySelector(selector);
|
|
737
|
-
if(found)return found;
|
|
738
|
-
}catch(e){}
|
|
739
|
-
}
|
|
740
|
-
var iframes=document.querySelectorAll('iframe');
|
|
741
|
-
for(var i=0;i<iframes.length;i++){
|
|
742
|
-
try{
|
|
743
|
-
var doc=iframes[i].contentDocument;
|
|
744
|
-
if(doc){var found=doc.querySelector(selector);if(found)return found;}
|
|
745
|
-
}catch(e){}
|
|
746
|
-
}
|
|
747
|
-
return null;
|
|
748
|
-
};
|
|
749
|
-
window.mcpElementFromPoint=function(x,y){
|
|
750
|
-
var el=document.elementFromPoint(x,y);
|
|
751
|
-
while(el&&el.shadowRoot){
|
|
752
|
-
try{
|
|
753
|
-
if(typeof el.shadowRoot.elementFromPoint!=='function')break;
|
|
754
|
-
var inner=el.shadowRoot.elementFromPoint(x,y);
|
|
755
|
-
if(!inner||inner===el)break;
|
|
756
|
-
el=inner;
|
|
757
|
-
}catch(e){break}
|
|
758
|
-
}
|
|
759
|
-
return el;
|
|
760
|
-
};
|
|
761
|
-
window.mcpIsVisible=function(el){
|
|
762
|
-
if(!el||el.nodeType!==1||!el.isConnected)return false;
|
|
763
|
-
var cs=window.getComputedStyle(el);
|
|
764
|
-
if(!cs||cs.display==='none'||cs.visibility==='hidden'||cs.visibility==='collapse'||cs.pointerEvents==='none'||cs.opacity==='0')return false;
|
|
765
|
-
var r=el.getBoundingClientRect();
|
|
766
|
-
return r.width>0&&r.height>0;
|
|
767
|
-
};
|
|
768
|
-
window.mcpIsActionable=function(el){
|
|
769
|
-
if(!window.mcpIsVisible(el))return false;
|
|
770
|
-
if(el.disabled||el.getAttribute('aria-disabled')==='true')return false;
|
|
771
|
-
if(el.matches&&el.matches('input[type="hidden"]'))return false;
|
|
772
|
-
return true;
|
|
773
|
-
};
|
|
774
|
-
window.mcpPickActionable=function(el){
|
|
775
|
-
var node=el&&el.nodeType===1?el:(el&&el.parentElement)||null;
|
|
776
|
-
while(node){
|
|
777
|
-
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;
|
|
778
|
-
node=node.parentElement;
|
|
779
|
-
}
|
|
780
|
-
return el&&el.nodeType===1?el:null;
|
|
781
|
-
};
|
|
782
|
-
window.mcpFindByAttr=function(attr,value,selector){
|
|
783
|
-
if(!value)return null;
|
|
784
|
-
var roots=window.mcpCollectRoots();
|
|
785
|
-
var sel=selector||('['+attr+']');
|
|
786
|
-
for(var i=0;i<roots.length;i++){
|
|
787
|
-
var els=roots[i].querySelectorAll(sel);
|
|
788
|
-
for(var j=0;j<els.length;j++){
|
|
789
|
-
var current=attr==='href'?els[j].href:els[j].getAttribute(attr);
|
|
790
|
-
if(current===value&&window.mcpIsVisible(els[j]))return els[j];
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
return null;
|
|
794
|
-
};
|
|
795
|
-
window.mcpNormalizeText=function(value){
|
|
796
|
-
return String(value||'').replace(/\\s+/g,' ').trim();
|
|
797
|
-
};
|
|
798
|
-
window.mcpResolveTarget=function(el){
|
|
799
|
-
var base=window.mcpPickActionable(el)||el;
|
|
800
|
-
if(!base)return null;
|
|
801
|
-
try{base.scrollIntoView({block:'center',inline:'center'});}catch(e){}
|
|
802
|
-
var r=base.getBoundingClientRect();
|
|
803
|
-
var dx=Math.min(12,Math.max(4,r.width/4));
|
|
804
|
-
var dy=Math.min(12,Math.max(4,r.height/4));
|
|
805
|
-
var pts=[
|
|
806
|
-
[r.left+r.width/2,r.top+r.height/2],
|
|
807
|
-
[r.left+dx,r.top+r.height/2],
|
|
808
|
-
[r.right-dx,r.top+r.height/2],
|
|
809
|
-
[r.left+r.width/2,r.top+dy],
|
|
810
|
-
[r.left+r.width/2,r.bottom-dy]
|
|
811
|
-
];
|
|
812
|
-
for(var i=0;i<pts.length;i++){
|
|
813
|
-
var x=pts[i][0],y=pts[i][1];
|
|
814
|
-
if(x<0||y<0||x>window.innerWidth||y>window.innerHeight)continue;
|
|
815
|
-
var hit=window.mcpElementFromPoint(x,y);
|
|
816
|
-
if(!hit)continue;
|
|
817
|
-
if(base===hit||base.contains(hit)||hit.contains(base))return window.mcpPickActionable(hit)||hit;
|
|
818
|
-
// Overlay pattern: transparent button/link overlays a text label (e.g. dropdown items).
|
|
819
|
-
// If the hit is an interactive element sharing a nearby common ancestor, prefer it.
|
|
820
|
-
var hitAction=window.mcpPickActionable(hit);
|
|
821
|
-
if(hitAction&&hitAction!==base&&hitAction.matches&&hitAction.matches('button,a[href],[role="button"],[role="option"],[role="menuitem"]')){
|
|
822
|
-
var p=base.parentElement;
|
|
823
|
-
for(var k=0;k<4&&p;k++){
|
|
824
|
-
if(p.contains(hitAction))return hitAction;
|
|
825
|
-
p=p.parentElement;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
return base;
|
|
830
|
-
};
|
|
831
|
-
window.mcpClick=function(el){
|
|
832
|
-
var target=window.mcpResolveTarget(el);
|
|
833
|
-
if(!window.mcpIsActionable(target))return false;
|
|
834
|
-
var r=target.getBoundingClientRect();
|
|
835
|
-
var x=r.left+r.width/2,y=r.top+r.height/2;
|
|
836
|
-
var beforeUrl=location.href;
|
|
837
|
-
var anchor=target.closest?target.closest('a[href]'):null;
|
|
838
|
-
var href=anchor&&anchor.href&&!anchor.href.startsWith('javascript:')?anchor.href:'';
|
|
839
|
-
var s={bubbles:true,cancelable:true,composed:true,view:window,clientX:x,clientY:y,button:0,detail:1};
|
|
840
|
-
var p={...s,pointerId:1,pointerType:'mouse',isPrimary:true,width:1,height:1,pressure:0.5};
|
|
841
|
-
target.dispatchEvent(new PointerEvent('pointerover',{...p,buttons:0}));
|
|
842
|
-
target.dispatchEvent(new MouseEvent('mouseover',{...s,buttons:0}));
|
|
843
|
-
target.dispatchEvent(new PointerEvent('pointerenter',{...p,buttons:0}));
|
|
844
|
-
target.dispatchEvent(new MouseEvent('mouseenter',{...s,buttons:0}));
|
|
845
|
-
target.dispatchEvent(new PointerEvent('pointermove',{...p,buttons:0}));
|
|
846
|
-
target.dispatchEvent(new MouseEvent('mousemove',{...s,buttons:0}));
|
|
847
|
-
target.dispatchEvent(new PointerEvent('pointerdown',{...p,buttons:1}));
|
|
848
|
-
target.dispatchEvent(new MouseEvent('mousedown',{...s,buttons:1}));
|
|
849
|
-
if(target.focus){try{target.focus({preventScroll:true});}catch(e){try{target.focus();}catch(_){}}}
|
|
850
|
-
target.dispatchEvent(new PointerEvent('pointerup',{...p,buttons:0,pressure:0}));
|
|
851
|
-
target.dispatchEvent(new MouseEvent('mouseup',{...s,buttons:0}));
|
|
852
|
-
try{
|
|
853
|
-
if(typeof target.click==='function'){
|
|
854
|
-
target.click();
|
|
855
|
-
if(href&&href!==beforeUrl){
|
|
856
|
-
location.href=href;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
}catch(e){}
|
|
860
|
-
target.dispatchEvent(new MouseEvent('click',{...s,buttons:0}));
|
|
861
|
-
if(href&&href!==beforeUrl){
|
|
862
|
-
location.href=href;
|
|
863
|
-
}
|
|
864
|
-
var form=target.closest?target.closest('form'):null;
|
|
865
|
-
if(form&&(target.type==='submit'||(target.tagName==='BUTTON'&&target.type!=='button'&&target.type!=='reset'))){
|
|
866
|
-
try{form.requestSubmit?form.requestSubmit(target.type==='submit'?target:undefined):form.submit();}catch(e){}
|
|
867
|
-
}
|
|
868
|
-
return true;
|
|
869
|
-
};
|
|
870
|
-
window.mcpReactClick=function(el){
|
|
871
|
-
var startNode=window.mcpResolveTarget(el)||el;
|
|
872
|
-
if(!startNode)return false;
|
|
873
|
-
function makeSynth(targetNode, type){
|
|
874
|
-
var r=targetNode.getBoundingClientRect();
|
|
875
|
-
return{type:type||'click',target:targetNode,currentTarget:targetNode,
|
|
876
|
-
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,
|
|
877
|
-
preventDefault:function(){},stopPropagation:function(){},nativeEvent:new MouseEvent(type||'click'),persist:function(){},bubbles:true,cancelable:true};
|
|
878
|
-
}
|
|
879
|
-
// For React radio/checkbox inputs, onChange is the primary handler (not onClick)
|
|
880
|
-
var isToggle=startNode.tagName==='INPUT'&&(startNode.type==='radio'||startNode.type==='checkbox');
|
|
881
|
-
function tryOnChange(propsObj,targetNode){
|
|
882
|
-
if(!isToggle||!propsObj.onChange)return false;
|
|
883
|
-
var newChecked=startNode.type==='radio'?true:!startNode.checked;
|
|
884
|
-
propsObj.onChange({target:{value:startNode.value,checked:newChecked,type:startNode.type,name:startNode.name,id:startNode.id,tagName:'INPUT'},
|
|
885
|
-
currentTarget:targetNode,preventDefault:function(){},stopPropagation:function(){},nativeEvent:new Event('change'),persist:function(){},bubbles:true,cancelable:true,type:'change'});
|
|
886
|
-
return true;
|
|
887
|
-
}
|
|
888
|
-
var node=startNode;
|
|
889
|
-
for(var depth=0;depth<15&&node;depth++){
|
|
890
|
-
var pk=Object.keys(node).find(function(k){return k.startsWith('__reactProps$')});
|
|
891
|
-
if(pk&&node[pk]){
|
|
892
|
-
var props=node[pk];
|
|
893
|
-
if(tryOnChange(props,node))return true;
|
|
894
|
-
if(props.onClick){props.onClick(makeSynth(node,'click'));return true}
|
|
895
|
-
if(props.onMouseDown){props.onMouseDown(makeSynth(node,'mousedown'));return true}
|
|
896
|
-
if(props.onPointerDown){props.onPointerDown(makeSynth(node,'pointerdown'));return true}
|
|
897
|
-
}
|
|
898
|
-
var fk=Object.keys(node).find(function(k){return k.startsWith('__reactFiber$')||k.startsWith('__reactInternalInstance$')});
|
|
899
|
-
if(fk){
|
|
900
|
-
var f=node[fk];
|
|
901
|
-
while(f){
|
|
902
|
-
if(f.memoizedProps){
|
|
903
|
-
if(tryOnChange(f.memoizedProps,node))return true;
|
|
904
|
-
if(f.memoizedProps.onClick){f.memoizedProps.onClick(makeSynth(node,'click'));return true}
|
|
905
|
-
if(f.memoizedProps.onMouseDown){f.memoizedProps.onMouseDown(makeSynth(node,'mousedown'));return true}
|
|
906
|
-
if(f.memoizedProps.onPointerDown){f.memoizedProps.onPointerDown(makeSynth(node,'pointerdown'));return true}
|
|
907
|
-
}
|
|
908
|
-
f=f.return;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
node=node.parentElement;
|
|
912
|
-
}
|
|
913
|
-
return false;
|
|
914
|
-
};
|
|
915
|
-
window.mcpClickWithReact=function(el){
|
|
916
|
-
var target=window.mcpResolveTarget(el)||el;
|
|
917
|
-
var reactFired=false;
|
|
918
|
-
try{reactFired=window.mcpReactClick(target);}catch(e){}
|
|
919
|
-
var anchor=target&&target.closest?target.closest('a[href]'):null;
|
|
920
|
-
if(!reactFired||anchor)window.mcpClick(target);
|
|
921
|
-
return target;
|
|
922
|
-
};
|
|
923
|
-
window.mcpFindRef=function(ref){
|
|
924
|
-
var el=window.mcpQuerySelectorDeep('[data-mcp-ref="'+ref+'"]');
|
|
925
|
-
if(el)return el;
|
|
926
|
-
if(!window.__mcpRefs||!window.__mcpRefs[ref])return null;
|
|
927
|
-
var m=window.__mcpRefs[ref];
|
|
928
|
-
if(m.id){el=window.mcpFindByAttr('id',m.id);if(el)return el}
|
|
929
|
-
if(m.testid){el=window.mcpFindByAttr('data-testid',m.testid);if(el)return el}
|
|
930
|
-
if(m.nameAttr){el=window.mcpFindByAttr('name',m.nameAttr);if(el)return el}
|
|
931
|
-
if(m.href){el=window.mcpFindByAttr('href',m.href,'a[href]');if(el)return el}
|
|
932
|
-
if(m.al){el=window.mcpFindByAttr('aria-label',m.al);if(el)return el}
|
|
933
|
-
if(m.ph){el=window.mcpFindByAttr('placeholder',m.ph);if(el)return el}
|
|
934
|
-
if(m.text){
|
|
935
|
-
el=window.mcpFindText(m.text,true)||window.mcpFindText(m.text,false);
|
|
936
|
-
if(el)return el;
|
|
937
|
-
}
|
|
938
|
-
if(m.cx!==undefined&&m.cy!==undefined){
|
|
939
|
-
try{
|
|
940
|
-
window.scrollTo(window.scrollX,Math.max(0,m.cy-window.innerHeight/2));
|
|
941
|
-
}catch(e){}
|
|
942
|
-
el=window.mcpElementFromPoint(m.cx-window.scrollX,m.cy-window.scrollY);
|
|
943
|
-
if(el)return window.mcpPickActionable(el)||el;
|
|
944
|
-
}
|
|
945
|
-
return null;
|
|
946
|
-
};
|
|
947
|
-
window.mcpFindText=function(text,exact){
|
|
948
|
-
var needle=window.mcpNormalizeText(text);
|
|
949
|
-
var best=null,bestScore=Infinity;
|
|
950
|
-
function consider(node){
|
|
951
|
-
var target=window.mcpPickActionable(node)||node;
|
|
952
|
-
if(!window.mcpIsVisible(target))return;
|
|
953
|
-
var r=target.getBoundingClientRect();
|
|
954
|
-
var area=r.width*r.height;
|
|
955
|
-
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]');
|
|
956
|
-
var score=area+(interactive?0:1000000);
|
|
957
|
-
if(score<bestScore){best=target;bestScore=score}
|
|
958
|
-
}
|
|
959
|
-
var roots=window.mcpCollectRoots();
|
|
960
|
-
for(var i=0;i<roots.length;i++){
|
|
961
|
-
var attrEls=roots[i].querySelectorAll('[aria-label],[placeholder],[title],[data-testid],[alt]');
|
|
962
|
-
for(var j=0;j<attrEls.length;j++){
|
|
963
|
-
var a=attrEls[j];
|
|
964
|
-
var vals=[a.getAttribute('aria-label'),a.getAttribute('placeholder'),a.getAttribute('title'),a.getAttribute('data-testid'),a.getAttribute('alt')];
|
|
965
|
-
for(var k=0;k<vals.length;k++){
|
|
966
|
-
var val=vals[k];
|
|
967
|
-
if(!val)continue;
|
|
968
|
-
var normalized=window.mcpNormalizeText(val);
|
|
969
|
-
if(exact?normalized===needle:normalized.includes(needle)){consider(a);break}
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
var tw=document.createTreeWalker(roots[i],NodeFilter.SHOW_TEXT,null);
|
|
973
|
-
while(tw.nextNode()){
|
|
974
|
-
var n=tw.currentNode;
|
|
975
|
-
var t=window.mcpNormalizeText(n.textContent);
|
|
976
|
-
if(!t)continue;
|
|
977
|
-
if(exact?(t!==needle):!t.includes(needle))continue;
|
|
978
|
-
if(n.parentElement)consider(n.parentElement);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
if(!best){
|
|
982
|
-
var allEls=document.querySelectorAll('*');
|
|
983
|
-
for(var i=0;i<allEls.length;i++){
|
|
984
|
-
var el=allEls[i];
|
|
985
|
-
var it=window.mcpNormalizeText(el.innerText);
|
|
986
|
-
if(!it)continue;
|
|
987
|
-
if(exact?(it!==needle):!it.includes(needle))continue;
|
|
988
|
-
consider(el);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
return best;
|
|
992
|
-
};
|
|
993
|
-
}`;
|
|
699
|
+
// NOTE: ~300 lines of legacy inline helpers were here — now in mcp-helpers.js (deleted in v2.4.0)
|
|
700
|
+
|
|
994
701
|
|
|
995
702
|
// Precomputed escaped helper string — avoids re-escaping ~4KB on every injection call
|
|
996
703
|
const _HELPERS_ESCAPED = INJECT_MCP_HELPERS
|
|
@@ -1219,6 +926,7 @@ export async function nativeClick({ selector, text, x, y, ref, doubleClick = fal
|
|
|
1219
926
|
|
|
1220
927
|
export async function fill({ selector, value, ref }) {
|
|
1221
928
|
if (ref) selector = refSelector(ref);
|
|
929
|
+
if (!selector) throw new Error("fill requires selector or ref");
|
|
1222
930
|
const sel = selector.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1223
931
|
// Proper escaping order: backslashes first, then quotes
|
|
1224
932
|
const val = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
|
|
@@ -1678,7 +1386,7 @@ export async function screenshot({ fullPage = false } = {}) {
|
|
|
1678
1386
|
}
|
|
1679
1387
|
// Compress: convert PNG to JPEG (50% quality) + resize to max 1200px width
|
|
1680
1388
|
// Cuts ~600KB PNG → ~60KB JPEG — critical for staying under 20MB context limit
|
|
1681
|
-
const jpgFile = tmpFile.replace(
|
|
1389
|
+
const jpgFile = tmpFile.replace(/\.png$/, '.jpg');
|
|
1682
1390
|
try {
|
|
1683
1391
|
await execFileAsync("sips", [
|
|
1684
1392
|
"-s", "format", "jpeg",
|
|
@@ -3433,6 +3141,8 @@ export async function scrollToElement({ selector, text, block = "center", timeou
|
|
|
3433
3141
|
// Navigate + wait + read — the most common 3-step workflow
|
|
3434
3142
|
export async function navigateAndRead(url, { maxLength = 50000 } = {}) {
|
|
3435
3143
|
await refreshTargetWindow();
|
|
3144
|
+
// Suppress onbeforeunload dialogs (same as navigate())
|
|
3145
|
+
await runJS("window.onbeforeunload=null", { timeout: 2000 }).catch(() => {});
|
|
3436
3146
|
let targetUrl = url;
|
|
3437
3147
|
if (!/^https?:\/\//i.test(targetUrl)) targetUrl = "https://" + targetUrl;
|
|
3438
3148
|
const safeUrl = targetUrl.replace(/"/g, '\\"');
|