@gaiventures/pilotfish-mcp 0.1.3 → 0.1.4

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 (2) hide show
  1. package/cli.js +1 -1
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -411,7 +411,7 @@ return __pfClassify("Accept all") === "accept" && __pfClassify("Reject all") ===
411
411
  return click.call(this);
412
412
  };
413
413
  })();
414
- ${es(tr(nr,"autoconsent.playwright.js"),"utf8")}`,eu=3,tu=2500,su=6500,nu=6e3,ou=3e3,iu=1500,ru=3e3,au=4e3;function cu(n,e){if(n.doneAt)return"opted_out";if(n.optOut===!1)return"failed";if(n.optOut===!0)return e-n.optOutAt>ou?"opted_out":"pending";if(n.popupAt)return"pending";if(n.detectedAt)return e-n.detectedAt<su?"pending":"no_cmp";let t=n.topLifecycle!=="nothingDetected",s=l((o,i)=>e-o>=tu&&i!==void 0&&n.topAttempts>=i+eu,"quiet");return n.cmpRequestAt&&e-n.cmpRequestAt<nu&&t&&!s(n.cmpRequestAt,n.roundAtCmpRequest)||!n.topInitAt&&/^https?:/.test(n.url)&&e-n.navAt<iu||n.topInitAt&&n.topAttempts<2&&e-n.topInitAt<ru||n.patternsAt&&e-n.patternsAt<au&&t&&!s(n.patternsAt,n.roundAtPatterns)?"pending":"no_cmp"}l(cu,"outcomeOf");var Zt=class{static{l(this,"ConsentEngine")}opts;emitter=new Gd;pages=new WeakMap;disposers=new Set;armed=new WeakSet;retried=new WeakSet;retryTimers=new Set;disposed=!1;config;constructor(e={}){if(this.opts=e,this.config=Xd(e),this.config.autoAction!=="optOut"||!["off","reject"].includes(this.config.heuristicMode))throw new Error("ConsentEngine only ever opts out")}on(e,t){return this.emitter.on(e,t),()=>this.emitter.off(e,t)}consentStatus(e){let t=this.state(e),s=Date.now(),o=cu(t,s),i=o==="opted_out"?t.doneAt??t.optOutAt:s;return{cmp:t.cmp,outcome:o,ms:Math.max(0,i-t.navAt),url:t.url,...t.cosmetic!==void 0?{cosmetic:t.cosmetic}:{},...t.selfTest!==void 0?{selfTest:t.selfTest}:{},refusedClicks:t.refused,frames:t.frames.size}}async waitForConsent(e,t=1e4){let s=Date.now()+t;for(;;){let o=this.consentStatus(e);if(o.outcome!=="pending"||Date.now()>=s||e.closed)return o;await new Promise(i=>setTimeout(i,Math.min(100,Math.max(1,s-Date.now()))))}}dispose(){this.disposed=!0;for(let e of this.retryTimers)clearTimeout(e);this.retryTimers.clear();for(let e of this.disposers)e();this.disposers.clear()}initSession=l(async e=>{let t=e.session,s=e.page;if(!e.isMain&&we(e.url)||this.armed.has(t))return;this.armed.add(t);let o=new Map,i=[()=>this.armed.delete(t)],r;try{[,r]=await Promise.all([t.send("Runtime.addBinding",{name:ze,executionContextName:dn},{timeoutMs:5e3}),t.send("Page.addScriptToEvaluateOnNewDocument",{source:Zd,worldName:dn,runImmediately:!0},{timeoutMs:5e3})])}catch(p){if(this.armed.delete(t),e.isMain&&!this.retried.has(t)&&!this.disposed){this.retried.add(t);let f=setTimeout(()=>{this.retryTimers.delete(f),this.disposed||this.initSession(e).catch(()=>{})},2e3);f.unref?.(),this.retryTimers.add(f)}throw p}let a=l(()=>{for(let p of i)p();t.send("Page.removeScriptToEvaluateOnNewDocument",{identifier:r.identifier}).catch(()=>{}),t.send("Runtime.removeBinding",{name:ze}).catch(()=>{})},"dispose");this.disposers.add(a),i.push(()=>this.disposers.delete(a));let c=l(async(p,f,y)=>{if(!/^https?:/.test(f)||!y&&we(f))return;let{executionContextId:b}=await t.send("Page.createIsolatedWorld",{frameId:p,worldName:dn,grantUniveralAccess:!1},{timeoutMs:5e3});o.set(b,{frameId:p,mainFrame:y}),await t.send("Runtime.addBinding",{name:ze,executionContextId:b},{timeoutMs:5e3}),await t.send("Runtime.evaluate",{contextId:b,expression:"globalThis.__pfAcFlush && __pfAcFlush()",returnByValue:!0},{timeoutMs:5e3})},"arm"),d=l((p,f,y)=>{c(p,f,y).catch(b=>{/not found|Cannot find|No frame|detached|closed|destroyed|navigated/i.test(String(b))||this.emit("error",{page:s,frameId:p,url:f,error:`arm: ${String(b)}`})})},"armSafe");i.push(t.on("Page.frameNavigated",p=>{let f=e.isMain&&!p.frame.parentId;f&&this.reset(s,p.frame.url),d(p.frame.id,p.frame.url,f)}),t.on("Runtime.bindingCalled",p=>{if(p.name!==ze)return;let f=o.get(p.executionContextId);f&&this.onMessage(t,s,p.executionContextId,f,p.payload).catch(()=>{})}),t.on("Network.requestWillBeSent",p=>{if(!Fi(p.request.url))return;let f=this.state(s);f.cmpRequestAt===void 0&&(f.cmpRequestAt=Date.now(),f.roundAtCmpRequest=f.topAttempts)}));let{frameTree:u}=await t.send("Page.getFrameTree"),h=l((p,f)=>{d(p.frame.id,p.frame.url,f);for(let y of p.childFrames??[])h(y,!1)},"walk");h(u,e.isMain)},"initSession");async onMessage(e,t,s,o,i){let r;try{r=JSON.parse(i)}catch{return}let a=this.state(t),c=typeof r.url=="string"?r.url:"",d={page:t,frameId:o.frameId,url:c},u=typeof r.cmp=="string"?r.cmp:"";switch(r.type){case"init":{a.frames.add(o.frameId),o.mainFrame&&(a.topInitAt??=Date.now());let h={autoconsent:[],compact:Yd(or,{url:c,mainFrame:o.mainFrame})};await this.reply(e,s,{type:"initResp",config:this.config,rules:h});return}case"eval":await this.reply(e,s,{type:"evalResp",id:r.id,result:await this.runSnippet(e,t,s,o,r.snippetId)});return;case"cmpDetected":a.cmp??=u,a.detectedAt??=Date.now(),this.emit("cmpDetected",{...d,cmp:u});return;case"popupFound":a.cmp=u,a.popupAt??=Date.now(),this.emit("popupFound",{...d,cmp:u});return;case"optOutResult":a.cmp=u||a.cmp,a.optOut=r.result===!0,a.optOutAt=Date.now(),r.result===!0&&r.scheduleSelfTest===!0&&(a.selfTestTarget={s:e,contextId:s}),this.emit("optOutResult",{...d,cmp:u,result:r.result===!0});return;case"autoconsentDone":{a.cmp=u||a.cmp,a.doneAt=Date.now(),a.cosmetic=r.isCosmetic===!0,this.emit("autoconsentDone",{...d,cmp:u,cosmetic:a.cosmetic,durationMs:Number(r.duration)||0,clicks:Number(r.totalClicks)||0});let h=a.selfTestTarget;a.selfTestTarget=void 0,this.opts.selfTest!==!1&&h&&await this.reply(h.s,h.contextId,{type:"selfTest"});return}case"selfTestResult":a.selfTest=r.result===!0,this.emit("selfTestResult",{...d,cmp:u,result:a.selfTest});return;case"optInResult":this.emit("error",{...d,error:"unexpected optInResult",details:r});return;case"autoconsentError":this.emit("error",{...d,error:"autoconsent",details:r.details});return;case"pfRefusedClick":a.refused++,this.emit("error",{...d,error:`refused to click an accept button: "${String(r.text)}"`});return;case"report":{let h=r.state??{};if(!o.mainFrame||r.mainFrame!==!0)return;a.topLifecycle=h.lifecycle,a.topAttempts=Math.max(a.topAttempts,Number(h.findCmpAttempts)||0),h.heuristicPatterns?.length&&a.patternsAt===void 0&&(a.patternsAt=Date.now(),a.roundAtPatterns=a.topAttempts);return}default:return}}reply(e,t,s){return e.send("Runtime.evaluate",{contextId:t,expression:`autoconsentReceiveMessage(${JSON.stringify(s)})`,returnByValue:!0}).catch(()=>{})}async runSnippet(e,t,s,o,i){let r=typeof i=="string"?sr[i]:void 0;if(!r||Jd.has(i))return this.emit("error",{page:t,frameId:o.frameId,error:`refused eval snippet ${String(i)}`}),!1;let a=await e.send("Runtime.evaluate",{contextId:s,expression:"document",returnByValue:!1});if(!a.result.objectId)return!1;try{let{node:c}=await e.send("DOM.describeNode",{objectId:a.result.objectId}),{object:d}=await e.send("DOM.resolveNode",{backendNodeId:c.backendNodeId});if(!d.objectId)return!1;try{return(await e.send("Runtime.callFunctionOn",{objectId:d.objectId,functionDeclaration:`async function () { try { return !!(await (${r.toString()}).call(window)); } catch (e) { return false; } }`,returnByValue:!0,awaitPromise:!0})).result.value===!0}finally{e.send("Runtime.releaseObject",{objectId:d.objectId}).catch(()=>{})}}catch{return!1}finally{e.send("Runtime.releaseObject",{objectId:a.result.objectId}).catch(()=>{})}}state(e){let t=this.pages.get(e);return t||(t={navAt:Date.now(),url:e.url,cmp:null,refused:0,frames:new Set,topAttempts:0},this.pages.set(e,t)),t}reset(e,t){this.pages.set(e,{navAt:Date.now(),url:t,cmp:null,refused:0,frames:new Set,topAttempts:0})}emit(e,t){e==="error"&&this.emitter.listenerCount("error")===0||this.emitter.emit(e,t)}};var lu=["cmpDetected","popupFound","optOutResult","autoconsentDone","selfTestResult","error"],du=["blocked","error"];async function hn(n,e={}){let t=e.consent===!1?null:new Zt(typeof e.consent=="object"?e.consent:{}),s=e.adblock===!1?null:await Qt.create(typeof e.adblock=="object"?e.adblock:{}),o=[];if(e.onEvent){let r=e.onEvent;if(t)for(let a of lu)o.push(t.on(a,(c=>r({source:"consent",type:a,data:c}))));if(s)for(let a of du)o.push(s.on(a,(c=>r({source:"adblock",type:a,data:c}))))}let i=l(async r=>{await Promise.all([t?.initSession(r),s?.initSession(r)])},"init");return o.push(n.addSessionInitializer(i)),{consent:t,adblock:s,detach(){for(let r of o)r();t?.dispose(),s?.dispose()}}}l(hn,"attachInterrupts");var uu="pilotfish's open MCP makes no model calls: the MCP client's own model reads the page and decides the next step",hu=l(()=>{let n=l(async()=>{throw new Error(uu)},"refuse");return{messages:{create:n},beta:{messages:{create:n}}}},"noModelClient"),pn=class{static{l(this,"ConfirmThrough")}inner;canAsk;constructor(e,t){this.inner=e,this.canAsk=t}async check(e){let t=await this.inner.check(e);return t.kind!=="confirm"||this.canAsk()?t:{kind:"deny",reason:`${t.reason}. This needs a person's confirmation, and this MCP client cannot ask for one (no elicitation support)`}}},We=class{static{l(this,"BrowserSession")}config;opts;browser=null;launching=null;interrupts=null;tabs=null;agent=null;queue=Promise.resolve();policy;asker;constructor(e,t={}){this.config=e,this.opts=t,this.policy=new Ce({allowedHosts:e.allowedHosts,allowPrivateNetwork:e.allowPrivateNetwork})}get launched(){return!!this.browser&&!this.browser.closed}async page(){return await this.ensure(),this.tabs.current}get tabTable(){return this.tabs}async ensure(){return this.browser&&!this.browser.closed?this.browser:(this.launching??=this.start().finally(()=>{this.launching=null}),this.launching)}async start(){let e=await gs({...this.opts.launch,headless:this.config.headless});this.interrupts=await hn(e,{consent:!0,adblock:!0}).catch(()=>null);let t=e.allPages()[0]??await e.newPage();return this.tabs=new Ae(t),this.browser=e,e.on("disconnected",()=>{this.browser===e&&this.reset()}),e}reset(){this.agent?.dispose(),this.agent=null,this.tabs?.dispose(),this.tabs=null,this.interrupts?.detach(),this.interrupts=null,this.browser=null}executor(){return this.agent??=new ht(this.tabs.current,{tabs:this.tabs,client:hu(),secrets:this.config.secrets,policy:new pn(this.policy,()=>!!this.asker),consent:this.interrupts?.consent??void 0,allowScreenshots:this.config.allowScreenshots,askUser:l(e=>this.asker?.(e)??Promise.resolve(null),"askUser"),fastSettle:!0,enableQuery:!0,compactSnapshots:!0,autoDismiss:!0,awaitLoading:!0,actionSnapshotBand:0,snapshotMoreHint:"browser_scroll for the next screen, browser_get_text for the page's text, or browser_snapshot (whole_page) for every element"}),this.agent}async act(e,t,s={}){let o=this.queue.then(async()=>(await this.ensure(),this.executor().act(e,t,"",s)));return this.queue=o.catch(()=>{}),o}async obstacle(e={}){if(!this.launched)return null;let t=this.queue.then(()=>this.executor().obstacle(e));return this.queue=t.catch(()=>{}),t.catch(()=>null)}async close(){let e=this.browser??(this.launching?await this.launching.catch(()=>null):null);return e?(this.reset(),await e.close().catch(()=>{}),!0):!1}};import{McpServer as pu}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as H}from"zod";var ts="0.1.3";var Ge=Ie.click.shape.ref.describe('Element ref from the latest snapshot, e.g. "e12" or "f1e3"'),ne={browser_navigate:H.object({url:H.url({protocol:/^https?$/}).describe("Absolute http(s) URL"),snapshot:H.boolean().optional().describe("Default true. false: no page snapshot, only what happened (title, counts). Only when this same reply also reads the page with browser_query and you will not act on it; keep it for a page you have not seen or whose tabs, sorts, filters or forms you may use.")}).strict(),browser_snapshot:H.object({whole_page:H.boolean().optional().describe("Include everything, not only the viewport band (default false)")}).strict(),browser_click:H.object({ref:Ge}).strict(),browser_type:H.object({ref:Ge,text:H.string().max(2e4).describe("Text to enter (replaces what is there). Type secrets as {{name}} placeholders."),submit:H.boolean().optional().describe("Press Enter afterwards")}).strict(),browser_select:H.object({ref:Ge,values:H.array(H.string()).min(1).max(50).describe("Option labels or values")}).strict(),browser_press:H.object({key:H.string().min(1).max(40).describe('A key or shortcut: "Enter", "Escape", "Tab", "ArrowDown", "Control+a"')}).strict(),browser_scroll:H.object({direction:H.enum(["down","up","left","right"])}).strict(),browser_screenshot:H.object({}).strict(),browser_get_text:H.object({max_chars:H.number().int().min(500).max(6e4).optional().describe("At most this many characters (default 20000). Ask for less when the start of the page is enough."),selector:H.string().min(1).max(300).optional().describe('Only the text inside the first element matching this CSS selector, e.g. "main" or "#results"')}).strict(),browser_query:H.object({expression:H.string().min(1).max(4e3).describe("A read-only JavaScript expression returning just the values you need as JSON, e.g. [...document.querySelectorAll('.result')].slice(0,10).map(e => e.innerText). Runs in a world of its own: it must not click, type, submit or change the page.")}).strict(),browser_find:H.object({query:H.string().min(1).max(300).describe('Words naming the element(s), e.g. "Sort by" or "price max input, Go button"')}).strict(),browser_wait:H.object({seconds:H.number().min(.5).max(10).describe("Seconds to wait before looking at the page again")}).strict(),browser_resume:H.object({timeout_s:H.number().int().min(5).max(900).optional().describe("How long to wait for the page to move on (default 300)")}).strict(),browser_actions:H.object({actions:H.array(H.discriminatedUnion("action",[H.object({action:H.literal("click"),ref:Ge}).strict(),H.object({action:H.literal("type"),ref:Ge,text:H.string().max(2e4),submit:H.boolean().optional()}).strict(),H.object({action:H.literal("select"),ref:Ge,values:H.array(H.string()).min(1).max(50)}).strict(),H.object({action:H.literal("press"),key:H.string().min(1).max(40)}).strict(),H.object({action:H.literal("scroll"),direction:H.enum(["down","up","left","right"])}).strict(),H.object({action:H.literal("wait"),seconds:H.number().min(.5).max(10)}).strict()])).min(1).max(10).describe("Steps run in order, e.g. fill a form's fields then submit. They stop at the first that fails or when the page moves to another address (later refs would belong to the old page).")}).strict(),browser_tabs:H.object({switch_to:H.string().min(1).max(10).optional().describe('Tab id to work in from now on, e.g. "t2"')}).strict(),browser_close:H.object({}).strict()};function xe(n,e=!1){return{content:[{type:"text",text:n}],...e?{isError:!0}:{}}}l(xe,"text");function fu(n){let e=Object.keys(n.secrets);return["pilotfish drives a real Chromium for you: you read each page and decide every step; pilotfish acts, checks each action and reports what changed.","Control: browser_navigate / browser_snapshot to get element refs (e12), then browser_click / browser_type / browser_select / browser_press / browser_scroll by ref. Every action returns EVIDENCE of what changed and, when the page changed, a snapshot of the visible screen with counts of the elements above and below it (browser_scroll for the next screen, browser_snapshot for three screens or whole_page for everything), or, when most of the screen is as in a snapshot you were given a few results back, only the lines that changed; refs stay valid while their element is on the page.","Reading: browser_query reads just the values you need with read-only JavaScript (a results list, prices, counts): the cheapest way to read. browser_find returns refs for elements anywhere on the page. browser_get_text gives a whole page's text, or one part of it (selector, max_chars). browser_navigate with snapshot:false skips the screen: only beside a browser_query of that page in the same reply, for a page you will not act on.","Acting: browser_actions runs several steps in one call (fill a form's fields, then submit) and stops at the first that fails or after one that moves to another page.","Words like cheapest, latest, highest-rated or most mean the site's own sort or tab for that order (a list's default order is usually something else, e.g. 'Best' or 'Featured'): apply it, then read the top.","The browser closes by itself when this client disconnects; browser_close is only for starting over within a session.",e.length?`Secrets are typed as placeholders and never shown: ${e.map(t=>`{{${t}}}`).join(", ")}.`:"",n.allowedHosts.length?`Navigation is limited to: ${n.allowedHosts.join(", ")}.`:"","When a result says NEEDS A PERSON (a sign-in, a one-time code or 2FA, a device approval, a CAPTCHA), tell the user in your reply what the page needs and that the pilotfish browser window is open on it; when they have done it there, call browser_resume and carry on. BLOCKED and STOP mean the site cannot be continued that way."].filter(Boolean).join(`
414
+ ${es(tr(nr,"autoconsent.playwright.js"),"utf8")}`,eu=3,tu=2500,su=6500,nu=6e3,ou=3e3,iu=1500,ru=3e3,au=4e3;function cu(n,e){if(n.doneAt)return"opted_out";if(n.optOut===!1)return"failed";if(n.optOut===!0)return e-n.optOutAt>ou?"opted_out":"pending";if(n.popupAt)return"pending";if(n.detectedAt)return e-n.detectedAt<su?"pending":"no_cmp";let t=n.topLifecycle!=="nothingDetected",s=l((o,i)=>e-o>=tu&&i!==void 0&&n.topAttempts>=i+eu,"quiet");return n.cmpRequestAt&&e-n.cmpRequestAt<nu&&t&&!s(n.cmpRequestAt,n.roundAtCmpRequest)||!n.topInitAt&&/^https?:/.test(n.url)&&e-n.navAt<iu||n.topInitAt&&n.topAttempts<2&&e-n.topInitAt<ru||n.patternsAt&&e-n.patternsAt<au&&t&&!s(n.patternsAt,n.roundAtPatterns)?"pending":"no_cmp"}l(cu,"outcomeOf");var Zt=class{static{l(this,"ConsentEngine")}opts;emitter=new Gd;pages=new WeakMap;disposers=new Set;armed=new WeakSet;retried=new WeakSet;retryTimers=new Set;disposed=!1;config;constructor(e={}){if(this.opts=e,this.config=Xd(e),this.config.autoAction!=="optOut"||!["off","reject"].includes(this.config.heuristicMode))throw new Error("ConsentEngine only ever opts out")}on(e,t){return this.emitter.on(e,t),()=>this.emitter.off(e,t)}consentStatus(e){let t=this.state(e),s=Date.now(),o=cu(t,s),i=o==="opted_out"?t.doneAt??t.optOutAt:s;return{cmp:t.cmp,outcome:o,ms:Math.max(0,i-t.navAt),url:t.url,...t.cosmetic!==void 0?{cosmetic:t.cosmetic}:{},...t.selfTest!==void 0?{selfTest:t.selfTest}:{},refusedClicks:t.refused,frames:t.frames.size}}async waitForConsent(e,t=1e4){let s=Date.now()+t;for(;;){let o=this.consentStatus(e);if(o.outcome!=="pending"||Date.now()>=s||e.closed)return o;await new Promise(i=>setTimeout(i,Math.min(100,Math.max(1,s-Date.now()))))}}dispose(){this.disposed=!0;for(let e of this.retryTimers)clearTimeout(e);this.retryTimers.clear();for(let e of this.disposers)e();this.disposers.clear()}initSession=l(async e=>{let t=e.session,s=e.page;if(!e.isMain&&we(e.url)||this.armed.has(t))return;this.armed.add(t);let o=new Map,i=[()=>this.armed.delete(t)],r;try{[,r]=await Promise.all([t.send("Runtime.addBinding",{name:ze,executionContextName:dn},{timeoutMs:5e3}),t.send("Page.addScriptToEvaluateOnNewDocument",{source:Zd,worldName:dn,runImmediately:!0},{timeoutMs:5e3})])}catch(p){if(this.armed.delete(t),e.isMain&&!this.retried.has(t)&&!this.disposed){this.retried.add(t);let f=setTimeout(()=>{this.retryTimers.delete(f),this.disposed||this.initSession(e).catch(()=>{})},2e3);f.unref?.(),this.retryTimers.add(f)}throw p}let a=l(()=>{for(let p of i)p();t.send("Page.removeScriptToEvaluateOnNewDocument",{identifier:r.identifier}).catch(()=>{}),t.send("Runtime.removeBinding",{name:ze}).catch(()=>{})},"dispose");this.disposers.add(a),i.push(()=>this.disposers.delete(a));let c=l(async(p,f,y)=>{if(!/^https?:/.test(f)||!y&&we(f))return;let{executionContextId:b}=await t.send("Page.createIsolatedWorld",{frameId:p,worldName:dn,grantUniveralAccess:!1},{timeoutMs:5e3});o.set(b,{frameId:p,mainFrame:y}),await t.send("Runtime.addBinding",{name:ze,executionContextId:b},{timeoutMs:5e3}),await t.send("Runtime.evaluate",{contextId:b,expression:"globalThis.__pfAcFlush && __pfAcFlush()",returnByValue:!0},{timeoutMs:5e3})},"arm"),d=l((p,f,y)=>{c(p,f,y).catch(b=>{/not found|Cannot find|No frame|detached|closed|destroyed|navigated/i.test(String(b))||this.emit("error",{page:s,frameId:p,url:f,error:`arm: ${String(b)}`})})},"armSafe");i.push(t.on("Page.frameNavigated",p=>{let f=e.isMain&&!p.frame.parentId;f&&this.reset(s,p.frame.url),d(p.frame.id,p.frame.url,f)}),t.on("Runtime.bindingCalled",p=>{if(p.name!==ze)return;let f=o.get(p.executionContextId);f&&this.onMessage(t,s,p.executionContextId,f,p.payload).catch(()=>{})}),t.on("Network.requestWillBeSent",p=>{if(!Fi(p.request.url))return;let f=this.state(s);f.cmpRequestAt===void 0&&(f.cmpRequestAt=Date.now(),f.roundAtCmpRequest=f.topAttempts)}));let{frameTree:u}=await t.send("Page.getFrameTree"),h=l((p,f)=>{d(p.frame.id,p.frame.url,f);for(let y of p.childFrames??[])h(y,!1)},"walk");h(u,e.isMain)},"initSession");async onMessage(e,t,s,o,i){let r;try{r=JSON.parse(i)}catch{return}let a=this.state(t),c=typeof r.url=="string"?r.url:"",d={page:t,frameId:o.frameId,url:c},u=typeof r.cmp=="string"?r.cmp:"";switch(r.type){case"init":{a.frames.add(o.frameId),o.mainFrame&&(a.topInitAt??=Date.now());let h={autoconsent:[],compact:Yd(or,{url:c,mainFrame:o.mainFrame})};await this.reply(e,s,{type:"initResp",config:this.config,rules:h});return}case"eval":await this.reply(e,s,{type:"evalResp",id:r.id,result:await this.runSnippet(e,t,s,o,r.snippetId)});return;case"cmpDetected":a.cmp??=u,a.detectedAt??=Date.now(),this.emit("cmpDetected",{...d,cmp:u});return;case"popupFound":a.cmp=u,a.popupAt??=Date.now(),this.emit("popupFound",{...d,cmp:u});return;case"optOutResult":a.cmp=u||a.cmp,a.optOut=r.result===!0,a.optOutAt=Date.now(),r.result===!0&&r.scheduleSelfTest===!0&&(a.selfTestTarget={s:e,contextId:s}),this.emit("optOutResult",{...d,cmp:u,result:r.result===!0});return;case"autoconsentDone":{a.cmp=u||a.cmp,a.doneAt=Date.now(),a.cosmetic=r.isCosmetic===!0,this.emit("autoconsentDone",{...d,cmp:u,cosmetic:a.cosmetic,durationMs:Number(r.duration)||0,clicks:Number(r.totalClicks)||0});let h=a.selfTestTarget;a.selfTestTarget=void 0,this.opts.selfTest!==!1&&h&&await this.reply(h.s,h.contextId,{type:"selfTest"});return}case"selfTestResult":a.selfTest=r.result===!0,this.emit("selfTestResult",{...d,cmp:u,result:a.selfTest});return;case"optInResult":this.emit("error",{...d,error:"unexpected optInResult",details:r});return;case"autoconsentError":this.emit("error",{...d,error:"autoconsent",details:r.details});return;case"pfRefusedClick":a.refused++,this.emit("error",{...d,error:`refused to click an accept button: "${String(r.text)}"`});return;case"report":{let h=r.state??{};if(!o.mainFrame||r.mainFrame!==!0)return;a.topLifecycle=h.lifecycle,a.topAttempts=Math.max(a.topAttempts,Number(h.findCmpAttempts)||0),h.heuristicPatterns?.length&&a.patternsAt===void 0&&(a.patternsAt=Date.now(),a.roundAtPatterns=a.topAttempts);return}default:return}}reply(e,t,s){return e.send("Runtime.evaluate",{contextId:t,expression:`autoconsentReceiveMessage(${JSON.stringify(s)})`,returnByValue:!0}).catch(()=>{})}async runSnippet(e,t,s,o,i){let r=typeof i=="string"?sr[i]:void 0;if(!r||Jd.has(i))return this.emit("error",{page:t,frameId:o.frameId,error:`refused eval snippet ${String(i)}`}),!1;let a=await e.send("Runtime.evaluate",{contextId:s,expression:"document",returnByValue:!1});if(!a.result.objectId)return!1;try{let{node:c}=await e.send("DOM.describeNode",{objectId:a.result.objectId}),{object:d}=await e.send("DOM.resolveNode",{backendNodeId:c.backendNodeId});if(!d.objectId)return!1;try{return(await e.send("Runtime.callFunctionOn",{objectId:d.objectId,functionDeclaration:`async function () { try { return !!(await (${r.toString()}).call(window)); } catch (e) { return false; } }`,returnByValue:!0,awaitPromise:!0})).result.value===!0}finally{e.send("Runtime.releaseObject",{objectId:d.objectId}).catch(()=>{})}}catch{return!1}finally{e.send("Runtime.releaseObject",{objectId:a.result.objectId}).catch(()=>{})}}state(e){let t=this.pages.get(e);return t||(t={navAt:Date.now(),url:e.url,cmp:null,refused:0,frames:new Set,topAttempts:0},this.pages.set(e,t)),t}reset(e,t){this.pages.set(e,{navAt:Date.now(),url:t,cmp:null,refused:0,frames:new Set,topAttempts:0})}emit(e,t){e==="error"&&this.emitter.listenerCount("error")===0||this.emitter.emit(e,t)}};var lu=["cmpDetected","popupFound","optOutResult","autoconsentDone","selfTestResult","error"],du=["blocked","error"];async function hn(n,e={}){let t=e.consent===!1?null:new Zt(typeof e.consent=="object"?e.consent:{}),s=e.adblock===!1?null:await Qt.create(typeof e.adblock=="object"?e.adblock:{}),o=[];if(e.onEvent){let r=e.onEvent;if(t)for(let a of lu)o.push(t.on(a,(c=>r({source:"consent",type:a,data:c}))));if(s)for(let a of du)o.push(s.on(a,(c=>r({source:"adblock",type:a,data:c}))))}let i=l(async r=>{await Promise.all([t?.initSession(r),s?.initSession(r)])},"init");return o.push(n.addSessionInitializer(i)),{consent:t,adblock:s,detach(){for(let r of o)r();t?.dispose(),s?.dispose()}}}l(hn,"attachInterrupts");var uu="pilotfish's open MCP makes no model calls: the MCP client's own model reads the page and decides the next step",hu=l(()=>{let n=l(async()=>{throw new Error(uu)},"refuse");return{messages:{create:n},beta:{messages:{create:n}}}},"noModelClient"),pn=class{static{l(this,"ConfirmThrough")}inner;canAsk;constructor(e,t){this.inner=e,this.canAsk=t}async check(e){let t=await this.inner.check(e);return t.kind!=="confirm"||this.canAsk()?t:{kind:"deny",reason:`${t.reason}. This needs a person's confirmation, and this MCP client cannot ask for one (no elicitation support)`}}},We=class{static{l(this,"BrowserSession")}config;opts;browser=null;launching=null;interrupts=null;tabs=null;agent=null;queue=Promise.resolve();policy;asker;constructor(e,t={}){this.config=e,this.opts=t,this.policy=new Ce({allowedHosts:e.allowedHosts,allowPrivateNetwork:e.allowPrivateNetwork})}get launched(){return!!this.browser&&!this.browser.closed}async page(){return await this.ensure(),this.tabs.current}get tabTable(){return this.tabs}async ensure(){return this.browser&&!this.browser.closed?this.browser:(this.launching??=this.start().finally(()=>{this.launching=null}),this.launching)}async start(){let e=await gs({...this.opts.launch,headless:this.config.headless});this.interrupts=await hn(e,{consent:!0,adblock:!0}).catch(()=>null);let t=e.allPages()[0]??await e.newPage();return this.tabs=new Ae(t),this.browser=e,e.on("disconnected",()=>{this.browser===e&&this.reset()}),e}reset(){this.agent?.dispose(),this.agent=null,this.tabs?.dispose(),this.tabs=null,this.interrupts?.detach(),this.interrupts=null,this.browser=null}executor(){return this.agent??=new ht(this.tabs.current,{tabs:this.tabs,client:hu(),secrets:this.config.secrets,policy:new pn(this.policy,()=>!!this.asker),consent:this.interrupts?.consent??void 0,allowScreenshots:this.config.allowScreenshots,askUser:l(e=>this.asker?.(e)??Promise.resolve(null),"askUser"),fastSettle:!0,enableQuery:!0,compactSnapshots:!0,autoDismiss:!0,awaitLoading:!0,actionSnapshotBand:0,snapshotMoreHint:"browser_scroll for the next screen, browser_get_text for the page's text, or browser_snapshot (whole_page) for every element"}),this.agent}async act(e,t,s={}){let o=this.queue.then(async()=>(await this.ensure(),this.executor().act(e,t,"",s)));return this.queue=o.catch(()=>{}),o}async obstacle(e={}){if(!this.launched)return null;let t=this.queue.then(()=>this.executor().obstacle(e));return this.queue=t.catch(()=>{}),t.catch(()=>null)}async close(){let e=this.browser??(this.launching?await this.launching.catch(()=>null):null);return e?(this.reset(),await e.close().catch(()=>{}),!0):!1}};import{McpServer as pu}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as H}from"zod";var ts="0.1.4";var Ge=Ie.click.shape.ref.describe('Element ref from the latest snapshot, e.g. "e12" or "f1e3"'),ne={browser_navigate:H.object({url:H.url({protocol:/^https?$/}).describe("Absolute http(s) URL"),snapshot:H.boolean().optional().describe("Default true. false: no page snapshot, only what happened (title, counts). Only when this same reply also reads the page with browser_query and you will not act on it; keep it for a page you have not seen or whose tabs, sorts, filters or forms you may use.")}).strict(),browser_snapshot:H.object({whole_page:H.boolean().optional().describe("Include everything, not only the viewport band (default false)")}).strict(),browser_click:H.object({ref:Ge}).strict(),browser_type:H.object({ref:Ge,text:H.string().max(2e4).describe("Text to enter (replaces what is there). Type secrets as {{name}} placeholders."),submit:H.boolean().optional().describe("Press Enter afterwards")}).strict(),browser_select:H.object({ref:Ge,values:H.array(H.string()).min(1).max(50).describe("Option labels or values")}).strict(),browser_press:H.object({key:H.string().min(1).max(40).describe('A key or shortcut: "Enter", "Escape", "Tab", "ArrowDown", "Control+a"')}).strict(),browser_scroll:H.object({direction:H.enum(["down","up","left","right"])}).strict(),browser_screenshot:H.object({}).strict(),browser_get_text:H.object({max_chars:H.number().int().min(500).max(6e4).optional().describe("At most this many characters (default 20000). Ask for less when the start of the page is enough."),selector:H.string().min(1).max(300).optional().describe('Only the text inside the first element matching this CSS selector, e.g. "main" or "#results"')}).strict(),browser_query:H.object({expression:H.string().min(1).max(4e3).describe("A read-only JavaScript expression returning just the values you need as JSON, e.g. [...document.querySelectorAll('.result')].slice(0,10).map(e => e.innerText). Runs in a world of its own: it must not click, type, submit or change the page.")}).strict(),browser_find:H.object({query:H.string().min(1).max(300).describe('Words naming the element(s), e.g. "Sort by" or "price max input, Go button"')}).strict(),browser_wait:H.object({seconds:H.number().min(.5).max(10).describe("Seconds to wait before looking at the page again")}).strict(),browser_resume:H.object({timeout_s:H.number().int().min(5).max(900).optional().describe("How long to wait for the page to move on (default 300)")}).strict(),browser_actions:H.object({actions:H.array(H.discriminatedUnion("action",[H.object({action:H.literal("click"),ref:Ge}).strict(),H.object({action:H.literal("type"),ref:Ge,text:H.string().max(2e4),submit:H.boolean().optional()}).strict(),H.object({action:H.literal("select"),ref:Ge,values:H.array(H.string()).min(1).max(50)}).strict(),H.object({action:H.literal("press"),key:H.string().min(1).max(40)}).strict(),H.object({action:H.literal("scroll"),direction:H.enum(["down","up","left","right"])}).strict(),H.object({action:H.literal("wait"),seconds:H.number().min(.5).max(10)}).strict()])).min(1).max(10).describe("Steps run in order, e.g. fill a form's fields then submit. They stop at the first that fails or when the page moves to another address (later refs would belong to the old page).")}).strict(),browser_tabs:H.object({switch_to:H.string().min(1).max(10).optional().describe('Tab id to work in from now on, e.g. "t2"')}).strict(),browser_close:H.object({}).strict()};function xe(n,e=!1){return{content:[{type:"text",text:n}],...e?{isError:!0}:{}}}l(xe,"text");function fu(n){let e=Object.keys(n.secrets);return["pilotfish drives a real Chromium for you: you read each page and decide every step; pilotfish acts, checks each action and reports what changed.","Control: browser_navigate / browser_snapshot to get element refs (e12), then browser_click / browser_type / browser_select / browser_press / browser_scroll by ref. Every action returns EVIDENCE of what changed and, when the page changed, a snapshot of the visible screen with counts of the elements above and below it (browser_scroll for the next screen, browser_snapshot for three screens or whole_page for everything), or, when most of the screen is as in a snapshot you were given a few results back, only the lines that changed; refs stay valid while their element is on the page.","Reading: browser_query reads just the values you need with read-only JavaScript (a results list, prices, counts): the cheapest way to read. browser_find returns refs for elements anywhere on the page. browser_get_text gives a whole page's text, or one part of it (selector, max_chars). browser_navigate with snapshot:false skips the screen: only beside a browser_query of that page in the same reply, for a page you will not act on.","Acting: browser_actions runs several steps in one call (fill a form's fields, then submit) and stops at the first that fails or after one that moves to another page.","Words like cheapest, latest, highest-rated or most mean the site's own sort or tab for that order (a list's default order is usually something else, e.g. 'Best' or 'Featured'): apply it, then read the top.","The browser closes by itself when this client disconnects; browser_close is only for starting over within a session.",e.length?`Secrets are typed as placeholders and never shown: ${e.map(t=>`{{${t}}}`).join(", ")}.`:"",n.allowedHosts.length?`Navigation is limited to: ${n.allowedHosts.join(", ")}.`:"","When a result says NEEDS A PERSON (a sign-in, a one-time code or 2FA, a device approval, a CAPTCHA), tell the user in your reply what the page needs and that the pilotfish browser window is open on it; when they have done it there, call browser_resume and carry on. BLOCKED and STOP mean the site cannot be continued that way."].filter(Boolean).join(`
415
415
  `)}l(fu,"serverInstructions");function fn(n={}){let e=n.config??n.session?.config??yt(),t=!n.session,s=n.session??new We(e,n),o=new pu({name:"pilotfish",version:n.version??ts},{capabilities:{logging:{}},instructions:fu(e)}),i=Object.keys(e.secrets),r=i.length?` Secrets: ${i.map(k=>`{{${k}}}`).join(", ")}.`:"",a=l(()=>!!o.server.getClientCapabilities()?.elicitation?.form,"canElicit"),c=l(k=>a()?async x=>{let v=await o.server.elicitInput({message:x,requestedSchema:{type:"object",properties:{answer:{type:"string",title:"Answer",description:'Reply "yes" to allow an action'}},required:["answer"]}},{relatedRequestId:k.requestId,timeout:e.interventionTimeoutMs}).catch(()=>null);return v?.action==="accept"&&typeof v.content?.answer=="string"?v.content.answer:null}:void 0,"askerFor"),d=l(k=>k.replace(/in the live view/g,"in the pilotfish browser window").replace(/Please send the code, or type it/,"Please type the code"),"forWindow"),u=l(k=>k.needs==="solve_in_live_view"||k.needs==="login_in_live_view"||k.needs==="code"||k.needs==="credentials","byHand"),h=l((k,x="")=>[`NEEDS A PERSON (${k.category}${k.vendor?`, ${k.vendor}`:""}): ${d(k.message)}`,x,u(k)&&e.headless?"This browser is headless, so it cannot be done by hand here: tell the user, and that the pilotfish server needs PILOTFISH_HEADLESS=0 (a visible window) for steps like this one.":`Tell the user now, in your reply: what the page needs, and that the pilotfish browser window is open on it (${k.url}). Ask them to do it there and to tell you when it is done. Then call browser_resume: it waits for the page to move on and returns it.`,"Do not try to get past it yourself: pilotfish does not type passwords or codes it was not given, and does not solve CAPTCHAs."].filter(Boolean).join(`
416
416
  `),"needsPerson"),p=l(async()=>{let k=await s.act("read_page",{}).catch(()=>null);return k?k.text||k.summary:""},"pageNow"),f=l(async(k,x=1e3)=>{let v=Date.now()+k,R=await s.obstacle({fresh:!0});for(;R&&Date.now()<v;)await new Promise(M=>setTimeout(M,x)),R=await s.obstacle({fresh:!0});return R},"untilClear"),y=l((k,x)=>({...k,content:[...k.content,{type:"text",text:x}]}),"withNote"),b=l(async(k,x)=>{let v=await s.obstacle();if(!v)return x;if(v.autoResolvable&&!v.neverAutomate&&(v=await f(12e3),!v))return y(x,`A browser check was showing and cleared by itself. The page now:
417
417
  ${await p()}`);if(v.neverAutomate)return y(x,`STOP: ${v.host} shows a bot check, and this site is never automated past one (ticket sellers: the US BOTS Act; UIDAI: the Aadhaar Act). Do not continue on it; tell the user.`);if(v.category==="hard_block"||v.needs==="none_possible")return y(x,`BLOCKED: ${d(v.message)} A person cannot fix this from the browser window: try another way (another page or site), or tell the user.`);if(a()&&!(u(v)&&e.headless)){let R=await o.server.elicitInput({message:`${d(v.message)} (${v.url})`,requestedSchema:{type:"object",properties:{done:{type:"boolean",title:"I have done it in the pilotfish browser window"}},required:["done"]}},{relatedRequestId:k.requestId,timeout:e.interventionTimeoutMs}).catch(()=>null);if(R?.action==="accept"&&R.content?.done!==!1){let M=await f(15e3);return M?y(x,h(M,"The person said it is done, but the page still shows it.")):y(x,`The person handled it. The page now:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaiventures/pilotfish-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A real Chromium on your machine for any MCP agent: ref-based tools, evidence per action, no API key.",
5
5
  "license": "MIT",
6
6
  "type": "module",