@agimon-ai/browse-tool 0.17.2 → 0.17.3
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/dist/cli.cjs +5 -5
- package/dist/cli.mjs +5 -5
- package/package.json +5 -5
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const e=require(`./streamable-http-C871T1fS.cjs`);require(`reflect-metadata/lite`);let t=require(`@agimon-ai/foundation-port-registry`),n=require(`@agimon-ai/foundation-process-registry`),r=require(`inversify`),i=require(`node:path`);i=e.k(i,1);let a=require(`node:fs`),o=require(`node:fs/promises`),s=require(`node:os`),c=require(`node:module`),l=require(`zod`),u=require(`@opentelemetry/api`),d=require(`node:crypto`),f=require(`@agimon-ai/foundation-validator`),p=require(`@modelcontextprotocol/sdk/server/index.js`),m=require(`@modelcontextprotocol/sdk/types.js`),h=require(`@modelcontextprotocol/sdk/server/stdio.js`),g=require(`commander`),_=require(`@hono/node-server`),v=require(`hono`),y=require(`hono/cors`),b=require(`@hono/node-ws`),x=require(`hono/html`),S=require(`hono/jsx`),C=require(`hono/jsx/jsx-runtime`);var w=`0.17.1`;const T=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function E(e,t){if(!T)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function D(t){let n=new v.Hono,r;try{r=t.get(e.O.TelemetryService)}catch{r=new e.g}return n.get(`/telemetry-config`,async t=>{try{let n=new URL(t.req.url).origin;return t.json(await e._(process.env,n))}catch(e){return t.json({enabled:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/tasks`,n=>{try{let r=t.get(e.O.ExtensionTaskQueue).getNextTask();return r?n.json({task:{id:r.id,tool:r.tool,arguments:r.arguments,telemetry:r.telemetry}}):n.json({})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/result`,async n=>{try{let r=await n.req.json();if(!r.taskId)return n.json({success:!1,error:`Missing taskId in request body`},400);let i=t.get(e.O.ExtensionTaskQueue),a=t.get(e.O.BrowserService),o={taskId:r.taskId,success:r.success,result:r.result,error:r.error},s=i.submitResult(o);return s?(s.browserId&&a.recordBrowserActivity(s.browserId,s.pageId),n.json({success:!0})):n.json({success:!1,error:`Task ${r.taskId} not found or already completed`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/status`,n=>{try{let r=t.get(e.O.ExtensionTaskQueue),i=r.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:r.queueSize};return n.json(a)}catch(e){return n.json({connected:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/register`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.O.ExtensionSessionRegistry).register(r);return n.json({success:!0,session:{id:i.id,browserId:i.browserId,controlMode:i.controlMode,createdAt:i.createdAt.toISOString()}})}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/heartbeat`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.O.ExtensionSessionRegistry).heartbeat(r);return i?n.json({success:!0,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested,lastHeartbeatAt:i.lastHeartbeatAt.toISOString()}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/tab-mapped`,async n=>{try{let r=await n.req.json();if(!r.pageId||typeof r.tabId!=`number`)return n.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=t.get(e.O.PageRegistry),a=t.get(e.O.BrowserService),o=i.get(r.pageId);return o?(o.extensionTabId=r.tabId,a.recordBrowserActivity(o.browserId,r.pageId),n.json({success:!0})):n.json({success:!1,error:`Page ${r.pageId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.O.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return E(`artifact received`,{browserId:r.browserId,videoBase64Size:r.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(r.browserId),n.json({success:!0})):n.json({success:!1,error:`Browser "${r.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording/chunk`,async n=>{try{let i=await n.req.json();if(!i.browserId||!i.chunkBase64)return n.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=t.get(e.O.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return E(`chunk received`,{browserId:i.browserId,chunkIndex:i.chunkIndex,mimeType:i.mimeType,persisted:!!o,chunkBase64Size:i.chunkBase64.length}),o?(r.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":i.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof i.chunkIndex==`number`?{"browse_tool.recording.chunk_index":i.chunkIndex}:{},...typeof i.mimeType==`string`?{"browse_tool.recording.mime_type":i.mimeType}:{}}}),a.recordBrowserActivity(i.browserId),n.json({success:!0})):n.json({success:!1,error:`Browser "${i.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/browser-log`,async e=>{try{let t=await e.req.json();return!t.message||typeof t.message!=`string`?e.json({success:!1,error:`Missing message in request body`},400):(r.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),E(`browser log relayed`,{level:t.level??`info`,message:t.message,attributes:t.attributes}),e.json({success:!0}))}catch(t){return e.json({success:!1,error:t instanceof Error?t.message:String(t)},500)}}),n.post(`/handoff`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.O.ExtensionSessionRegistry).requestHandoff(r);return i?n.json({success:!0,message:`Handoff requested. AI will take control when ready.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/handoff/acknowledge`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.O.ExtensionSessionRegistry).acknowledgeHandoff(r.sessionId);return i?n.json({success:!0,message:`Handoff acknowledged. AI now has control.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found or no handoff pending`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/sessions`,n=>{try{let r=t.get(e.O.ExtensionSessionRegistry).listSessions();return n.json({sessions:r.map(e=>({id:e.id,browserId:e.browserId,tabId:e.tabId,currentUrl:e.currentUrl,controlMode:e.controlMode,activeSpecPath:e.activeSpecPath,handoffRequested:e.handoffRequested,createdAt:e.createdAt.toISOString(),lastHeartbeatAt:e.lastHeartbeatAt.toISOString()}))})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n}function O(e){if(e==null||e===``)return;let t=typeof e==`number`?e:typeof e==`string`?Number.parseInt(e,10):NaN;if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}const k=[`pnpm-workspace.yaml`,`nx.json`,`.git`],ee=`browse-tool-chrome`,A=`tool`,j=process.env.NODE_ENV||`development`,te=`127.0.0.1`;function ne(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of k)if((0,a.existsSync)(i.default.join(t,e)))return t;let e=i.default.dirname(t);if(e===t)return process.cwd();t=e}}function re(){return new t.PortRegistryService(process.env.PORT_REGISTRY_PATH)}async function ie(e,t,n){let r=re(),i=await r.reservePort({repositoryPath:e,serviceName:ee,serviceType:A,environment:j,preferredPort:t,portRange:n,pid:process.pid,host:te,force:!0,metadata:{transport:`stdio`,mode:`chrome-serve`}});if(!i.success||!i.record)throw Error(i.error||`Failed to reserve port ${t}`);let a=!1;return{port:i.record.port,release:async()=>{if(a)return;a=!0;let n=await r.releasePort({repositoryPath:e,serviceName:ee,serviceType:A,environment:j,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function ae(){return new r.ContainerModule(t=>{t.bind(e.O.ExtensionTaskQueue).to(e.h).inSingletonScope(),t.bind(e.O.ExtensionToolDelegator).to(e.m).inSingletonScope()})}function oe(e){let t=new p.Server({name:`browse-tool-chrome`,version:`0.1.0`},{capabilities:{tools:{}}}),n=e.getSupportedTools().map(e=>({name:e,description:`Browser automation tool (Chrome extension mode): ${e}`,inputSchema:{type:`object`,properties:{},additionalProperties:!0}}));return t.setRequestHandler(m.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(m.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const se=new g.Command(`chrome-serve`).description(`[DEPRECATED] Start MCP server with Chrome extension HTTP polling for bot-detection-free browser automation`).option(`-p, --port <port>`,`HTTP server port for extension polling`).option(`-v, --verbose`,`Enable verbose output`,!1).option(`--wait-for-extension`,`Wait for extension to connect before accepting MCP requests`,!1).action(async i=>{console.error(``),console.error(`╔════════════════════════════════════════════════════════════════╗`),console.error(`║ DEPRECATED: chrome-serve is deprecated. ║`),console.error(`║ Use mcp-serve instead - extension routes are now ║`),console.error(`║ automatically available in the HTTP server at /extension/*. ║`),console.error(`╚════════════════════════════════════════════════════════════════╝`),console.error(``);let a,o;try{let s=O(i.port),c=ne(process.cwd()),l=process.env.PORT_REGISTRY_PATH;l&&(process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(l,`processes.json`)),a=await ie(c,s??t.DEFAULT_PORT_RANGE.min,s?{min:s,max:s}:t.DEFAULT_PORT_RANGE);let u=a.port;o=await(0,n.createProcessLease)({repositoryPath:c,serviceName:ee,serviceType:A,environment:j,pid:process.pid,port:u,host:te,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:i.waitForExtension}}),i.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${u}`),console.error(` Wait for extension: ${i.waitForExtension}`));let d=new r.Container({defaultScope:`Singleton`});d.load(ae());let f=d.get(e.O.ExtensionTaskQueue),p=d.get(e.O.ExtensionToolDelegator),m=new v.Hono;m.use(`*`,(0,y.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let g=D(d);m.route(`/extension`,g),m.get(`/health`,e=>{let t=f.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let b=(0,_.serve)({fetch:m.fetch,port:u});b.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${u} is already in use.`),console.error(`Recovery: Try a different port with --port <port>`)):console.error(`Error [SERVER_ERROR]: HTTP server error: ${e.message}`),process.exit(1)}),console.error(`HTTP server started on port ${u}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${u}/extension/tasks`),i.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{f.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let x=oe(p),S=new h.StdioServerTransport;await x.connect(S),console.error(`Chrome extension MCP server started on stdio`);let C=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{f.clearAllTasks(`Server shutting down`),await S.close(),b.close(),await o.release({kill:!1}),await a.release(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>C(`SIGINT`)),process.on(`SIGTERM`,()=>C(`SIGTERM`))}catch(e){if(o||a)try{await o?.release(),await a?.release()}catch{}let t=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${t}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),ce=`BROWSE_TOOL_CONFIG`,le=`.browse-tool`,ue=`config.json`,de=[`json`,`text`,`quiet`],fe={commands:{},tools:{}},pe=l.z.record(l.z.string(),l.z.unknown()),me=l.z.object({mcpServe:l.z.object({type:l.z.string().optional(),browser:l.z.string().optional(),headless:l.z.boolean().optional(),profile:l.z.string().optional(),mode:l.z.string().optional(),host:l.z.string().optional(),port:l.z.coerce.number().int().positive().optional(),httpPort:l.z.coerce.number().int().positive().optional(),tags:l.z.string().optional(),exclude:l.z.string().optional(),customTools:l.z.string().optional(),chromeForTestingPath:l.z.string().optional(),snippetsDir:l.z.string().optional(),registryPath:l.z.string().optional(),registryDir:l.z.string().optional(),pidsDir:l.z.string().optional(),profilesDir:l.z.string().optional(),proxyConfigDir:l.z.string().optional()}).partial().optional(),httpServe:l.z.object({port:l.z.coerce.number().int().positive().optional(),headless:l.z.boolean().optional(),idleTimeout:l.z.coerce.number().positive().optional(),host:l.z.string().optional(),registryDir:l.z.string().optional(),registryPath:l.z.string().optional(),pidsDir:l.z.string().optional(),profilesDir:l.z.string().optional(),snippetsDir:l.z.string().optional(),proxyConfigDir:l.z.string().optional()}).partial().optional(),exec:l.z.object({format:l.z.enum(de).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),tools:l.z.object({format:l.z.enum(de).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),status:l.z.record(l.z.string(),l.z.unknown()).optional(),stop:l.z.record(l.z.string(),l.z.unknown()).optional()}),he=l.z.object({commands:me.default({}),tools:l.z.record(l.z.string(),pe).default({})});let M={config:fe};function ge(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(n===`--config`)return e[t+1];if(n.startsWith(`--config=`))return n.slice(9)}}function _e(e){let t=i.default.resolve(e);if(!(0,a.existsSync)(t))throw Error(`Config path not found: ${t}`);return(0,a.statSync)(t).isDirectory()?i.default.join(t,ue):t}function ve(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),o=t.homeDir??(0,s.homedir)(),c=ge(e);if(c)return _e(c);if(n[ce])return _e(n[ce]);let l=i.default.join(r,le,ue);if((0,a.existsSync)(l))return l;let u=i.default.join(o,le,ue);if((0,a.existsSync)(u))return u}function ye(e){let t=(0,a.readFileSync)(e,`utf8`),n=JSON.parse(t);return he.parse(n)}function be(e,t={}){let n=ve(e,t);return n?{configPath:n,config:ye(n)}:{config:fe}}function xe(e,t={}){return M=be(e,t),M}function N(e){return(M.config.commands??{})[e]??{}}function Se(e){return M.config.tools?.[e]??{}}function P(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}var Ce=class{port;exactPort;timeout;serverPort=null;constructor(t={}){this.port=t.port??e.d,this.exactPort=t.exactPort??!1,this.timeout=t.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let t=await e.o().get(e.O.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!t.running||!t.port)throw Error(t.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=t.port,t.port}async getBaseUrl(){return`http://localhost:${await this.ensureServer()}`}async listTools(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/tools`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async listCustomTools(e){let t=await this.getBaseUrl(),n=new AbortController,r=setTimeout(()=>n.abort(),this.timeout);try{let r=new URL(`/custom-tools`,t);r.searchParams.set(`dir`,e);let i=await fetch(r,{method:`GET`,headers:{Accept:`application/json`},signal:n.signal});if(!i.ok)throw Error(`HTTP ${i.status}: ${i.statusText}`);let a=await i.json();if(a.error)throw Error(a.error);return a.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(r)}}async listBrowsers(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/browsers`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.browsers}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async execute(e,t){let n=await this.getBaseUrl(),r=e===`run_spec`?void 0:this.timeout,i=new AbortController,a=r===void 0?void 0:setTimeout(()=>i.abort(),r);try{let a=await fetch(`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:e,arguments:t}),signal:r===void 0?void 0:i.signal});if(!a.ok){let e=await a.text();throw Error(`HTTP ${a.status}: ${e||a.statusText}`)}let o=await a.json();return o.success?o.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:o.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{a&&clearTimeout(a)}}async executeCustomTool(e,t,n){let r=await this.getBaseUrl(),i=new AbortController,a=setTimeout(()=>i.abort(),this.timeout);try{let a=new URL(`/custom-tools`,r);a.searchParams.set(`dir`,e);let o=await fetch(a,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:t,arguments:n}),signal:i.signal});if(!o.ok){let e=await o.text();throw Error(`HTTP ${o.status}: ${e||o.statusText}`)}let s=await o.json();return s.success?s.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:s.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(a)}}wrapConnectionError(e){return e instanceof Error?e.message.includes(`ECONNREFUSED`)||e.message.includes(`fetch failed`)?Error(`Cannot connect to browse-tool HTTP server.\n\nTry one of the following:\n 1. Start the server: browse-tool http-serve\n 2. Check if another process is using port ${this.port}\n 3. Run with a different port: browse-tool --port 3201 <command>\n\nOriginal error: ${e.message}`,{cause:e}):e:Error(String(e),{cause:e})}};function F(e){return new Ce(e)}const we={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function I(e,t,n){return n?`${we[t]}${e}${we.reset}`:e}function Te(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Ee(e):De(e,r)}function Ee(e){return JSON.stringify(e,null,2)}function De(e,t){let n=[];e.isError&&n.push(I(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(Oe(r,t)):r.type===`image`?n.push(je(r,t)):n.push(I(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
2
|
+
const e=require(`./streamable-http-C871T1fS.cjs`);require(`reflect-metadata/lite`);let t=require(`@agimon-ai/foundation-port-registry`),n=require(`@agimon-ai/foundation-process-registry`),r=require(`inversify`),i=require(`node:path`);i=e.k(i,1);let a=require(`node:fs`),o=require(`node:fs/promises`),s=require(`node:os`),c=require(`node:module`),l=require(`zod`),u=require(`@opentelemetry/api`),d=require(`node:crypto`),f=require(`@agimon-ai/foundation-validator`),p=require(`@modelcontextprotocol/sdk/server/index.js`),m=require(`@modelcontextprotocol/sdk/types.js`),h=require(`@modelcontextprotocol/sdk/server/stdio.js`),g=require(`commander`),_=require(`@hono/node-server`),v=require(`hono`),y=require(`hono/cors`),b=require(`@hono/node-ws`),x=require(`hono/html`),S=require(`hono/jsx`),C=require(`hono/jsx/jsx-runtime`);var w=`0.17.2`;const T=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function E(e,t){if(!T)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function D(t){let n=new v.Hono,r;try{r=t.get(e.O.TelemetryService)}catch{r=new e.g}return n.get(`/telemetry-config`,async t=>{try{let n=new URL(t.req.url).origin;return t.json(await e._(process.env,n))}catch(e){return t.json({enabled:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/tasks`,n=>{try{let r=t.get(e.O.ExtensionTaskQueue).getNextTask();return r?n.json({task:{id:r.id,tool:r.tool,arguments:r.arguments,telemetry:r.telemetry}}):n.json({})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/result`,async n=>{try{let r=await n.req.json();if(!r.taskId)return n.json({success:!1,error:`Missing taskId in request body`},400);let i=t.get(e.O.ExtensionTaskQueue),a=t.get(e.O.BrowserService),o={taskId:r.taskId,success:r.success,result:r.result,error:r.error},s=i.submitResult(o);return s?(s.browserId&&a.recordBrowserActivity(s.browserId,s.pageId),n.json({success:!0})):n.json({success:!1,error:`Task ${r.taskId} not found or already completed`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/status`,n=>{try{let r=t.get(e.O.ExtensionTaskQueue),i=r.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:r.queueSize};return n.json(a)}catch(e){return n.json({connected:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/register`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.O.ExtensionSessionRegistry).register(r);return n.json({success:!0,session:{id:i.id,browserId:i.browserId,controlMode:i.controlMode,createdAt:i.createdAt.toISOString()}})}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/heartbeat`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.O.ExtensionSessionRegistry).heartbeat(r);return i?n.json({success:!0,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested,lastHeartbeatAt:i.lastHeartbeatAt.toISOString()}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/tab-mapped`,async n=>{try{let r=await n.req.json();if(!r.pageId||typeof r.tabId!=`number`)return n.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=t.get(e.O.PageRegistry),a=t.get(e.O.BrowserService),o=i.get(r.pageId);return o?(o.extensionTabId=r.tabId,a.recordBrowserActivity(o.browserId,r.pageId),n.json({success:!0})):n.json({success:!1,error:`Page ${r.pageId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.O.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return E(`artifact received`,{browserId:r.browserId,videoBase64Size:r.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(r.browserId),n.json({success:!0})):n.json({success:!1,error:`Browser "${r.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording/chunk`,async n=>{try{let i=await n.req.json();if(!i.browserId||!i.chunkBase64)return n.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=t.get(e.O.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return E(`chunk received`,{browserId:i.browserId,chunkIndex:i.chunkIndex,mimeType:i.mimeType,persisted:!!o,chunkBase64Size:i.chunkBase64.length}),o?(r.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":i.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof i.chunkIndex==`number`?{"browse_tool.recording.chunk_index":i.chunkIndex}:{},...typeof i.mimeType==`string`?{"browse_tool.recording.mime_type":i.mimeType}:{}}}),a.recordBrowserActivity(i.browserId),n.json({success:!0})):n.json({success:!1,error:`Browser "${i.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/browser-log`,async e=>{try{let t=await e.req.json();return!t.message||typeof t.message!=`string`?e.json({success:!1,error:`Missing message in request body`},400):(r.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),E(`browser log relayed`,{level:t.level??`info`,message:t.message,attributes:t.attributes}),e.json({success:!0}))}catch(t){return e.json({success:!1,error:t instanceof Error?t.message:String(t)},500)}}),n.post(`/handoff`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.O.ExtensionSessionRegistry).requestHandoff(r);return i?n.json({success:!0,message:`Handoff requested. AI will take control when ready.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/handoff/acknowledge`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.O.ExtensionSessionRegistry).acknowledgeHandoff(r.sessionId);return i?n.json({success:!0,message:`Handoff acknowledged. AI now has control.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found or no handoff pending`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/sessions`,n=>{try{let r=t.get(e.O.ExtensionSessionRegistry).listSessions();return n.json({sessions:r.map(e=>({id:e.id,browserId:e.browserId,tabId:e.tabId,currentUrl:e.currentUrl,controlMode:e.controlMode,activeSpecPath:e.activeSpecPath,handoffRequested:e.handoffRequested,createdAt:e.createdAt.toISOString(),lastHeartbeatAt:e.lastHeartbeatAt.toISOString()}))})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n}function O(e){if(e==null||e===``)return;let t=typeof e==`number`?e:typeof e==`string`?Number.parseInt(e,10):NaN;if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}const k=[`pnpm-workspace.yaml`,`nx.json`,`.git`],ee=`browse-tool-chrome`,A=`tool`,j=process.env.NODE_ENV||`development`,te=`127.0.0.1`;function ne(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of k)if((0,a.existsSync)(i.default.join(t,e)))return t;let e=i.default.dirname(t);if(e===t)return process.cwd();t=e}}function re(){return new t.PortRegistryService(process.env.PORT_REGISTRY_PATH)}async function ie(e,t,n){let r=re(),i=await r.reservePort({repositoryPath:e,serviceName:ee,serviceType:A,environment:j,preferredPort:t,portRange:n,pid:process.pid,host:te,force:!0,metadata:{transport:`stdio`,mode:`chrome-serve`}});if(!i.success||!i.record)throw Error(i.error||`Failed to reserve port ${t}`);let a=!1;return{port:i.record.port,release:async()=>{if(a)return;a=!0;let n=await r.releasePort({repositoryPath:e,serviceName:ee,serviceType:A,environment:j,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function ae(){return new r.ContainerModule(t=>{t.bind(e.O.ExtensionTaskQueue).to(e.h).inSingletonScope(),t.bind(e.O.ExtensionToolDelegator).to(e.m).inSingletonScope()})}function oe(e){let t=new p.Server({name:`browse-tool-chrome`,version:`0.1.0`},{capabilities:{tools:{}}}),n=e.getSupportedTools().map(e=>({name:e,description:`Browser automation tool (Chrome extension mode): ${e}`,inputSchema:{type:`object`,properties:{},additionalProperties:!0}}));return t.setRequestHandler(m.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(m.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const se=new g.Command(`chrome-serve`).description(`[DEPRECATED] Start MCP server with Chrome extension HTTP polling for bot-detection-free browser automation`).option(`-p, --port <port>`,`HTTP server port for extension polling`).option(`-v, --verbose`,`Enable verbose output`,!1).option(`--wait-for-extension`,`Wait for extension to connect before accepting MCP requests`,!1).action(async i=>{console.error(``),console.error(`╔════════════════════════════════════════════════════════════════╗`),console.error(`║ DEPRECATED: chrome-serve is deprecated. ║`),console.error(`║ Use mcp-serve instead - extension routes are now ║`),console.error(`║ automatically available in the HTTP server at /extension/*. ║`),console.error(`╚════════════════════════════════════════════════════════════════╝`),console.error(``);let a,o;try{let s=O(i.port),c=ne(process.cwd()),l=process.env.PORT_REGISTRY_PATH;l&&(process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(l,`processes.json`)),a=await ie(c,s??t.DEFAULT_PORT_RANGE.min,s?{min:s,max:s}:t.DEFAULT_PORT_RANGE);let u=a.port;o=await(0,n.createProcessLease)({repositoryPath:c,serviceName:ee,serviceType:A,environment:j,pid:process.pid,port:u,host:te,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:i.waitForExtension}}),i.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${u}`),console.error(` Wait for extension: ${i.waitForExtension}`));let d=new r.Container({defaultScope:`Singleton`});d.load(ae());let f=d.get(e.O.ExtensionTaskQueue),p=d.get(e.O.ExtensionToolDelegator),m=new v.Hono;m.use(`*`,(0,y.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let g=D(d);m.route(`/extension`,g),m.get(`/health`,e=>{let t=f.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let b=(0,_.serve)({fetch:m.fetch,port:u});b.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${u} is already in use.`),console.error(`Recovery: Try a different port with --port <port>`)):console.error(`Error [SERVER_ERROR]: HTTP server error: ${e.message}`),process.exit(1)}),console.error(`HTTP server started on port ${u}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${u}/extension/tasks`),i.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{f.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let x=oe(p),S=new h.StdioServerTransport;await x.connect(S),console.error(`Chrome extension MCP server started on stdio`);let C=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{f.clearAllTasks(`Server shutting down`),await S.close(),b.close(),await o.release({kill:!1}),await a.release(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>C(`SIGINT`)),process.on(`SIGTERM`,()=>C(`SIGTERM`))}catch(e){if(o||a)try{await o?.release(),await a?.release()}catch{}let t=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${t}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),ce=`BROWSE_TOOL_CONFIG`,le=`.browse-tool`,ue=`config.json`,de=[`json`,`text`,`quiet`],fe={commands:{},tools:{}},pe=l.z.record(l.z.string(),l.z.unknown()),me=l.z.object({mcpServe:l.z.object({type:l.z.string().optional(),browser:l.z.string().optional(),headless:l.z.boolean().optional(),profile:l.z.string().optional(),mode:l.z.string().optional(),host:l.z.string().optional(),port:l.z.coerce.number().int().positive().optional(),httpPort:l.z.coerce.number().int().positive().optional(),tags:l.z.string().optional(),exclude:l.z.string().optional(),customTools:l.z.string().optional(),chromeForTestingPath:l.z.string().optional(),snippetsDir:l.z.string().optional(),registryPath:l.z.string().optional(),registryDir:l.z.string().optional(),pidsDir:l.z.string().optional(),profilesDir:l.z.string().optional(),proxyConfigDir:l.z.string().optional()}).partial().optional(),httpServe:l.z.object({port:l.z.coerce.number().int().positive().optional(),headless:l.z.boolean().optional(),idleTimeout:l.z.coerce.number().positive().optional(),host:l.z.string().optional(),registryDir:l.z.string().optional(),registryPath:l.z.string().optional(),pidsDir:l.z.string().optional(),profilesDir:l.z.string().optional(),snippetsDir:l.z.string().optional(),proxyConfigDir:l.z.string().optional()}).partial().optional(),exec:l.z.object({format:l.z.enum(de).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),tools:l.z.object({format:l.z.enum(de).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),status:l.z.record(l.z.string(),l.z.unknown()).optional(),stop:l.z.record(l.z.string(),l.z.unknown()).optional()}),he=l.z.object({commands:me.default({}),tools:l.z.record(l.z.string(),pe).default({})});let M={config:fe};function ge(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(n===`--config`)return e[t+1];if(n.startsWith(`--config=`))return n.slice(9)}}function _e(e){let t=i.default.resolve(e);if(!(0,a.existsSync)(t))throw Error(`Config path not found: ${t}`);return(0,a.statSync)(t).isDirectory()?i.default.join(t,ue):t}function ve(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),o=t.homeDir??(0,s.homedir)(),c=ge(e);if(c)return _e(c);if(n[ce])return _e(n[ce]);let l=i.default.join(r,le,ue);if((0,a.existsSync)(l))return l;let u=i.default.join(o,le,ue);if((0,a.existsSync)(u))return u}function ye(e){let t=(0,a.readFileSync)(e,`utf8`),n=JSON.parse(t);return he.parse(n)}function be(e,t={}){let n=ve(e,t);return n?{configPath:n,config:ye(n)}:{config:fe}}function xe(e,t={}){return M=be(e,t),M}function N(e){return(M.config.commands??{})[e]??{}}function Se(e){return M.config.tools?.[e]??{}}function P(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}var Ce=class{port;exactPort;timeout;serverPort=null;constructor(t={}){this.port=t.port??e.d,this.exactPort=t.exactPort??!1,this.timeout=t.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let t=await e.o().get(e.O.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!t.running||!t.port)throw Error(t.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=t.port,t.port}async getBaseUrl(){return`http://localhost:${await this.ensureServer()}`}async listTools(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/tools`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async listCustomTools(e){let t=await this.getBaseUrl(),n=new AbortController,r=setTimeout(()=>n.abort(),this.timeout);try{let r=new URL(`/custom-tools`,t);r.searchParams.set(`dir`,e);let i=await fetch(r,{method:`GET`,headers:{Accept:`application/json`},signal:n.signal});if(!i.ok)throw Error(`HTTP ${i.status}: ${i.statusText}`);let a=await i.json();if(a.error)throw Error(a.error);return a.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(r)}}async listBrowsers(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/browsers`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.browsers}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async execute(e,t){let n=await this.getBaseUrl(),r=e===`run_spec`?void 0:this.timeout,i=new AbortController,a=r===void 0?void 0:setTimeout(()=>i.abort(),r);try{let a=await fetch(`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:e,arguments:t}),signal:r===void 0?void 0:i.signal});if(!a.ok){let e=await a.text();throw Error(`HTTP ${a.status}: ${e||a.statusText}`)}let o=await a.json();return o.success?o.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:o.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{a&&clearTimeout(a)}}async executeCustomTool(e,t,n){let r=await this.getBaseUrl(),i=new AbortController,a=setTimeout(()=>i.abort(),this.timeout);try{let a=new URL(`/custom-tools`,r);a.searchParams.set(`dir`,e);let o=await fetch(a,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:t,arguments:n}),signal:i.signal});if(!o.ok){let e=await o.text();throw Error(`HTTP ${o.status}: ${e||o.statusText}`)}let s=await o.json();return s.success?s.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:s.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(a)}}wrapConnectionError(e){return e instanceof Error?e.message.includes(`ECONNREFUSED`)||e.message.includes(`fetch failed`)?Error(`Cannot connect to browse-tool HTTP server.\n\nTry one of the following:\n 1. Start the server: browse-tool http-serve\n 2. Check if another process is using port ${this.port}\n 3. Run with a different port: browse-tool --port 3201 <command>\n\nOriginal error: ${e.message}`,{cause:e}):e:Error(String(e),{cause:e})}};function F(e){return new Ce(e)}const we={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function I(e,t,n){return n?`${we[t]}${e}${we.reset}`:e}function Te(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Ee(e):De(e,r)}function Ee(e){return JSON.stringify(e,null,2)}function De(e,t){let n=[];e.isError&&n.push(I(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(Oe(r,t)):r.type===`image`?n.push(je(r,t)):n.push(I(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
3
3
|
`)}function Oe(e,t){let n=e.text;try{return ke(JSON.parse(n),t)}catch{return n}}function ke(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=I(r,`cyan`,t),a=Ae(i,t);n.push(`${e}: ${a}`)}return n.join(`
|
|
4
4
|
`)}function Ae(e,t){return e===null?I(`null`,`gray`,t):e===void 0?I(`undefined`,`gray`,t):typeof e==`boolean`?I(String(e),e?`green`:`red`,t):typeof e==`number`?I(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?I(e,`blue`,t):e:Array.isArray(e)?e.length===0?I(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function je(e,t){let{mimeType:n,data:r}=e;return I(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function L(e,t){return I(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function Me(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=I(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
|
|
5
5
|
`)}function Ne(e,t){let n=N(`tools`),r=P(e,`format`,t.format,n.format),i=P(e,`color`,t.color,n.color);return{port:P(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const Pe=new g.Command(`list-custom-tools`).description(`List custom tools from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.d)).action(async function(e,t){let{port:n,formatterOptions:r}=Ne(this,t);try{let t=await F({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?Me(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(L(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),Fe=new g.Command(`exec-custom-tool`).description(`Execute a custom tool from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).argument(`<tool>`,`Custom tool name to execute`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.d)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=Ne(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(L(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await F({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=Te(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(L(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new g.Command(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(Pe).addCommand(Fe);const Ie=new g.Command(`docker-build-cft`).description(`Build the Chrome for Testing Docker image used by vm mode`).option(`--cft-version <version>`,`Chrome for Testing version to build`,e.D).option(`--image <image>`,`Docker image tag to produce`,e.T).option(`--platform <platform>`,`Docker target platform`,e.E).action(async t=>{try{let n=await e.S({version:t.cftVersion,image:t.image,platform:t.platform,stdio:`inherit`});console.log(`Built Docker image ${n.image}`),console.log(` Version: ${n.version}`),console.log(` Platform: ${n.platform}`),console.log(` Archive: ${e.C(n.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),Le=new g.Command(`exec`).description(`Execute a tool directly with JSON arguments`).argument(`<tool>`,`Tool name to execute (e.g., browser_launch)`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.d)).action(async function(e,t,n){let r=N(`exec`),i=P(this,`format`,n.format,r.format),a=P(this,`color`,n.color,r.color),o=P(this,`port`,n.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),s=this.getOptionValueSourceWithGlobals(`port`),c=s!==void 0&&s!==`default`&&s!==`implied`,l={format:i,color:a};try{let n;try{n=JSON.parse(t)}catch{console.error(L(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await F({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=Te(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(L(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),R=`pageId`,z=`browserId`,Re=l.z.record(l.z.string(),l.z.unknown()),ze=l.z.object({type:l.z.literal(`object`),properties:l.z.record(l.z.string(),l.z.unknown()).optional(),required:l.z.array(l.z.string()).optional(),additionalProperties:l.z.boolean().optional()}).passthrough(),Be=l.z.object({name:l.z.string().min(1),description:l.z.string().min(1),script:l.z.string().min(1),suggestionActions:l.z.string().min(1).optional(),capabilities:Re,inputSchema:ze}),Ve=l.z.object({tools:l.z.array(Be)}),He=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function Ue(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function B(e,t){if(He){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function V(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function We(e){return V(e)&&Array.isArray(e.content)}function H(e){let t=e.trim();return t===``?``:t.startsWith(`"`)&&t.endsWith(`"`)||t.startsWith(`[`)&&t.endsWith(`]`)||t.startsWith(`{`)&&t.endsWith(`}`)?JSON.parse(t):t.startsWith(`'`)&&t.endsWith(`'`)?t.slice(1,-1):t===`true`?!0:t===`false`?!1:t===`null`?null:/^-?\d+(\.\d+)?$/.test(t)?Number(t):t}function Ge(e){if(!e||Object.keys(e).length===0)return;let t={};for(let[n,r]of Object.entries(e))if(r!=null){if(typeof r==`string`||typeof r==`number`||typeof r==`boolean`){t[n]=r;continue}t[n]=JSON.stringify(r)}return Object.keys(t).length>0?t:void 0}function Ke(e){return e.replace(/^\uFEFF/,``).split(/\r?\n/).map(e=>{if(e.includes(` `))throw Error(`Tab indentation is not supported in tools.yaml`);let t=e.replace(/\s+#.*$/,``);return t.trim().length===0?null:{indent:t.match(/^ */)?.[0].length??0,text:t.trim()}}).filter(e=>e!==null)}function U(e,t,n){let r=e[t];if(!r||r.indent!==n)throw Error(`Invalid indentation in tools.yaml at line ${t+1}`);return r.text.startsWith(`-`)?qe(e,t,n):Je(e,t,n)}function qe(e,t,n){let r=[],i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||!t.text.startsWith(`-`))break;let a=t.text.slice(1).trim();if(a===``){let t=e[i+1];if(!t||t.indent<=n){r.push(null),i+=1;continue}let[a,o]=U(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=Ye(a),s={};if(o===void 0){let r=e[i+1];if(!r||r.indent<=n)throw Error(`Expected nested value for "${t}" in tools.yaml`);let[a,o]=U(e,i+1,r.indent);s[t]=a,i=o}else s[t]=H(o),i+=1;for(;i<e.length&&e[i].indent>n;){let t=e[i];if(t.indent!==n+2||t.text.startsWith(`-`)){let[n,r]=U(e,i,t.indent);if(!V(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=Ye(t.text);if(a===void 0){let n=e[i+1];if(!n||n.indent<=t.indent)throw Error(`Expected nested value for "${r}" in tools.yaml`);let[a,o]=U(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=H(a),i+=1}r.push(s);continue}r.push(H(a)),i+=1}return[r,i]}function Je(e,t,n){let r={},i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||t.text.startsWith(`-`))break;let[a,o]=Ye(t.text);if(o===void 0){let t=e[i+1];if(!t||t.indent<=n){r[a]=null,i+=1;continue}let[o,s]=U(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=H(o),i+=1}return[r,i]}function Ye(e){let t=e.indexOf(`:`);if(t===-1)throw Error(`Invalid tools.yaml entry: "${e}"`);let n=e.slice(0,t).trim(),r=e.slice(t+1).trim();return[n,r===``?void 0:r]}function Xe(e){let t=Ke(e);if(t.length===0)return{};let[n]=U(t,0,t[0].indent);return n}function Ze(e){e.inputSchema.properties===void 0&&(e.inputSchema.properties={});let t=e.inputSchema.properties;if(!V(t))throw Error(`Custom tool "${e.name}" inputSchema.properties must be an object`);let n=t[R],r=t[z];if(n!==void 0&&!(V(n)&&n.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!(V(r)&&r.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`);n===void 0&&(t[R]={type:`string`,description:`Browse-tool page ID to run this custom tool against. Either pageId or browserId is required at call time.`}),r===void 0&&(t[z]={type:`string`,description:`Browse-tool browser ID to run this custom tool against when no pageId is supplied. The tool will use the browser's current page or open a new one.`})}async function Qe(e){let t=(0,c.stripTypeScriptTypes)(await(0,o.readFile)(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function $e(e,t){let n=typeof t.run==`function`?t.run:void 0;if(!n)throw Error(`Custom tool script "${e}" must export a "run" function`);return{execute:n}}function et(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function tt(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(V(r)){let i=et(r,t);return{...e,content:[{...n,text:JSON.stringify(i,null,2)},...e.content.slice(1)]}}}catch{}return{...e,content:[...e.content,{type:`text`,text:`suggestionActions: ${t}`}]}}function nt(e,t,n){if(We(t))return n?tt(t,n):t;if(typeof t==`string`)return n?{content:[{type:`text`,text:JSON.stringify({result:t,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:t}]};if(t===void 0)return n?{content:[{type:`text`,text:JSON.stringify({result:`Custom tool "${e}" completed successfully`,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:`Custom tool "${e}" completed successfully`}]};let r=V(t)&&n?et(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var rt=class{constructor(t,n,r=new e.g,i){this.pageRegistry=t,this.extensionTaskQueue=n,this.telemetry=r,this.browserService=i}resolveToolPage(t,n,r){if(r.page)return r.page;if(r.mode===`extension`&&this.extensionTaskQueue){let t=new e.v(this.extensionTaskQueue);return t.setTarget(n,r.browserId),t}throw Error(`Custom tool "${t}" requires a supported page context`)}toPageSummary(e,t){return{pageId:t.id,url:t.url,title:t.title,active:e===t.id}}async createPageForBrowser(e,t,n){if(!this.browserService)throw Error(`Custom tool "${e}" requires browser service support to create a page`);let r=this.browserService.getBrowser(t);if(!r)throw Error(`Browser "${t}" not found`);let i=n?.setAsCurrent!==!1;if(r.mode===`extension`||r.mode===`vm`){if(!this.extensionTaskQueue)throw Error(`Custom tool "${e}" requires extension task support to create a page`);let a=this.pageRegistry.registerExtensionPage(t,void 0,n?.url,!1);try{let o=await this.extensionTaskQueue.queueTask(`browser_new_page`,{browserId:t,pageId:a,url:n?.url,setAsCurrent:i},1e4,t);if(!o.success)throw Error(o.error??`Custom tool "${e}" failed to create a page`);let s=o.result?.content[0]?.type===`text`?o.result.content[0].text:void 0,c={};if(typeof s==`string`&&s.length>0)try{c=JSON.parse(s)}catch{c={}}let l=this.pageRegistry.get(a);if(!l)throw Error(`Page "${a}" was not registered`);l.url=c.url??l.url,l.title=c.title??l.title,l.extensionTabId=c.tabId??l.extensionTabId;let u=await this.waitForResolvedPageMetadata(a);return r.pageIds.add(a),(i||!r.currentPageId)&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a),{...this.toPageSummary(r.currentPageId,u),page:this.resolveToolPage(e,a,u)}}catch(e){throw this.pageRegistry.remove(a),e}}let{pageId:a,page:o}=await this.browserService.newPage(t);n?.url&&(await o.goto(n.url),await this.pageRegistry.updateMetadata(a)),i&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a);let s=this.pageRegistry.get(a);if(!s)throw Error(`Page "${a}" was not registered`);return{...this.toPageSummary(r.currentPageId,s),page:this.resolveToolPage(e,a,s)}}async waitForResolvedPageMetadata(e){let t=Date.now(),n=this.pageRegistry.get(e);for(;n&&Date.now()-t<1500;){let t=typeof n.url==`string`&&n.url.length>0,r=typeof n.title==`string`&&n.title.length>0&&n.title!==`Extension Tab`;if(t&&r)return n;await new Promise(e=>setTimeout(e,50)),n=this.pageRegistry.get(e)}if(!n)throw Error(`Page "${e}" was not registered`);return n}async resolveExecutionContext(t,n){let r=typeof n[R]==`string`&&n[R].length>0?n[R]:void 0,i=typeof n[z]==`string`&&n[z].length>0?n[z]:void 0;if(!r&&!i)throw Error(`Custom tool "${t}" requires a string pageId or browserId`);if(r){let n=this.pageRegistry.get(r);if(!n)throw Error(`Page "${r}" not found`);let a=e.b();if(a&&n.ownerId&&n.ownerId!==a)throw Error(`Custom tool "${t}" cannot use page "${r}" owned by another session`);if(i&&n.browserId!==i)throw Error(`Custom tool "${t}" received pageId "${r}" for browser "${n.browserId}", not "${i}"`);let o=this.createBrowserHelper(t,n.browserId);return{pageId:r,pageEntry:n,page:this.resolveToolPage(t,r,n),browser:o}}let a=this.createBrowserHelper(t,i);try{let e=await a.getCurrentPage(),t=this.pageRegistry.get(e.pageId);if(!t)throw Error(`Page "${e.pageId}" not found`);return{pageId:e.pageId,pageEntry:t,page:e.page,browser:a}}catch(e){if(!(e instanceof Error?e.message:String(e)).includes(`has no pages`))throw e;let t=await a.newPage(),n=this.pageRegistry.get(t.pageId);if(!n)throw Error(`Page "${t.pageId}" not found`,{cause:e});return{pageId:t.pageId,pageEntry:n,page:t.page,browser:a}}}createBrowserHelper(t,n){return{browserId:n,mode:this.browserService?.getBrowser(n)?.mode??`extension`,listPages:async()=>{let e=this.browserService?.getBrowser(n);if(!e)throw Error(`Browser "${n}" not found`);return this.pageRegistry.findByBrowser(n).map(t=>this.toPageSummary(e.currentPageId,t))},getPage:async e=>{let r=this.pageRegistry.get(e);if(!r||r.browserId!==n)throw Error(`Page "${e}" not found in browser "${n}"`);let i=this.browserService?.getBrowser(n);return{...this.toPageSummary(i?.currentPageId??null,r),page:this.resolveToolPage(t,e,r)}},getCurrentPage:async()=>{let r=this.browserService?.getBrowser(n);if(!r)throw Error(`Browser "${n}" not found`);let i=e.b(),a;if(i){let e=this.pageRegistry.findByOwner(i).filter(e=>e.browserId===n);if(a=e[e.length-1]?.id,!a)throw Error(`Browser "${n}" has no pages owned by this session`)}else a=r.currentPageId??this.pageRegistry.findByBrowser(n)[0]?.id??void 0;if(!a)throw Error(`Browser "${n}" has no pages`);let o=this.pageRegistry.get(a);if(!o)throw Error(`Page "${a}" not found`);return{...this.toPageSummary(r.currentPageId,o),page:this.resolveToolPage(t,a,o)}},newPage:async e=>this.createPageForBrowser(t,n,e)}}createToolLogger(e,t,n){let r={"browse_tool.tool.name":e,"browse_tool.page.id":t,"browse_tool.browser.id":n.browserId,"browse_tool.execution.mode":n.mode},i=(e,t,n)=>{this.telemetry.log(e,t,{attributes:Ge({...r,...n?.attributes??{}}),exception:n?.exception})};return{getTraceContext:()=>this.telemetry.getActiveTraceContext(),trace:(e,t)=>i(`trace`,e,t),debug:(e,t)=>i(`debug`,e,t),info:(e,t)=>i(`info`,e,t),warn:(e,t)=>i(`warn`,e,t),error:(e,t)=>i(`error`,e,t),fatal:(e,t)=>i(`fatal`,e,t)}}async listTools(e){return(await this.loadTools(e)).map(({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i})=>({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i}))}async executeTool(e,t,n){let r=typeof n[R]==`string`?n[R]:void 0,a=typeof n[z]==`string`?n[z]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":i.default.resolve(e),"browse_tool.page.id":r,"browse_tool.browser.id":a}},async r=>{let a=(await this.loadTools(e)).find(e=>e.name===t);if(!a)throw r?.setStatus({code:u.SpanStatusCode.ERROR,message:`Custom tool "${t}" not found`}),Error(`Custom tool "${t}" not found`);let{pageId:o,pageEntry:s,page:c,browser:l}=await this.resolveExecutionContext(t,n),d=this.createToolLogger(t,o,s);r?.setAttributes({"browse_tool.browser.id":s.browserId,"browse_tool.execution.mode":s.mode,"browse_tool.page.id":o}),B(`Executing custom tool`,{toolName:t,directory:i.default.resolve(e),pageId:o,browserId:s.browserId,mode:s.mode,input:Ue(n),scriptPath:a.scriptPath});let f;try{f=await a.execute?.({page:c,browser:l,input:n,logger:d})}catch(e){let n=e instanceof Error?e.message:String(e);throw B(`Custom tool execution failed`,{toolName:t,pageId:o,browserId:s.browserId,mode:s.mode,error:n,stack:e instanceof Error?e.stack:void 0}),Error(`Custom tool "${t}" failed on page "${o}": ${n}`,{cause:e})}B(`Custom tool execution completed`,{toolName:t,pageId:o,browserId:s.browserId,mode:s.mode,result:Ue(f)});let p=nt(t,f,a.suggestionActions),m=p.isError?p.content[0]?.text:void 0;return m&&r?.setStatus({code:u.SpanStatusCode.ERROR,message:m}),p})}async loadTools(e){let t=i.default.resolve(e),n=i.default.join(t,`tools.yaml`),r=Xe(await(0,o.readFile)(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),a=Ve.parse(r);return Promise.all(a.tools.map(async e=>{Ze(e);let n=i.default.resolve(t,e.script);await(0,o.stat)(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=$e(n,await Qe(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function it(t){let n=new v.Hono;return n.get(`/browsers`,async n=>{try{let r=t.get(e.O.BrowserService),i=t.get(e.O.PageRegistry),a=r.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString(),pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt.toISOString()}))}}),c={totalBrowsers:a.length,totalPages:o.length};return n.json({browsers:s,stats:c})}catch(e){return console.error(`Failed to list browsers:`,e),n.json({error:`Failed to list browsers`,message:e instanceof Error?e.message:String(e)},500)}}),n.delete(`/browsers`,async n=>{try{return await t.get(e.O.BrowserService).closeAll(),n.json({success:!0,message:`All browsers closed`})}catch(e){return console.error(`Failed to close all browsers:`,e),n.json({error:`Failed to close all browsers`,message:e instanceof Error?e.message:String(e)},500)}}),n.delete(`/browsers/:id`,async n=>{try{let r=n.req.param(`id`),i=t.get(e.O.BrowserService);return i.getBrowser(r)?(await i.closeBrowser(r),n.json({success:!0,message:`Browser "${r}" closed`})):n.json({error:`Browser "${r}" not found`},404)}catch(e){return console.error(`Failed to close browser:`,e),n.json({error:`Failed to close browser`,message:e instanceof Error?e.message:String(e)},500)}}),n.delete(`/pages/:id`,async n=>{try{let r=n.req.param(`id`),i=t.get(e.O.PageRegistry).get(r);return i?i.page?(await i.page.close(),n.json({success:!0,message:`Page "${r}" closed`})):n.json({error:`Page "${r}" is in extension mode and cannot be closed via API`},400):n.json({error:`Page "${r}" not found`},404)}catch(e){return console.error(`Failed to close page:`,e),n.json({error:`Failed to close page`,message:e instanceof Error?e.message:String(e)},500)}}),n}function at(e){return e.toLocaleString(`en-US`,{year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1})}function ot(e){let t=Math.floor(e/1e3),n=Math.floor(t/60),r=Math.floor(n/60);return r>0?`${r}h ${n%60}m`:n>0?`${n}m ${t%60}s`:`${t}s`}function st(e){return ot(new Date().getTime()-e.getTime())}const ct=`table-container`,lt=`stat-card`,ut=`browser-row`,dt=`page-row`,W=`status-active`,ft=`url-cell`,G=`timestamp-cell`,K=`actions-cell`,q=`kill-btn`,pt=`empty-state`;function mt({browsers:e}){return e.length===0?(0,C.jsx)(`div`,{class:ct,children:(0,C.jsxs)(`div`,{class:pt,children:[(0,C.jsx)(`h3`,{children:`No Active Browsers`}),(0,C.jsx)(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):(0,C.jsx)(`div`,{class:ct,children:(0,C.jsxs)(`table`,{class:`browser-table`,children:[(0,C.jsx)(`thead`,{children:(0,C.jsxs)(`tr`,{children:[(0,C.jsx)(`th`,{children:`ID`}),(0,C.jsx)(`th`,{children:`Status`}),(0,C.jsx)(`th`,{children:`URL / Title`}),(0,C.jsx)(`th`,{children:`Created`}),(0,C.jsx)(`th`,{children:`Age`}),(0,C.jsx)(`th`,{children:`Actions`})]})}),(0,C.jsx)(`tbody`,{id:`browser-table-body`,children:e.map(e=>(0,C.jsxs)(S.Fragment,{children:[(0,C.jsxs)(`tr`,{class:ut,children:[(0,C.jsx)(`td`,{children:e.id}),(0,C.jsx)(`td`,{children:(0,C.jsxs)(`span`,{class:W,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),(0,C.jsx)(`td`,{children:e.profileName||`Default Profile`}),(0,C.jsx)(`td`,{class:G,children:at(e.createdAt)}),(0,C.jsx)(`td`,{class:G,children:st(e.createdAt)}),(0,C.jsx)(`td`,{class:K,children:(0,C.jsx)(`button`,{type:`button`,class:q,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>(0,C.jsxs)(`tr`,{class:dt,children:[(0,C.jsxs)(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),(0,C.jsx)(`td`,{children:(0,C.jsx)(`span`,{class:W,children:`Open`})}),(0,C.jsx)(`td`,{class:ft,title:t.url,children:t.title||t.url||`about:blank`}),(0,C.jsx)(`td`,{class:G,children:at(t.createdAt)}),(0,C.jsx)(`td`,{class:G,children:st(t.createdAt)}),(0,C.jsx)(`td`,{class:K,children:(0,C.jsx)(`button`,{type:`button`,class:q,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]},t.id))]},e.id))})]})})}function ht({stats:e}){return(0,C.jsxs)(`div`,{class:`stats-container`,children:[(0,C.jsxs)(`div`,{class:lt,children:[(0,C.jsx)(`h3`,{children:`Active Browsers`}),(0,C.jsx)(`div`,{class:`value`,children:e.totalBrowsers})]}),(0,C.jsxs)(`div`,{class:lt,children:[(0,C.jsx)(`h3`,{children:`Active Pages`}),(0,C.jsx)(`div`,{class:`value`,children:e.totalPages})]})]})}function gt({browsers:e,stats:t}){return(0,C.jsxs)(`div`,{class:`dashboard-container`,children:[(0,C.jsxs)(`div`,{class:`dashboard-header`,children:[(0,C.jsxs)(`div`,{children:[(0,C.jsx)(`h1`,{children:`Playwright MCP Dashboard`}),(0,C.jsx)(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),(0,C.jsxs)(`div`,{children:[(0,C.jsx)(`button`,{type:`button`,id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),(0,C.jsx)(`button`,{type:`button`,id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),(0,C.jsx)(ht,{stats:t}),(0,C.jsx)(mt,{browsers:e}),(0,C.jsx)(`script`,{children:(0,x.raw)(`
|
|
@@ -428,7 +428,7 @@ window.addEventListener('beforeunload', () => {
|
|
|
428
428
|
margin-bottom: 0.5rem;
|
|
429
429
|
color: #999;
|
|
430
430
|
}
|
|
431
|
-
`)})]}),(0,C.jsx)(`body`,{children:t})]})}function vt(t){let n=new v.Hono;return n.get(`/`,async n=>{try{let r=t.get(e.O.BrowserService),i=t.get(e.O.PageRegistry),a=r.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt,pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt}))}}),c={totalBrowsers:a.length,totalPages:o.length};return n.html((0,C.jsx)(_t,{title:`Playwright MCP Dashboard`,children:(0,C.jsx)(gt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html((0,C.jsx)(_t,{title:`Playwright MCP Dashboard - Error`,children:(0,C.jsxs)(`div`,{style:`padding: 2rem; text-align: center;`,children:[(0,C.jsx)(`h1`,{children:`Failed to load dashboard`}),(0,C.jsx)(`p`,{children:e instanceof Error?e.message:String(e)}),(0,C.jsx)(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function yt(e){if(!e.isError)return;let t=e.content[0];return t?.type===`text`&&typeof t.text==`string`?t.text:`Unknown tool execution error`}function bt(t){try{return t.get(e.O.TelemetryService)}catch{return new e.g}}function xt(e){return e.startsWith(`browser-`)||e===`run-spec`||e===`discover-specs`?e.replaceAll(`-`,`_`):e}function St(t){let n=new v.Hono,r=t.get(e.O.PageRegistry),i=t.get(e.O.BrowserService),a=t.get(e.O.ExtensionTaskQueue),o=bt(t),s=new rt(r,a,o,i);n.use(`*`,(0,y.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`DELETE`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`,`Authorization`]})),n.get(`/health`,e=>{try{let t=i.listBrowsers(),n={status:`healthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,timestamp:new Date().toISOString(),browsers:{count:t.length,instances:t.map(e=>({id:e.id,pageCount:e.pageIds.size,createdAt:e.createdAt.toISOString()}))}};return e.json(n)}catch(t){return e.json({status:`unhealthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,error:t instanceof Error?t.message:String(t)},503)}}),n.post(`/execute`,async n=>{try{let i=await n.req.json();if(!i.tool)return n.json({success:!1,error:`Missing "tool" field in request body`},400);let a=typeof i.arguments?.pageId==`string`?i.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.execute`,{attributes:{"http.method":`POST`,"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":a}},async a=>{let s=t.getAll(e.O.Tool),c=xt(i.tool),d=s.find(e=>e.getDefinition().name===c);if(process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`&&i.tool===`browser_launch`){let e=typeof i.arguments?.videoDir==`string`?i.arguments.videoDir:typeof i.arguments?.video_dir==`string`?i.arguments.video_dir:``;console.log(`[ExtensionRecordingDebug] http execute browser_launch mode=${String(i.arguments?.mode??``)} videoDir=${e}`)}if(!d)return a?.setStatus({code:u.SpanStatusCode.ERROR,message:`Tool "${i.tool}" not found`}),o.log(`warn`,`browse-tool HTTP execute rejected unknown tool`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool}}),n.json({success:!1,error:`Tool "${i.tool}" not found`},404);let p=i.arguments||{},m;try{m=d.getInputSchema().parse((0,f.coerceArgs)(p,d.getInputSchema()))}catch(e){if(e instanceof l.z.ZodError){let t=(0,f.formatZodError)(e,{schemaName:c,schema:d.getInputSchema()});return a?.setStatus({code:u.SpanStatusCode.ERROR,message:t}),n.json({success:!1,result:{content:[{type:`text`,text:t}],isError:!0},error:t})}throw e}let h=await e.x(n.req.header(e.y),()=>d.execute(m)),g=m,_=typeof g.pageId==`string`?g.pageId:void 0;if(_){let n=t.get(e.O.BrowserService),i=r.get(_);i&&n.recordBrowserActivity(i.browserId,_)}let v=yt(h);return v?(a?.setStatus({code:u.SpanStatusCode.ERROR,message:v}),o.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":c,"browse_tool.page.id":_}})):o.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":c,"browse_tool.page.id":_}}),n.json({success:!h.isError,result:h,error:v})})}catch(e){return o.log(`error`,`browse-tool HTTP execute request crashed`,{attributes:{"http.route":`/execute`},exception:e}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/browsers`,n=>{try{let r=t.get(e.O.BrowserService).listBrowsers();return n.json({browsers:r.map(e=>({id:e.id,profileName:e.profileName,pageIds:Array.from(e.pageIds),currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString()}))})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/tools`,n=>{try{let r=t.getAll(e.O.Tool);return n.json({tools:r.map(e=>e.getDefinition())})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/custom-tools`,async e=>{let t=e.req.query(`dir`);if(!t)return e.json({tools:[],error:`Missing "dir" query parameter`},400);try{let n=await s.listTools(t);return e.json({tools:n})}catch(t){return e.json({tools:[],error:t instanceof Error?t.message:String(t)},400)}}),n.post(`/custom-tools`,async n=>{let i=n.req.query(`dir`);if(!i)return n.json({success:!1,error:`Missing "dir" query parameter`},400);try{let a=await n.req.json();if(!a.tool)return n.json({success:!1,error:`Missing "tool" field in request body`},400);let c=typeof a.arguments?.pageId==`string`?a.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.custom_tool.execute`,{attributes:{"http.method":`POST`,"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c,"browse_tool.custom_tools.directory":i}},async l=>{let d=await e.x(n.req.header(e.y),()=>s.executeTool(i,a.tool,a.arguments||{}));if(c){let n=t.get(e.O.BrowserService),i=r.get(c);i&&n.recordBrowserActivity(i.browserId,c)}let f=yt(d);return f?(l?.setStatus({code:u.SpanStatusCode.ERROR,message:f}),o.log(`error`,`browse-tool custom tool execution failed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}})):o.log(`info`,`browse-tool custom tool execution succeeded`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}}),n.json({success:!d.isError,result:d,error:f})})}catch(e){return o.log(`error`,`browse-tool custom tool request crashed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.custom_tools.directory":i},exception:e}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}});let c=D(t);n.route(`/extension`,c);let d=it(t);n.route(`/api`,d);let p=vt(t);return n.route(`/`,p),n}function Ct(t,n,r){t.get(`/ws/extension/:browserId`,r(t=>{let r=t.req.param(`browserId`),i=null;return{onOpen(t,a){if(!r){a.close(1008,`Missing browserId`);return}try{i=n.get(e.O.WebSocketHub).addConnection(a,r)}catch{a.close(1011,`Internal server error`)}},onMessage(t){if(i)try{let r=n.get(e.O.WebSocketHub),a=typeof t.data==`string`?t.data:t.data.toString();r.handleMessage(i,a)}catch{}},onClose(){if(i)try{n.get(e.O.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{n.get(e.O.WebSocketHub).removeConnection(i)}catch{}}}})),t.get(`/ws/stats`,t=>{try{let r=n.get(e.O.WebSocketHub).getStats();return t.json({totalConnections:r.totalConnections,browserCount:r.browserCount,connections:r.connections.map(e=>({id:e.id,browserId:e.browserId,connectedAt:e.connectedAt.toISOString()}))})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}})}const wt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function Tt(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of wt)if((0,a.existsSync)(i.default.join(t,e)))return t;let e=i.default.dirname(t);if(e===t)return process.cwd();t=e}}const Et=new g.Command(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(e.d)).option(`--host <host>`,`Host to bind`,e.p()).option(`--headless`,`Run browsers in headless mode by default`,!0).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before closing browsers`,`30`).option(`--registry-dir <path>`,`Custom registry path or directory for service discovery`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking (deprecated)`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--snippets-dir <path>`,`Directory used by browser_run_code to save and load reusable snippets`).option(`--proxy-config-dir <path>`,`Custom proxy config directory`).action(async function(r){let a=N(`httpServe`),o={port:P(this,`port`,r.port,a.port===void 0?void 0:String(a.port),process.env.PLAYWRIGHT_PORT),headless:P(this,`headless`,r.headless,a.headless),idleTimeout:P(this,`idleTimeout`,r.idleTimeout,a.idleTimeout===void 0?void 0:String(a.idleTimeout),process.env[e.u]),host:P(this,`host`,r.host,a.host,process.env.PLAYWRIGHT_HOST),registryDir:P(this,`registryDir`,r.registryDir,a.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:P(this,`registryPath`,r.registryPath,a.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:P(this,`pidsDir`,r.pidsDir,a.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:P(this,`profilesDir`,r.profilesDir,a.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:P(this,`snippetsDir`,r.snippetsDir,a.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR),proxyConfigDir:P(this,`proxyConfigDir`,r.proxyConfigDir,a.proxyConfigDir,process.env[e.s])},s;try{let r=Number.parseInt(o.port,10),a=`browse-tool-http`,c=process.env.NODE_ENV||`development`,l=Tt(process.cwd()),u=o.registryPath||o.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;u&&(process.env.PLAYWRIGHT_REGISTRY_DIR=u,process.env.PORT_REGISTRY_PATH=u,process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(u,`processes.json`)),process.env.PLAYWRIGHT_HOST=o.host,process.env.PLAYWRIGHT_PORT=o.port,process.env[e.u]=o.idleTimeout,o.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=o.pidsDir),o.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=o.profilesDir),o.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=i.default.resolve(o.snippetsDir)),o.proxyConfigDir&&(process.env[e.s]=i.default.resolve(o.proxyConfigDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${r}`),console.log(` Host: ${o.host}`),console.log(` Headless: ${o.headless}`),console.log(` Idle Timeout: ${o.idleTimeout} minutes`),process.env.BROWSE_TOOL_SNIPPETS_DIR&&console.log(` Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`),process.env.BROWSE_TOOL_PROXY_CONFIG_DIR&&console.log(` Proxy Config Dir: ${process.env[e.s]}`);let d=e.a(),f=d.get(e.O.ProcessRegistryService),p=St(d),{injectWebSocket:m,upgradeWebSocket:h}=(0,b.createNodeWebSocket)({app:p});Ct(p,d,h);let g=d.get(e.O.WebSocketHub),v=d.get(e.O.ExtensionTaskQueue),y=d.get(e.O.BrowserService),x=d.get(e.O.ExtensionSessionRegistry),S=d.get(e.O.PageRegistry),C=d.get(e.O.IdleCleanupService);C.start(),g.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=y.getBrowser(n),i,a;if(r&&(r.mode===`extension`||r.mode===`vm`)){i=r.id;let t=S.findByBrowser(i);a=r.currentPageId||t[0]?.id||``,g.reassignConnection(e.id,i),a||(a=S.registerExtensionPage(i),r.pageIds.add(a),r.currentPageId=a),console.log(`[WebSocket] Extension connected to existing browser: ${i}, pageId: ${a}`)}else{i=n,y.registerExtensionBrowserWithId(i),a=S.registerExtensionPage(i);let e=y.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=S.get(a);o&&(o.extensionTabId=t.payload.tabId,o.url=t.payload.url??o.url);let s=x.register({browserId:i,tabId:t.payload.tabId,url:t.payload.url,metadata:{transport:`websocket`}});g.sendSessionAck(e,t.id??``,s.id,s.controlMode),g.broadcastPageCreated(i,a,S.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=v.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&y.recordBrowserActivity(e.browserId,e.pageId)}},onTabMapped:(e,t)=>{if(t.type===`tab:mapped`){let e=S.get(t.payload.pageId);if(!e)return;e.extensionTabId=t.payload.tabId,y.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&x.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let w=new t.PortRegistryService(process.env.PORT_REGISTRY_PATH);console.log(u?` Registry path: ${u}`:` Registry path: default (~/.port-registry/ports.json)`);let T=await w.reservePort({repositoryPath:l,serviceName:a,serviceType:`tool`,environment:c,preferredPort:r,pid:process.pid,host:o.host,force:!0,portRange:t.DEFAULT_PORT_RANGE,metadata:{headless:o.headless,idleTimeout:o.idleTimeout}});if(!T.success||T.port===void 0)throw Error(T.error||`Failed to reserve port in range ${t.DEFAULT_PORT_RANGE.min}-${t.DEFAULT_PORT_RANGE.max}`);let E=T.port;T.record.metadata={...T.record.metadata,healthCheckUrl:`${e.f(o.host,E)}/health`};let D=(0,_.serve)({fetch:p.fetch,port:E,hostname:o.host});m(D),s=await(0,n.createProcessLease)({repositoryPath:l,serviceName:a,serviceType:`service`,environment:c,pid:process.pid,port:E,host:o.host,metadata:{healthCheckUrl:`${e.f(o.host,E)}/health`,headless:o.headless,idleTimeout:o.idleTimeout,transport:`http`}},f);let O=e.f(o.host,E);console.log(`HTTP server listening on ${O}`),console.log(` Health check: ${O}/health`),console.log(` Execute tool: POST ${O}/execute`),console.log(`
|
|
432
|
-
Press Ctrl+C to stop the server`);let k=async t=>{console.log(`\n\n${t} received. Shutting down gracefully...`);try{C.stop(),D.close(),console.log(`Server closed`);try{await d.get(e.O.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{s&&await s.release({kill:!1}),console.log(`Service deregistered`)}catch(e){console.error(`Process registry cleanup error:`,e)}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>k(`SIGINT`)),process.on(`SIGTERM`,()=>k(`SIGTERM`)),await new Promise(()=>{})}catch(e){if(s)try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Dt={browser_click:[`input`],browser_fill:[`input`],browser_type:[`input`],browser_select:[`input`],browser_hover:[`input`],browser_drag:[`input`],browser_press_key:[`input`],browser_upload_file:[`input`],browser_navigate:[`navigation`],browser_go_back:[`navigation`],browser_go_forward:[`navigation`],browser_reload:[`navigation`],browser_wait_for:[`navigation`],browser_snapshot:[`snapshot`],browser_screenshot:[`snapshot`],browser_pdf:[`snapshot`],browser_list_pages:[`page`],browser_new_page:[`page`],browser_select_page:[`page`],browser_close_page:[`page`],browser_resize_page:[`page`,`emulation`],browser_handle_dialog:[`dialog`],browser_list_network_requests:[`network`],browser_get_network_request:[`network`],browser_list_console_messages:[`console`],browser_evaluate_script:[`script`],browser_emulate:[`emulation`],browser_expect:[`testing`],browser_start_trace:[`tracing`],browser_stop_trace:[`tracing`],browser_list_profiles:[`profile`],browser_delete_profile:[`profile`],run_spec:[`spec`,`testing`],discover_specs:[`spec`,`testing`],browser_launch:[`browser`],browser_close:[`browser`],browser_run_code:[`code`,`script`]};function Ot(e){let t=new Set;for(let[n,r]of Object.entries(Dt))r.some(t=>e.includes(t))&&t.add(n);return t}const kt=3e3,At=[`browser_launch`],jt=[`browser_new_page`],Mt=[`browser_close`],Nt=[`browser_close_page`];function Pt(e,t){let n=e.replaceAll(`-`,`_`);return t&&(n===`browser_list`||n===`browser_list_session`||n===`browser_list_sessions`)?`browser_list_session`:e.startsWith(`browser-`)||e===`run-spec`||e===`discover-specs`?n:e}function J(e){if(!e?.content?.[0]?.text)return null;try{let t=JSON.parse(e.content[0].text);return{browserId:t.browserId,pageId:t.pageId,mode:t.mode,url:t.url,closedBrowserId:t.closedBrowserId,closedPageId:t.closedPageId,browserKeptOpen:t.browserKeptOpen}}catch{return null}}async function Ft(e){await new Promise(t=>setTimeout(t,e))}async function It(e){let t=new AbortController,n=setTimeout(()=>t.abort(),kt);try{let n=await fetch(`${e}/tools`,{signal:t.signal});if(!n.ok)throw Error(`HTTP error ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);if(!Array.isArray(r.tools))throw Error(`Invalid /tools response: missing tools array`);if(r.tools.length===0)throw Error(`HTTP server returned an empty tool list`);return r.tools}finally{clearTimeout(n)}}function Lt(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function Rt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),kt);try{let r=await fetch(Lt(e,t),{signal:n.signal});if(!r.ok)throw Error(`HTTP error ${r.status}: ${r.statusText}`);let i=await r.json();if(i.error)throw Error(i.error);if(!Array.isArray(i.tools))throw Error(`Invalid /custom-tools response: missing tools array`);return i.tools}finally{clearTimeout(r)}}async function zt(e){let t;for(let n=1;n<=4;n+=1)try{return await It(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await Ft(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function Bt(e,t){let n;for(let r=1;r<=4;r+=1)try{return await Rt(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await Ft(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function Vt(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?Ot(e.tags):null,n=new Set(e.exclude??[]);if(!t)return n.size>0?n:null;for(let e of n)t.delete(e);return t}function Ht(e){let t=new Map,n=(e,n)=>{let r=t.get(e)??new Set;r.add(n),t.set(e,r)};return e.defaultMode&&n(`browser_launch`,`mode`),e.enforcedProfileName&&n(`browser_launch`,`profileName`),t}function Ut(e,t){let n=t.get(e.name);if(!n?.size)return e;let r=e.inputSchema;if(!r.properties||typeof r.properties!=`object`)return e;let i=Object.fromEntries(Object.entries(r.properties).filter(([e])=>!n.has(e))),a={...r,properties:i};return Array.isArray(r.required)&&(a.required=r.required.filter(e=>typeof e!=`string`||!n.has(e))),{...e,inputSchema:a}}function Wt(t){let{httpBaseUrl:n,sessionTracker:r,defaultMode:i,toolFilter:a,customToolsDir:o,enforcedProfileName:s,proxyConfigDir:c,ownerId:l}=t,u=a?.tags?.length?Vt(a):null,d=new Set(a?.exclude??[]),f=u!==null||d.size>0,h=Ht({defaultMode:i,enforcedProfileName:s}),g=new p.Server({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),_={name:`browser_list_session`,description:`List all browsers and pages created in this MCP session. Use this to see what resources you have available.`,inputSchema:{type:`object`,properties:{},required:[],additionalProperties:!1}},v=[],y=[],b=new Set;function x(e){return f?e.filter(e=>d.has(e.name)?!1:u===null?!0:u.has(e.name)):e}function S(e){return e.filter(e=>!d.has(e.name))}function C(e){return s?{...e,profileName:s}:e}function w(t){let n=e.c({proxy:t.proxy,profileName:typeof t.profileName==`string`?t.profileName:void 0,proxyConfigDir:c});if(!n){let e={...t};return delete e.proxy,e}return{...t,proxy:n}}function T(e){if(!s)return e;let t=e?.content?.[0]?.text;if(!t)return e;try{let n=JSON.parse(t),r=(Array.isArray(n.profiles)?n.profiles:[]).filter(e=>e?.name===s);return{...e,content:[{...e.content[0],text:JSON.stringify({...n,profileCount:r.length,profiles:r},null,2)},...e.content.slice(1)]}}catch{return e}}function E(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function D(e){return d.has(e)?!1:u===null?!0:u.has(e)}return g.setRequestHandler(m.ListToolsRequestSchema,async()=>{try{let e=await zt(n);v=e,b=new Set(e.map(e=>e.name));let t=o?await Bt(n,o):[];y=t;let i=[...x(e).map(e=>Ut(e,h)),...S(t).map(e=>E(e))];return r&&i.push(_),{tools:i}}catch(e){if(v.length>0||y.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[...x(v).map(e=>Ut(e,h)),...S(y).map(e=>E(e))];return r&&t.push(_),{tools:t}}let t=e instanceof Error?e.message:String(e);throw Error(`Failed to list tools from HTTP server at ${n}: ${t}`,{cause:e})}}),g.setRequestHandler(m.CallToolRequestSchema,async t=>{let{name:a,arguments:s}=t.params,c=Pt(a,!!r),u=new Set(y.map(e=>e.name));if(c!==`browser_list_session`&&!D(c))return{content:[{type:`text`,text:`Tool "${c}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(c===`browser_list_session`&&r){let e=r.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let d=s||{};c===`browser_launch`&&i&&d.mode===void 0&&(d={...d,mode:i}),c===`browser_launch`&&(d=C(d),d=w(d));try{if(o&&!u.has(c)&&!b.has(c))try{y=await Bt(n,o),u=new Set(y.map(e=>e.name))}catch(e){console.error(`Failed to refresh custom tools, using cached list: ${e instanceof Error?e.message:String(e)}`)}let t=u.has(c),i=await fetch(t&&o?Lt(n,o):`${n}/execute`,{method:`POST`,headers:l?{"Content-Type":`application/json`,[e.y]:l}:{"Content-Type":`application/json`},body:JSON.stringify({tool:c,arguments:d})});if(!i.ok){let e=await i.text();return{content:[{type:`text`,text:`HTTP error ${i.status}: ${e}`}],isError:!0}}let a=await i.json();if(!a.success)return{content:[{type:`text`,text:a.error||a.result?.content?.[0]?.text||`Unknown error from HTTP server`}],isError:!0};if(r&&a.result){if(At.includes(c)){let e=J(a.result);e?.browserId&&(r.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&r.trackPage(e.pageId,e.browserId,e.url))}else if(jt.includes(c)){let e=J(a.result);e?.pageId&&e?.browserId&&r.trackPage(e.pageId,e.browserId,e.url)}else if(Mt.includes(c)){let e=J(a.result);e?.closedBrowserId&&r.untrackBrowser(e.closedBrowserId)}else if(Nt.includes(c)){let e=J(a.result);e?.closedPageId&&r.untrackPage(e.closedPageId),e?.browserId&&e.browserKeptOpen===!1&&r.untrackBrowser(e.browserId)}}return c===`browser_list_profiles`&&a.result?T(a.result):a.result||{content:[{type:`text`,text:`No result returned`}]}}catch(e){return{content:[{type:`text`,text:`Failed to execute tool via HTTP server: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),g}const Y=`stdio`,Gt=`http`,X=`streamable-http`,Kt=new Set([Y,Gt,X]),qt=`chromium`,Jt=new Set([qt,`firefox`,`webkit`]),Yt=new Set([`playwright`,`extension`,`vm`,`stealth`]),Z=`SIGINT`,Q=`SIGTERM`,Xt=`PLAYWRIGHT_PORT`,Zt=[`pnpm-workspace.yaml`,`nx.json`,`.git`],Qt=`browse-tool-mcp-http`,$t=`service`,en=t.DEFAULT_PORT_RANGE.min;var tn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...Kt].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},nn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...Jt].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},rn=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...Yt].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function an(e){let t=e.toLowerCase();if(!Jt.has(t))throw new nn(e);return t}function on(e){let t=e.toLowerCase();if(!Yt.has(t))throw new rn(e);return t}function sn(e){let t=typeof e==`number`?e:Number.parseInt(e,10);if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}function cn(e){return e!=null&&String(e).trim()!==``}function ln(e){let t=e.toLowerCase();return t===Gt?X:t}function un(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function dn(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of Zt)if((0,a.existsSync)(i.default.join(t,e)))return t;let e=i.default.dirname(t);if(e===t)return process.cwd();t=e}}function fn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function pn(e){let t=e.tags?fn(e.tags):void 0,n=e.exclude?fn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var mn=class extends Error{code=`BROWSER_CLEANUP_FAILED`;recovery=`Browsers may still be running; check the HTTP server or kill processes manually`;failedBrowserIds;httpBaseUrl;constructor(e,t,n){super(`Failed to close ${e.length} browser(s): ${e.join(`, `)}`,n),this.name=`BrowserCleanupError`,this.failedBrowserIds=e,this.httpBaseUrl=t}},hn=class extends Error{code=`SESSION_SHUTDOWN_FAILED`;recovery=`Check logs for cleanup and transport errors; processes may need manual cleanup`;signal;constructor(e,t){super(`Shutdown failed for signal ${e}`,t),this.name=`SessionShutdownError`,this.signal=e}},gn=class extends Error{code=`HTTP_SERVER_START_FAILED`;recovery=`Check if the HTTP server binary exists and the port is available`;constructor(e){super(`Failed to start HTTP server`,e),this.name=`HttpServerStartError`}},_n=class extends Error{code=`MCP_SERVER_BOOTSTRAP_FAILED`;recovery=`Check if all dependencies are installed and try again`;constructor(e){super(`Failed to start MCP server`,e),this.name=`McpServerBootstrapError`}};async function vn(t,n,r){let i=t.getBrowserIds(),a=[],o;for(let t of i)try{let i=await fetch(`${n}/execute`,{method:`POST`,headers:r?{"Content-Type":`application/json`,[e.y]:r}:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:t}})});if(!i.ok){let e=await i.text().catch(()=>``);throw Error(`HTTP ${i.status} from ${n}/execute: ${e}`)}console.error(` Closed browser: ${t}`)}catch(e){let n=e instanceof Error?e:Error(String(e));console.error(` Failed to close browser: ${t}`,n),a.push(t),o??=n}if(a.length>0)throw new mn(a,n,{cause:o});t.clear()}async function yn(e,t,n,r,i){await e.start();let a=!1,o=async o=>{if(a)return;a=!0,console.error(`\nReceived ${o}, shutting down gracefully...`);let s;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await vn(t,n,r))}catch(e){s=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,s)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),s??=t}try{await i?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),s??=t}if(s){let e=new hn(o,{cause:s});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(Z,()=>o(Z)),process.once(Q,()=>o(Q))}const bn=new g.Command(`mcp-serve`).description(`Start Playwright MCP server for browser automation`).option(`-t, --type <type>`,`Transport type: ${[Y,X].join(`, `)}`,Y).option(`-b, --browser <browser>`,`Default browser type: ${[...Jt].join(`, `)}`,qt).option(`--headless`,`Run browsers in headless mode by default`,!1).option(`--no-headless`,`Run browsers in headed mode (visible window)`).option(`--host <host>`,`Default host for HTTP services`,e.p()).option(`--port <port>`,`Port for streamable HTTP transport`,String(en)).option(`--http-port <port>`,`Port for the inner browse-tool HTTP service`).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...Yt].join(`, `)}`).option(`--tags <tags>`,`Comma-separated tags to filter tools (e.g. input,navigation,snapshot)`).option(`--exclude <tools>`,`Comma-separated tool names to exclude (applied after --tags)`).option(`--custom-tools <path>`,`Path to a folder containing tools.yaml and custom tool scripts`).option(`--chrome-for-testing-path <path>`,`Path to a Chrome for Testing executable for the inner HTTP server`).option(`--snippets-dir <path>`,`Path to a folder where browser_run_code snippets are stored`).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before auto-closing browsers`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--registry-dir <path>`,`Custom registry directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--proxy-config-dir <path>`,`Custom proxy config directory`).action(async function(t){let r=N(`mcpServe`),a={type:P(this,`type`,t.type,r.type),browser:P(this,`browser`,t.browser,r.browser),headless:P(this,`headless`,t.headless,r.headless),profile:P(this,`profile`,t.profile,r.profile),mode:P(this,`mode`,t.mode,r.mode),host:P(this,`host`,t.host,r.host,process.env.PLAYWRIGHT_HOST),port:P(this,`port`,t.port,r.port,process.env.PLAYWRIGHT_MCP_PORT),httpPort:P(this,`httpPort`,t.httpPort,r.httpPort,process.env[Xt]),tags:P(this,`tags`,t.tags,r.tags),exclude:P(this,`exclude`,t.exclude,r.exclude),customTools:P(this,`customTools`,t.customTools,r.customTools),chromeForTestingPath:P(this,`chromeForTestingPath`,t.chromeForTestingPath,r.chromeForTestingPath,process.env[e.w]),snippetsDir:P(this,`snippetsDir`,t.snippetsDir,r.snippetsDir),idleTimeout:P(this,`idleTimeout`,t.idleTimeout,r.idleTimeout===void 0?void 0:String(r.idleTimeout),process.env[e.u]),registryPath:P(this,`registryPath`,t.registryPath,r.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:P(this,`registryDir`,t.registryDir,r.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:P(this,`pidsDir`,t.pidsDir,r.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:P(this,`profilesDir`,t.profilesDir,r.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),proxyConfigDir:P(this,`proxyConfigDir`,t.proxyConfigDir,r.proxyConfigDir,process.env[e.s])},o=ln(a.type),s=dn(process.cwd()),c=process.env.NODE_ENV||`development`,l,u;try{if(!Kt.has(o))throw new tn(o);let t=an(a.browser),r=a.mode?on(a.mode):void 0,f=sn(a.port??en),p=cn(a.httpPort),m=p?sn(a.httpPort):void 0,h=pn(a),g=a.customTools?i.default.resolve(a.customTools):void 0,_=a.chromeForTestingPath?i.default.resolve(a.chromeForTestingPath):void 0,v=a.snippetsDir?i.default.resolve(a.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${o}`),o===X&&console.error(` MCP endpoint: http://${a.host??e.p()}:${f}/mcp`),console.error(` Default browser: ${t}`),console.error(` Headless: ${a.headless}`),r&&console.error(` Default mode: ${r}`),a.profile&&console.error(` Profile: ${a.profile}`),h?.tags?.length&&console.error(` Tags: ${h.tags.join(`, `)}`),h?.exclude?.length&&console.error(` Exclude: ${h.exclude.join(`, `)}`),g&&console.error(` Custom tools: ${g}`),_&&console.error(` Chrome for Testing path: ${_}`),v&&console.error(` Snippets dir: ${v}`),a.idleTimeout&&console.error(` Idle timeout: ${a.idleTimeout} minutes`);let y=a.registryPath||a.registryDir;y&&(process.env.PLAYWRIGHT_REGISTRY_DIR=y,process.env.PORT_REGISTRY_PATH=y,process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(y,`processes.json`),console.error(` Registry path: ${y}`)),a.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=a.pidsDir,console.error(` PIDs dir: ${a.pidsDir}`)),a.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=a.profilesDir,console.error(` Profiles dir: ${a.profilesDir}`)),a.proxyConfigDir&&(process.env[e.s]=i.default.resolve(a.proxyConfigDir),console.error(` Proxy config dir: ${process.env[e.s]}`)),v&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=v),_&&(process.env[e.w]=_),a.idleTimeout&&(process.env[e.u]=a.idleTimeout),a.host&&(process.env.PLAYWRIGHT_HOST=a.host);let b=e.o(),x=b.get(e.O.ProcessRegistryService);u=await b.get(e.O.McpPortAllocationService).acquirePorts({repositoryPath:s,environment:c,host:a.host??e.p(),pid:process.pid,preferredMcpPort:o===X?f:void 0,preferredHttpPort:m,exactHttpPort:p,reserveMcpPort:o===X,mcpServiceName:Qt,mcpServiceType:`tool`,mcpMetadata:o===X?{healthCheckUrl:`${e.f(a.host??e.p(),f)}/health`,transport:X}:void 0});let S=u.httpPort;process.env[Xt]=S.toString();let C=await b.get(e.O.HttpServerManager).ensureRunning(S,{exactPort:!0});if(!C.running)throw new gn;let w=e.f(e.p(),C.port),T=C.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${T} on port ${C.port}`),o===Y){let i=new e.l,o=(0,d.randomUUID)(),f=new e.n(Wt({httpBaseUrl:w,sessionTracker:i,defaultMode:r,toolFilter:h,customToolsDir:g,enforcedProfileName:a.profile,proxyConfigDir:process.env[e.s],ownerId:o}));l=await(0,n.createProcessLease)({repositoryPath:s,serviceName:`browse-tool-mcp`,serviceType:$t,environment:c,pid:process.pid,metadata:{transport:Y,browser:t,mode:r}},x),await yn(f,i,w,o,async()=>{await l?.release({kill:!1}),await u?.release()});return}let E=u.mcpPort;if(!E)throw Error(`Failed to reserve ${X} port ${f}`);l=await(0,n.createProcessLease)({repositoryPath:s,serviceName:Qt,serviceType:$t,environment:c,pid:process.pid,port:E,host:a.host??e.p(),metadata:{healthCheckUrl:`${e.f(a.host??e.p(),E)}/health`,transport:X}},x);let D=new e.t(({headers:t})=>{let n=un(t,`x-profile`)??a.profile,i=new e.l,o=(0,d.randomUUID)();return{server:Wt({httpBaseUrl:w,sessionTracker:i,defaultMode:r,toolFilter:h,customToolsDir:g,enforcedProfileName:n,proxyConfigDir:process.env[e.s],ownerId:o}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await vn(i,w,o)}}},{host:a.host??e.p(),port:E});try{await D.start()}catch(e){try{await l.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}throw await u.release(),u=void 0,e}let O=!1,k=async e=>{if(!O){O=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await D.stop();try{await l?.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}await u?.release(),u=void 0,process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(Z,()=>k(Z)),process.once(Q,()=>k(Q))}catch(e){if(l)try{await l.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}if(u)try{await u.release()}catch(e){console.error(`Port allocation cleanup error:`,e)}(e instanceof tn||e instanceof nn||e instanceof rn||e instanceof gn)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new _n({cause:e instanceof Error?e:Error(String(e))});console.error(`Error [${t.code}]: ${t.message}`,t.cause),console.error(`Recovery: ${t.recovery}`),process.exit(1)}}),xn=new g.Command(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=N(`httpServe`),n=process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH??t.registryPath??t.registryDir;n&&(process.env.PLAYWRIGHT_REGISTRY_DIR=n,process.env.PLAYWRIGHT_REGISTRY_PATH=n,process.env.PORT_REGISTRY_PATH=n),!process.env.PLAYWRIGHT_PIDS_DIR&&t.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=t.pidsDir),console.log(`browse-tool Status
|
|
433
|
-
`),console.log(`-`.repeat(50));let r=await e.i().get(e.O.HttpServerManager).getStatus();if(r.running){let n=process.env.PLAYWRIGHT_HOST??t.host??e.p();console.log(`HTTP Server: Running`),console.log(` PID: ${r.pid}`),console.log(` Port: ${r.port}`),console.log(` Health: ${e.f(n,r.port)}/health`),r.browserCount!==void 0&&console.log(` Browsers: ${r.browserCount}`)}else console.log(`HTTP Server: Not Running`),r.error&&console.log(` Error: ${r.error}`);console.log(``),console.log(`-`.repeat(50)),process.exit(0)}catch(e){console.error(`Error getting status:`,e),process.exit(1)}}),
|
|
434
|
-
`)}}function
|
|
431
|
+
`)})]}),(0,C.jsx)(`body`,{children:t})]})}function vt(t){let n=new v.Hono;return n.get(`/`,async n=>{try{let r=t.get(e.O.BrowserService),i=t.get(e.O.PageRegistry),a=r.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt,pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt}))}}),c={totalBrowsers:a.length,totalPages:o.length};return n.html((0,C.jsx)(_t,{title:`Playwright MCP Dashboard`,children:(0,C.jsx)(gt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html((0,C.jsx)(_t,{title:`Playwright MCP Dashboard - Error`,children:(0,C.jsxs)(`div`,{style:`padding: 2rem; text-align: center;`,children:[(0,C.jsx)(`h1`,{children:`Failed to load dashboard`}),(0,C.jsx)(`p`,{children:e instanceof Error?e.message:String(e)}),(0,C.jsx)(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function yt(e){if(!e.isError)return;let t=e.content[0];return t?.type===`text`&&typeof t.text==`string`?t.text:`Unknown tool execution error`}function bt(t){try{return t.get(e.O.TelemetryService)}catch{return new e.g}}function xt(e){return e.startsWith(`browser-`)||e===`run-spec`||e===`discover-specs`?e.replaceAll(`-`,`_`):e}function St(e){return{browser_console_logs:`browser_list_console_messages`,browser_console_messages:`browser_list_console_messages`,browser_evaluate:`browser_evaluate_script`,browser_get_console_logs:`browser_list_console_messages`,browser_resize:`browser_resize_page`}[e]}function Ct(t){let n=new v.Hono,r=t.get(e.O.PageRegistry),i=t.get(e.O.BrowserService),a=t.get(e.O.ExtensionTaskQueue),o=bt(t),s=new rt(r,a,o,i);n.use(`*`,(0,y.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`DELETE`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`,`Authorization`]})),n.get(`/health`,e=>{try{let t=i.listBrowsers(),n={status:`healthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,timestamp:new Date().toISOString(),browsers:{count:t.length,instances:t.map(e=>({id:e.id,pageCount:e.pageIds.size,createdAt:e.createdAt.toISOString()}))}};return e.json(n)}catch(t){return e.json({status:`unhealthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,error:t instanceof Error?t.message:String(t)},503)}}),n.post(`/execute`,async n=>{try{let i=await n.req.json();if(!i.tool)return n.json({success:!1,error:`Missing "tool" field in request body`},400);let a=typeof i.arguments?.pageId==`string`?i.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.execute`,{attributes:{"http.method":`POST`,"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":a}},async a=>{let s=t.getAll(e.O.Tool),c=xt(i.tool),d=s.find(e=>e.getDefinition().name===c);if(process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`&&i.tool===`browser_launch`){let e=typeof i.arguments?.videoDir==`string`?i.arguments.videoDir:typeof i.arguments?.video_dir==`string`?i.arguments.video_dir:``;console.log(`[ExtensionRecordingDebug] http execute browser_launch mode=${String(i.arguments?.mode??``)} videoDir=${e}`)}if(!d){let e=St(c),t=e?`Tool "${i.tool}" not found. Use "${e}" instead.`:`Tool "${i.tool}" not found`;return a?.setStatus({code:u.SpanStatusCode.ERROR,message:t}),o.log(`warn`,`browse-tool HTTP execute rejected unknown tool`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.tool.normalized_name":c,"browse_tool.tool.suggested_name":e}}),n.json({success:!1,error:t},404)}let p=i.arguments||{},m;try{m=d.getInputSchema().parse((0,f.coerceArgs)(p,d.getInputSchema()))}catch(e){if(e instanceof l.z.ZodError){let t=(0,f.formatZodError)(e,{schemaName:c,schema:d.getInputSchema()});return a?.setStatus({code:u.SpanStatusCode.ERROR,message:t}),n.json({success:!1,result:{content:[{type:`text`,text:t}],isError:!0},error:t})}throw e}let h=await e.x(n.req.header(e.y),()=>d.execute(m)),g=m,_=typeof g.pageId==`string`?g.pageId:void 0;if(_){let n=t.get(e.O.BrowserService),i=r.get(_);i&&n.recordBrowserActivity(i.browserId,_)}let v=yt(h);return v?(a?.setStatus({code:u.SpanStatusCode.ERROR,message:v}),o.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":c,"browse_tool.page.id":_}})):o.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":c,"browse_tool.page.id":_}}),n.json({success:!h.isError,result:h,error:v})})}catch(e){return o.log(`error`,`browse-tool HTTP execute request crashed`,{attributes:{"http.route":`/execute`},exception:e}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/browsers`,n=>{try{let r=t.get(e.O.BrowserService).listBrowsers();return n.json({browsers:r.map(e=>({id:e.id,profileName:e.profileName,pageIds:Array.from(e.pageIds),currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString()}))})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/tools`,n=>{try{let r=t.getAll(e.O.Tool);return n.json({tools:r.map(e=>e.getDefinition())})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/custom-tools`,async e=>{let t=e.req.query(`dir`);if(!t)return e.json({tools:[],error:`Missing "dir" query parameter`},400);try{let n=await s.listTools(t);return e.json({tools:n})}catch(t){return e.json({tools:[],error:t instanceof Error?t.message:String(t)},400)}}),n.post(`/custom-tools`,async n=>{let i=n.req.query(`dir`);if(!i)return n.json({success:!1,error:`Missing "dir" query parameter`},400);try{let a=await n.req.json();if(!a.tool)return n.json({success:!1,error:`Missing "tool" field in request body`},400);let c=typeof a.arguments?.pageId==`string`?a.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.custom_tool.execute`,{attributes:{"http.method":`POST`,"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c,"browse_tool.custom_tools.directory":i}},async l=>{let d=await e.x(n.req.header(e.y),()=>s.executeTool(i,a.tool,a.arguments||{}));if(c){let n=t.get(e.O.BrowserService),i=r.get(c);i&&n.recordBrowserActivity(i.browserId,c)}let f=yt(d);return f?(l?.setStatus({code:u.SpanStatusCode.ERROR,message:f}),o.log(`error`,`browse-tool custom tool execution failed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}})):o.log(`info`,`browse-tool custom tool execution succeeded`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}}),n.json({success:!d.isError,result:d,error:f})})}catch(e){return o.log(`error`,`browse-tool custom tool request crashed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.custom_tools.directory":i},exception:e}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}});let c=D(t);n.route(`/extension`,c);let d=it(t);n.route(`/api`,d);let p=vt(t);return n.route(`/`,p),n}function wt(t,n,r){t.get(`/ws/extension/:browserId`,r(t=>{let r=t.req.param(`browserId`),i=null;return{onOpen(t,a){if(!r){a.close(1008,`Missing browserId`);return}try{i=n.get(e.O.WebSocketHub).addConnection(a,r)}catch{a.close(1011,`Internal server error`)}},onMessage(t){if(i)try{let r=n.get(e.O.WebSocketHub),a=typeof t.data==`string`?t.data:t.data.toString();r.handleMessage(i,a)}catch{}},onClose(){if(i)try{n.get(e.O.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{n.get(e.O.WebSocketHub).removeConnection(i)}catch{}}}})),t.get(`/ws/stats`,t=>{try{let r=n.get(e.O.WebSocketHub).getStats();return t.json({totalConnections:r.totalConnections,browserCount:r.browserCount,connections:r.connections.map(e=>({id:e.id,browserId:e.browserId,connectedAt:e.connectedAt.toISOString()}))})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}})}const Tt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function Et(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of Tt)if((0,a.existsSync)(i.default.join(t,e)))return t;let e=i.default.dirname(t);if(e===t)return process.cwd();t=e}}const Dt=new g.Command(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(e.d)).option(`--host <host>`,`Host to bind`,e.p()).option(`--headless`,`Run browsers in headless mode by default`,!0).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before closing browsers`,`30`).option(`--registry-dir <path>`,`Custom registry path or directory for service discovery`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking (deprecated)`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--snippets-dir <path>`,`Directory used by browser_run_code to save and load reusable snippets`).option(`--proxy-config-dir <path>`,`Custom proxy config directory`).action(async function(r){let a=N(`httpServe`),o={port:P(this,`port`,r.port,a.port===void 0?void 0:String(a.port),process.env.PLAYWRIGHT_PORT),headless:P(this,`headless`,r.headless,a.headless),idleTimeout:P(this,`idleTimeout`,r.idleTimeout,a.idleTimeout===void 0?void 0:String(a.idleTimeout),process.env[e.u]),host:P(this,`host`,r.host,a.host,process.env.PLAYWRIGHT_HOST),registryDir:P(this,`registryDir`,r.registryDir,a.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:P(this,`registryPath`,r.registryPath,a.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:P(this,`pidsDir`,r.pidsDir,a.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:P(this,`profilesDir`,r.profilesDir,a.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:P(this,`snippetsDir`,r.snippetsDir,a.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR),proxyConfigDir:P(this,`proxyConfigDir`,r.proxyConfigDir,a.proxyConfigDir,process.env[e.s])},s;try{let r=Number.parseInt(o.port,10),a=`browse-tool-http`,c=process.env.NODE_ENV||`development`,l=Et(process.cwd()),u=o.registryPath||o.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;u&&(process.env.PLAYWRIGHT_REGISTRY_DIR=u,process.env.PORT_REGISTRY_PATH=u,process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(u,`processes.json`)),process.env.PLAYWRIGHT_HOST=o.host,process.env.PLAYWRIGHT_PORT=o.port,process.env[e.u]=o.idleTimeout,o.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=o.pidsDir),o.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=o.profilesDir),o.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=i.default.resolve(o.snippetsDir)),o.proxyConfigDir&&(process.env[e.s]=i.default.resolve(o.proxyConfigDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${r}`),console.log(` Host: ${o.host}`),console.log(` Headless: ${o.headless}`),console.log(` Idle Timeout: ${o.idleTimeout} minutes`),process.env.BROWSE_TOOL_SNIPPETS_DIR&&console.log(` Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`),process.env.BROWSE_TOOL_PROXY_CONFIG_DIR&&console.log(` Proxy Config Dir: ${process.env[e.s]}`);let d=e.a(),f=d.get(e.O.ProcessRegistryService),p=Ct(d),{injectWebSocket:m,upgradeWebSocket:h}=(0,b.createNodeWebSocket)({app:p});wt(p,d,h);let g=d.get(e.O.WebSocketHub),v=d.get(e.O.ExtensionTaskQueue),y=d.get(e.O.BrowserService),x=d.get(e.O.ExtensionSessionRegistry),S=d.get(e.O.PageRegistry),C=d.get(e.O.IdleCleanupService);C.start(),g.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=y.getBrowser(n),i,a;if(r&&(r.mode===`extension`||r.mode===`vm`)){i=r.id;let t=S.findByBrowser(i);a=r.currentPageId||t[0]?.id||``,g.reassignConnection(e.id,i),a||(a=S.registerExtensionPage(i),r.pageIds.add(a),r.currentPageId=a),console.log(`[WebSocket] Extension connected to existing browser: ${i}, pageId: ${a}`)}else{i=n,y.registerExtensionBrowserWithId(i),a=S.registerExtensionPage(i);let e=y.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=S.get(a);o&&(o.extensionTabId=t.payload.tabId,o.url=t.payload.url??o.url);let s=x.register({browserId:i,tabId:t.payload.tabId,url:t.payload.url,metadata:{transport:`websocket`}});g.sendSessionAck(e,t.id??``,s.id,s.controlMode),g.broadcastPageCreated(i,a,S.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=v.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&y.recordBrowserActivity(e.browserId,e.pageId)}},onTabMapped:(e,t)=>{if(t.type===`tab:mapped`){let e=S.get(t.payload.pageId);if(!e)return;e.extensionTabId=t.payload.tabId,y.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&x.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let w=new t.PortRegistryService(process.env.PORT_REGISTRY_PATH);console.log(u?` Registry path: ${u}`:` Registry path: default (~/.port-registry/ports.json)`);let T=await w.reservePort({repositoryPath:l,serviceName:a,serviceType:`tool`,environment:c,preferredPort:r,pid:process.pid,host:o.host,force:!0,portRange:t.DEFAULT_PORT_RANGE,metadata:{headless:o.headless,idleTimeout:o.idleTimeout}});if(!T.success||T.port===void 0)throw Error(T.error||`Failed to reserve port in range ${t.DEFAULT_PORT_RANGE.min}-${t.DEFAULT_PORT_RANGE.max}`);let E=T.port;T.record.metadata={...T.record.metadata,healthCheckUrl:`${e.f(o.host,E)}/health`};let D=(0,_.serve)({fetch:p.fetch,port:E,hostname:o.host});m(D),s=await(0,n.createProcessLease)({repositoryPath:l,serviceName:a,serviceType:`service`,environment:c,pid:process.pid,port:E,host:o.host,metadata:{healthCheckUrl:`${e.f(o.host,E)}/health`,headless:o.headless,idleTimeout:o.idleTimeout,transport:`http`}},f);let O=e.f(o.host,E);console.log(`HTTP server listening on ${O}`),console.log(` Health check: ${O}/health`),console.log(` Execute tool: POST ${O}/execute`),console.log(`
|
|
432
|
+
Press Ctrl+C to stop the server`);let k=async t=>{console.log(`\n\n${t} received. Shutting down gracefully...`);try{C.stop(),D.close(),console.log(`Server closed`);try{await d.get(e.O.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{s&&await s.release({kill:!1}),console.log(`Service deregistered`)}catch(e){console.error(`Process registry cleanup error:`,e)}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>k(`SIGINT`)),process.on(`SIGTERM`,()=>k(`SIGTERM`)),await new Promise(()=>{})}catch(e){if(s)try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Ot={browser_click:[`input`],browser_fill:[`input`],browser_type:[`input`],browser_select:[`input`],browser_hover:[`input`],browser_drag:[`input`],browser_press_key:[`input`],browser_upload_file:[`input`],browser_navigate:[`navigation`],browser_go_back:[`navigation`],browser_go_forward:[`navigation`],browser_reload:[`navigation`],browser_wait_for:[`navigation`],browser_snapshot:[`snapshot`],browser_screenshot:[`snapshot`],browser_pdf:[`snapshot`],browser_list_pages:[`page`],browser_new_page:[`page`],browser_select_page:[`page`],browser_close_page:[`page`],browser_resize_page:[`page`,`emulation`],browser_handle_dialog:[`dialog`],browser_list_network_requests:[`network`],browser_get_network_request:[`network`],browser_list_console_messages:[`console`],browser_evaluate_script:[`script`],browser_emulate:[`emulation`],browser_expect:[`testing`],browser_start_trace:[`tracing`],browser_stop_trace:[`tracing`],browser_list_profiles:[`profile`],browser_delete_profile:[`profile`],run_spec:[`spec`,`testing`],discover_specs:[`spec`,`testing`],browser_launch:[`browser`],browser_close:[`browser`],browser_run_code:[`code`,`script`]};function kt(e){let t=new Set;for(let[n,r]of Object.entries(Ot))r.some(t=>e.includes(t))&&t.add(n);return t}const At=3e3,jt=[`browser_launch`],Mt=[`browser_new_page`],Nt=[`browser_close`],Pt=[`browser_close_page`];function Ft(e,t){let n=e.replaceAll(`-`,`_`);return t&&(n===`browser_list`||n===`browser_list_session`||n===`browser_list_sessions`)?`browser_list_session`:e.startsWith(`browser-`)||e===`run-spec`||e===`discover-specs`?n:e}function J(e){if(!e?.content?.[0]?.text)return null;try{let t=JSON.parse(e.content[0].text);return{browserId:t.browserId,pageId:t.pageId,mode:t.mode,url:t.url,closedBrowserId:t.closedBrowserId,closedPageId:t.closedPageId,browserKeptOpen:t.browserKeptOpen}}catch{return null}}async function It(e){await new Promise(t=>setTimeout(t,e))}async function Lt(e){let t=new AbortController,n=setTimeout(()=>t.abort(),At);try{let n=await fetch(`${e}/tools`,{signal:t.signal});if(!n.ok)throw Error(`HTTP error ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);if(!Array.isArray(r.tools))throw Error(`Invalid /tools response: missing tools array`);if(r.tools.length===0)throw Error(`HTTP server returned an empty tool list`);return r.tools}finally{clearTimeout(n)}}function Rt(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function zt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),At);try{let r=await fetch(Rt(e,t),{signal:n.signal});if(!r.ok)throw Error(`HTTP error ${r.status}: ${r.statusText}`);let i=await r.json();if(i.error)throw Error(i.error);if(!Array.isArray(i.tools))throw Error(`Invalid /custom-tools response: missing tools array`);return i.tools}finally{clearTimeout(r)}}async function Bt(e){let t;for(let n=1;n<=4;n+=1)try{return await Lt(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await It(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function Vt(e,t){let n;for(let r=1;r<=4;r+=1)try{return await zt(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await It(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function Ht(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?kt(e.tags):null,n=new Set(e.exclude??[]);if(!t)return n.size>0?n:null;for(let e of n)t.delete(e);return t}function Ut(e){let t=new Map,n=(e,n)=>{let r=t.get(e)??new Set;r.add(n),t.set(e,r)};return e.defaultMode&&n(`browser_launch`,`mode`),e.enforcedProfileName&&n(`browser_launch`,`profileName`),t}function Wt(e,t){let n=t.get(e.name);if(!n?.size)return e;let r=e.inputSchema;if(!r.properties||typeof r.properties!=`object`)return e;let i=Object.fromEntries(Object.entries(r.properties).filter(([e])=>!n.has(e))),a={...r,properties:i};return Array.isArray(r.required)&&(a.required=r.required.filter(e=>typeof e!=`string`||!n.has(e))),{...e,inputSchema:a}}function Gt(t){let{httpBaseUrl:n,sessionTracker:r,defaultMode:i,toolFilter:a,customToolsDir:o,enforcedProfileName:s,proxyConfigDir:c,ownerId:l}=t,u=a?.tags?.length?Ht(a):null,d=new Set(a?.exclude??[]),f=u!==null||d.size>0,h=Ut({defaultMode:i,enforcedProfileName:s}),g=new p.Server({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),_={name:`browser_list_session`,description:`List all browsers and pages created in this MCP session. Use this to see what resources you have available.`,inputSchema:{type:`object`,properties:{},required:[],additionalProperties:!1}},v=[],y=[],b=new Set;function x(e){return f?e.filter(e=>d.has(e.name)?!1:u===null?!0:u.has(e.name)):e}function S(e){return e.filter(e=>!d.has(e.name))}function C(e){return s?{...e,profileName:s}:e}function w(t){let n=e.c({proxy:t.proxy,profileName:typeof t.profileName==`string`?t.profileName:void 0,proxyConfigDir:c});if(!n){let e={...t};return delete e.proxy,e}return{...t,proxy:n}}function T(e){if(!s)return e;let t=e?.content?.[0]?.text;if(!t)return e;try{let n=JSON.parse(t),r=(Array.isArray(n.profiles)?n.profiles:[]).filter(e=>e?.name===s);return{...e,content:[{...e.content[0],text:JSON.stringify({...n,profileCount:r.length,profiles:r},null,2)},...e.content.slice(1)]}}catch{return e}}function E(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function D(e){return d.has(e)?!1:u===null?!0:u.has(e)}return g.setRequestHandler(m.ListToolsRequestSchema,async()=>{try{let e=await Bt(n);v=e,b=new Set(e.map(e=>e.name));let t=o?await Vt(n,o):[];y=t;let i=[...x(e).map(e=>Wt(e,h)),...S(t).map(e=>E(e))];return r&&i.push(_),{tools:i}}catch(e){if(v.length>0||y.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[...x(v).map(e=>Wt(e,h)),...S(y).map(e=>E(e))];return r&&t.push(_),{tools:t}}let t=e instanceof Error?e.message:String(e);throw Error(`Failed to list tools from HTTP server at ${n}: ${t}`,{cause:e})}}),g.setRequestHandler(m.CallToolRequestSchema,async t=>{let{name:a,arguments:s}=t.params,c=Ft(a,!!r),u=new Set(y.map(e=>e.name));if(c!==`browser_list_session`&&!D(c))return{content:[{type:`text`,text:`Tool "${c}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(c===`browser_list_session`&&r){let e=r.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let d=s||{};c===`browser_launch`&&i&&d.mode===void 0&&(d={...d,mode:i}),c===`browser_launch`&&(d=C(d),d=w(d));try{if(o&&!u.has(c)&&!b.has(c))try{y=await Vt(n,o),u=new Set(y.map(e=>e.name))}catch(e){console.error(`Failed to refresh custom tools, using cached list: ${e instanceof Error?e.message:String(e)}`)}let t=u.has(c),i=await fetch(t&&o?Rt(n,o):`${n}/execute`,{method:`POST`,headers:l?{"Content-Type":`application/json`,[e.y]:l}:{"Content-Type":`application/json`},body:JSON.stringify({tool:c,arguments:d})});if(!i.ok){let e=await i.text();return{content:[{type:`text`,text:`HTTP error ${i.status}: ${e}`}],isError:!0}}let a=await i.json();if(!a.success)return{content:[{type:`text`,text:a.error||a.result?.content?.[0]?.text||`Unknown error from HTTP server`}],isError:!0};if(r&&a.result){if(jt.includes(c)){let e=J(a.result);e?.browserId&&(r.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&r.trackPage(e.pageId,e.browserId,e.url))}else if(Mt.includes(c)){let e=J(a.result);e?.pageId&&e?.browserId&&r.trackPage(e.pageId,e.browserId,e.url)}else if(Nt.includes(c)){let e=J(a.result);e?.closedBrowserId&&r.untrackBrowser(e.closedBrowserId)}else if(Pt.includes(c)){let e=J(a.result);e?.closedPageId&&r.untrackPage(e.closedPageId),e?.browserId&&e.browserKeptOpen===!1&&r.untrackBrowser(e.browserId)}}return c===`browser_list_profiles`&&a.result?T(a.result):a.result||{content:[{type:`text`,text:`No result returned`}]}}catch(e){return{content:[{type:`text`,text:`Failed to execute tool via HTTP server: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),g}const Y=`stdio`,Kt=`http`,X=`streamable-http`,qt=new Set([Y,Kt,X]),Jt=`chromium`,Yt=new Set([Jt,`firefox`,`webkit`]),Xt=new Set([`playwright`,`extension`,`vm`,`stealth`]),Z=`SIGINT`,Q=`SIGTERM`,Zt=`PLAYWRIGHT_PORT`,Qt=[`pnpm-workspace.yaml`,`nx.json`,`.git`],$t=`browse-tool-mcp-http`,en=`service`,tn=t.DEFAULT_PORT_RANGE.min;var nn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...qt].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},rn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...Yt].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},an=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...Xt].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function on(e){let t=e.toLowerCase();if(!Yt.has(t))throw new rn(e);return t}function sn(e){let t=e.toLowerCase();if(!Xt.has(t))throw new an(e);return t}function cn(e){let t=typeof e==`number`?e:Number.parseInt(e,10);if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}function ln(e){return e!=null&&String(e).trim()!==``}function un(e){let t=e.toLowerCase();return t===Kt?X:t}function dn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function fn(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of Qt)if((0,a.existsSync)(i.default.join(t,e)))return t;let e=i.default.dirname(t);if(e===t)return process.cwd();t=e}}function pn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function mn(e){let t=e.tags?pn(e.tags):void 0,n=e.exclude?pn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var hn=class extends Error{code=`BROWSER_CLEANUP_FAILED`;recovery=`Browsers may still be running; check the HTTP server or kill processes manually`;failedBrowserIds;httpBaseUrl;constructor(e,t,n){super(`Failed to close ${e.length} browser(s): ${e.join(`, `)}`,n),this.name=`BrowserCleanupError`,this.failedBrowserIds=e,this.httpBaseUrl=t}},gn=class extends Error{code=`SESSION_SHUTDOWN_FAILED`;recovery=`Check logs for cleanup and transport errors; processes may need manual cleanup`;signal;constructor(e,t){super(`Shutdown failed for signal ${e}`,t),this.name=`SessionShutdownError`,this.signal=e}},_n=class extends Error{code=`HTTP_SERVER_START_FAILED`;recovery=`Check if the HTTP server binary exists and the port is available`;constructor(e){super(`Failed to start HTTP server`,e),this.name=`HttpServerStartError`}},vn=class extends Error{code=`MCP_SERVER_BOOTSTRAP_FAILED`;recovery=`Check if all dependencies are installed and try again`;constructor(e){super(`Failed to start MCP server`,e),this.name=`McpServerBootstrapError`}};async function yn(t,n,r){let i=t.getBrowserIds(),a=[],o;for(let t of i)try{let i=await fetch(`${n}/execute`,{method:`POST`,headers:r?{"Content-Type":`application/json`,[e.y]:r}:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:t}})});if(!i.ok){let e=await i.text().catch(()=>``);throw Error(`HTTP ${i.status} from ${n}/execute: ${e}`)}console.error(` Closed browser: ${t}`)}catch(e){let n=e instanceof Error?e:Error(String(e));console.error(` Failed to close browser: ${t}`,n),a.push(t),o??=n}if(a.length>0)throw new hn(a,n,{cause:o});t.clear()}async function bn(e,t,n,r,i){await e.start();let a=!1,o=async o=>{if(a)return;a=!0,console.error(`\nReceived ${o}, shutting down gracefully...`);let s;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await yn(t,n,r))}catch(e){s=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,s)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),s??=t}try{await i?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),s??=t}if(s){let e=new gn(o,{cause:s});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(Z,()=>o(Z)),process.once(Q,()=>o(Q))}const xn=new g.Command(`mcp-serve`).description(`Start Playwright MCP server for browser automation`).option(`-t, --type <type>`,`Transport type: ${[Y,X].join(`, `)}`,Y).option(`-b, --browser <browser>`,`Default browser type: ${[...Yt].join(`, `)}`,Jt).option(`--headless`,`Run browsers in headless mode by default`,!1).option(`--no-headless`,`Run browsers in headed mode (visible window)`).option(`--host <host>`,`Default host for HTTP services`,e.p()).option(`--port <port>`,`Port for streamable HTTP transport`,String(tn)).option(`--http-port <port>`,`Port for the inner browse-tool HTTP service`).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...Xt].join(`, `)}`).option(`--tags <tags>`,`Comma-separated tags to filter tools (e.g. input,navigation,snapshot)`).option(`--exclude <tools>`,`Comma-separated tool names to exclude (applied after --tags)`).option(`--custom-tools <path>`,`Path to a folder containing tools.yaml and custom tool scripts`).option(`--chrome-for-testing-path <path>`,`Path to a Chrome for Testing executable for the inner HTTP server`).option(`--snippets-dir <path>`,`Path to a folder where browser_run_code snippets are stored`).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before auto-closing browsers`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--registry-dir <path>`,`Custom registry directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--proxy-config-dir <path>`,`Custom proxy config directory`).action(async function(t){let r=N(`mcpServe`),a={type:P(this,`type`,t.type,r.type),browser:P(this,`browser`,t.browser,r.browser),headless:P(this,`headless`,t.headless,r.headless),profile:P(this,`profile`,t.profile,r.profile),mode:P(this,`mode`,t.mode,r.mode),host:P(this,`host`,t.host,r.host,process.env.PLAYWRIGHT_HOST),port:P(this,`port`,t.port,r.port,process.env.PLAYWRIGHT_MCP_PORT),httpPort:P(this,`httpPort`,t.httpPort,r.httpPort,process.env[Zt]),tags:P(this,`tags`,t.tags,r.tags),exclude:P(this,`exclude`,t.exclude,r.exclude),customTools:P(this,`customTools`,t.customTools,r.customTools),chromeForTestingPath:P(this,`chromeForTestingPath`,t.chromeForTestingPath,r.chromeForTestingPath,process.env[e.w]),snippetsDir:P(this,`snippetsDir`,t.snippetsDir,r.snippetsDir),idleTimeout:P(this,`idleTimeout`,t.idleTimeout,r.idleTimeout===void 0?void 0:String(r.idleTimeout),process.env[e.u]),registryPath:P(this,`registryPath`,t.registryPath,r.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:P(this,`registryDir`,t.registryDir,r.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:P(this,`pidsDir`,t.pidsDir,r.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:P(this,`profilesDir`,t.profilesDir,r.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),proxyConfigDir:P(this,`proxyConfigDir`,t.proxyConfigDir,r.proxyConfigDir,process.env[e.s])},o=un(a.type),s=fn(process.cwd()),c=process.env.NODE_ENV||`development`,l,u;try{if(!qt.has(o))throw new nn(o);let t=on(a.browser),r=a.mode?sn(a.mode):void 0,f=cn(a.port??tn),p=ln(a.httpPort),m=p?cn(a.httpPort):void 0,h=mn(a),g=a.customTools?i.default.resolve(a.customTools):void 0,_=a.chromeForTestingPath?i.default.resolve(a.chromeForTestingPath):void 0,v=a.snippetsDir?i.default.resolve(a.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${o}`),o===X&&console.error(` MCP endpoint: http://${a.host??e.p()}:${f}/mcp`),console.error(` Default browser: ${t}`),console.error(` Headless: ${a.headless}`),r&&console.error(` Default mode: ${r}`),a.profile&&console.error(` Profile: ${a.profile}`),h?.tags?.length&&console.error(` Tags: ${h.tags.join(`, `)}`),h?.exclude?.length&&console.error(` Exclude: ${h.exclude.join(`, `)}`),g&&console.error(` Custom tools: ${g}`),_&&console.error(` Chrome for Testing path: ${_}`),v&&console.error(` Snippets dir: ${v}`),a.idleTimeout&&console.error(` Idle timeout: ${a.idleTimeout} minutes`);let y=a.registryPath||a.registryDir;y&&(process.env.PLAYWRIGHT_REGISTRY_DIR=y,process.env.PORT_REGISTRY_PATH=y,process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(y,`processes.json`),console.error(` Registry path: ${y}`)),a.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=a.pidsDir,console.error(` PIDs dir: ${a.pidsDir}`)),a.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=a.profilesDir,console.error(` Profiles dir: ${a.profilesDir}`)),a.proxyConfigDir&&(process.env[e.s]=i.default.resolve(a.proxyConfigDir),console.error(` Proxy config dir: ${process.env[e.s]}`)),v&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=v),_&&(process.env[e.w]=_),a.idleTimeout&&(process.env[e.u]=a.idleTimeout),a.host&&(process.env.PLAYWRIGHT_HOST=a.host);let b=e.o(),x=b.get(e.O.ProcessRegistryService);u=await b.get(e.O.McpPortAllocationService).acquirePorts({repositoryPath:s,environment:c,host:a.host??e.p(),pid:process.pid,preferredMcpPort:o===X?f:void 0,preferredHttpPort:m,exactHttpPort:p,reserveMcpPort:o===X,mcpServiceName:$t,mcpServiceType:`tool`,mcpMetadata:o===X?{healthCheckUrl:`${e.f(a.host??e.p(),f)}/health`,transport:X}:void 0});let S=u.httpPort;process.env[Zt]=S.toString();let C=await b.get(e.O.HttpServerManager).ensureRunning(S,{exactPort:!0});if(!C.running)throw new _n;let w=e.f(e.p(),C.port),T=C.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${T} on port ${C.port}`),o===Y){let i=new e.l,o=(0,d.randomUUID)(),f=new e.n(Gt({httpBaseUrl:w,sessionTracker:i,defaultMode:r,toolFilter:h,customToolsDir:g,enforcedProfileName:a.profile,proxyConfigDir:process.env[e.s],ownerId:o}));l=await(0,n.createProcessLease)({repositoryPath:s,serviceName:`browse-tool-mcp`,serviceType:en,environment:c,pid:process.pid,metadata:{transport:Y,browser:t,mode:r}},x),await bn(f,i,w,o,async()=>{await l?.release({kill:!1}),await u?.release()});return}let E=u.mcpPort;if(!E)throw Error(`Failed to reserve ${X} port ${f}`);l=await(0,n.createProcessLease)({repositoryPath:s,serviceName:$t,serviceType:en,environment:c,pid:process.pid,port:E,host:a.host??e.p(),metadata:{healthCheckUrl:`${e.f(a.host??e.p(),E)}/health`,transport:X}},x);let D=new e.t(({headers:t})=>{let n=dn(t,`x-profile`)??a.profile,i=new e.l,o=(0,d.randomUUID)();return{server:Gt({httpBaseUrl:w,sessionTracker:i,defaultMode:r,toolFilter:h,customToolsDir:g,enforcedProfileName:n,proxyConfigDir:process.env[e.s],ownerId:o}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await yn(i,w,o)}}},{host:a.host??e.p(),port:E});try{await D.start()}catch(e){try{await l.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}throw await u.release(),u=void 0,e}let O=!1,k=async e=>{if(!O){O=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await D.stop();try{await l?.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}await u?.release(),u=void 0,process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(Z,()=>k(Z)),process.once(Q,()=>k(Q))}catch(e){if(l)try{await l.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}if(u)try{await u.release()}catch(e){console.error(`Port allocation cleanup error:`,e)}(e instanceof nn||e instanceof rn||e instanceof an||e instanceof _n)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new vn({cause:e instanceof Error?e:Error(String(e))});console.error(`Error [${t.code}]: ${t.message}`,t.cause),console.error(`Recovery: ${t.recovery}`),process.exit(1)}}),Sn=new g.Command(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=N(`httpServe`),n=process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH??t.registryPath??t.registryDir;n&&(process.env.PLAYWRIGHT_REGISTRY_DIR=n,process.env.PLAYWRIGHT_REGISTRY_PATH=n,process.env.PORT_REGISTRY_PATH=n),!process.env.PLAYWRIGHT_PIDS_DIR&&t.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=t.pidsDir),console.log(`browse-tool Status
|
|
433
|
+
`),console.log(`-`.repeat(50));let r=await e.i().get(e.O.HttpServerManager).getStatus();if(r.running){let n=process.env.PLAYWRIGHT_HOST??t.host??e.p();console.log(`HTTP Server: Running`),console.log(` PID: ${r.pid}`),console.log(` Port: ${r.port}`),console.log(` Health: ${e.f(n,r.port)}/health`),r.browserCount!==void 0&&console.log(` Browsers: ${r.browserCount}`)}else console.log(`HTTP Server: Not Running`),r.error&&console.log(` Error: ${r.error}`);console.log(``),console.log(`-`.repeat(50)),process.exit(0)}catch(e){console.error(`Error getting status:`,e),process.exit(1)}}),Cn=new g.Command(`stop`).description(`Stop HTTP server and clean up registry/PID files`).action(async()=>{try{let t=N(`httpServe`),n=process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH??t.registryPath??t.registryDir;n&&(process.env.PLAYWRIGHT_REGISTRY_DIR=n,process.env.PLAYWRIGHT_REGISTRY_PATH=n,process.env.PORT_REGISTRY_PATH=n),!process.env.PLAYWRIGHT_PIDS_DIR&&t.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=t.pidsDir),console.log(`Stopping browse-tool services...`),await e.i().get(e.O.HttpServerManager).stop()?(console.log(`HTTP server stopped`),console.log(`Registry and PID files cleaned up`)):console.log(`No HTTP server was running`),console.log(`Done!`),process.exit(0)}catch(e){console.error(`Error stopping services:`,e),process.exit(1)}}),wn=new Set([`--config`]);function Tn(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(wn.has(n)){t+=1;continue}if(![...wn].some(e=>n.startsWith(`${e}=`))&&!n.startsWith(`-`))return n}}function En(e,t=!1){if(t)return!1;let n=Tn(e);return n?n===`tools`?!0:n===`help`&&e.includes(`tools`):!1}function Dn(e){return e.replace(/_/g,`-`)}function $(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function On(e){let t=new Set;e.type&&t.add(e.type);for(let n of e.oneOf??[])n.type&&t.add(n.type);for(let n of e.anyOf??[])n.type&&t.add(n.type);return t}function kn(e,t){let n=On(t);if(n.has(`boolean`)){if(e===`true`||e===`1`)return!0;if(e===`false`||e===`0`)return!1}if(t.type===`number`||t.type===`integer`){let t=Number(e);if(Number.isNaN(t))throw Error(`Invalid number: ${e}`);return t}if(t.type===`boolean`){if(e===`true`||e===`1`)return!0;if(e===`false`||e===`0`)return!1;throw Error(`Invalid boolean: ${e}`)}if(n.has(`array`)||n.has(`object`))try{return JSON.parse(e)}catch{if(t.type===`array`||t.type===`object`)throw Error(`Invalid JSON: ${e}`)}if(n.has(`number`)||n.has(`integer`)){let t=Number(e);if(!Number.isNaN(t)&&e.trim()!==``)return t}return e}function An(e,t){let n=$(e),r=On(t);return t.type===`boolean`?`--${n}`:r.has(`boolean`)&&r.has(`object`)?`--${n} [value]`:`--${n} <value>`}function jn(t){let{definition:n,execute:r}=t,i=new g.Command(Dn(n.name));i.description(n.description);let a=n.inputSchema,o=a.properties||{},s=new Set(a.required||[]);for(let[e,t]of Object.entries(o)){let n=t,r=An(e,n),a=n.description||``;n.enum&&n.enum.length>0&&(a+=` (choices: ${n.enum.join(`, `)})`),n.default!==void 0&&(a+=` (default: ${JSON.stringify(n.default)})`),s.has(e)&&(a+=` [required]`),n.type===`boolean`?n.default===!0?i.option(`--no-${$(e)}`,`Disable ${a}`):i.option(r,a,n.default):n.enum&&n.enum.length>0?i.addOption(new g.Option(r,a).choices(n.enum)):i.option(r,a)}return i.action(async function(){let t=this.optsWithGlobals(),i=N(`tools`),a=P(this,`format`,t.format??`json`,i.format),c=P(this,`color`,t.color??!0,i.color),l=P(this,`port`,String(t.port??e.d),i.port===void 0?void 0:String(i.port),process.env.PLAYWRIGHT_PORT),u={format:a,color:c},d=Se(n.name);try{let e={};for(let[n,r]of Object.entries(o)){let i=r,a=$(n).replace(/-([a-z])/g,(e,t)=>t.toUpperCase()),o=this.getOptionValueSourceWithGlobals(a),s=t[a];i.type===`boolean`&&i.default===!0&&(s=t[a]),(o===void 0||o===`default`||o===`implied`)&&d[n]!==void 0&&(s=d[n]),s===void 0&&i.default!==void 0&&(s=i.default),s!==void 0&&(typeof s==`string`&&i.type!==`string`?e[n]=kn(s,i):e[n]=s)}for(let t of s)if(e[t]===void 0){let e=$(t);console.error(L(`Missing required option: --${e}`,u.color)),process.exit(1)}let i=Number.parseInt(l,10),a=F({port:i}),c=r?await r(e,{command:this,formatterOptions:u,port:i}):await a.execute(n.name,e),f=Te(c,u);f&&console.log(f),c.isError&&process.exit(1)}catch(e){console.error(L(e instanceof Error?e:String(e),u.color)),process.exit(1)}}),i}function Mn(e){return e.map(jn)}const Nn={name:`browser_list_session`,description:`List browsers and pages currently tracked by the browse-tool HTTP server.`,inputSchema:{type:`object`,properties:{},required:[],additionalProperties:!1}};function Pn(){return new g.Command(`tools`).description(`Execute browser automation tools`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.d))}function Fn(e){return{browserCount:e.length,browsers:e.map(e=>({browserId:e.id,profileName:e.profileName,currentPageId:e.currentPageId,pageIds:e.pageIds,createdAt:e.createdAt}))}}function In(){return{definition:Nn,execute:async(e,t)=>{let n=await F({port:t.port}).listBrowsers();return{content:[{type:`text`,text:JSON.stringify(Fn(n),null,2)}]}}}}function Ln(e){return[...e.map(e=>({definition:e})),In()]}async function Rn(t,n){let r=N(`tools`),i=Number(n?.port??process.env.PLAYWRIGHT_PORT??r.port??e.d);try{let e=Mn(Ln(await F({port:i}).listTools()));for(let n of e)t.addCommand(n)}catch(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(`${L(`Failed to load tool commands: ${t}`,!0)}\n`),process.stderr.write(`Make sure the HTTP server is running: browse-tool http-serve
|
|
434
|
+
`)}}function zn(e,t){let n=[...e];for(let e=0;e<n.length;e+=1){let t=n[e];if(t===`--port`||t===`-p`){let t=n[e+1],r=Number(t);if(!Number.isNaN(r)&&t)return r;continue}if(t.startsWith(`--port=`)){let e=Number(t.slice(7));if(!Number.isNaN(e))return e;continue}if(t.startsWith(`-p=`)){let e=Number(t.slice(3));if(!Number.isNaN(e))return e}}return Number(t)}async function Bn(){let t=xe(process.argv.slice(2)),n=process.env.PLAYWRIGHT_PORT??t.config.commands.tools?.port??t.config.commands.exec?.port??e.d,r=new g.Command;r.name(`browse-tool`).description(`MCP server for browser automation using Playwright with profile management, page registry, and multi-browser support`).option(`--config <path>`,`Path to browse-tool config file or config directory`).version(w),r.addCommand(xn),r.addCommand(Dt),r.addCommand(se),r.addCommand(Ie),r.addCommand(Cn),r.addCommand(Sn),r.addCommand(Le),r.addCommand(Pe),r.addCommand(Fe);let i=Pn();En(process.argv.slice(2),process.env.PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS===`1`)&&await Rn(i,{port:zn(process.argv.slice(2),n)}),r.addCommand(i),await r.parseAsync(process.argv)}Bn().catch(e=>{console.error(e instanceof Error?e.message:String(e)),process.exit(1)});
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{C as e,D as t,E as n,O as r,S as i,T as a,_ as o,a as s,b as c,c as l,d as u,f as d,g as f,h as p,i as m,l as h,m as g,n as _,o as v,p as y,s as b,t as ee,u as x,v as S,w as C,x as w,y as T}from"./streamable-http-Cct_zaGc.mjs";import{stripTypeScriptTypes as E}from"node:module";import"reflect-metadata/lite";import{DEFAULT_PORT_RANGE as D,PortRegistryService as O}from"@agimon-ai/foundation-port-registry";import{createProcessLease as te,resolveSiblingRegistryPath as ne}from"@agimon-ai/foundation-process-registry";import{Container as k,ContainerModule as re}from"inversify";import A from"node:path";import{existsSync as j,readFileSync as ie,statSync as ae}from"node:fs";import{readFile as M,stat as oe}from"node:fs/promises";import{homedir as se}from"node:os";import{z as N}from"zod";import{SpanStatusCode as P}from"@opentelemetry/api";import{randomUUID as ce}from"node:crypto";import{coerceArgs as le,formatZodError as ue}from"@agimon-ai/foundation-validator";import{Server as de}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as fe,ListToolsRequestSchema as pe}from"@modelcontextprotocol/sdk/types.js";import{StdioServerTransport as me}from"@modelcontextprotocol/sdk/server/stdio.js";import{Command as F,Option as he}from"commander";import{serve as ge}from"@hono/node-server";import{Hono as I}from"hono";import{cors as _e}from"hono/cors";import{createNodeWebSocket as ve}from"@hono/node-ws";import{raw as ye}from"hono/html";import{Fragment as be}from"hono/jsx";import{jsx as L,jsxs as R}from"hono/jsx/jsx-runtime";var xe=`0.17.1`;const Se=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function Ce(e,t){if(!Se)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function we(e){let t=new I,n;try{n=e.get(r.TelemetryService)}catch{n=new f}return t.get(`/telemetry-config`,async e=>{try{let t=new URL(e.req.url).origin;return e.json(await o(process.env,t))}catch(t){return e.json({enabled:!1,error:t instanceof Error?t.message:String(t)},500)}}),t.get(`/tasks`,t=>{try{let n=e.get(r.ExtensionTaskQueue).getNextTask();return n?t.json({task:{id:n.id,tool:n.tool,arguments:n.arguments,telemetry:n.telemetry}}):t.json({})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/result`,async t=>{try{let n=await t.req.json();if(!n.taskId)return t.json({success:!1,error:`Missing taskId in request body`},400);let i=e.get(r.ExtensionTaskQueue),a=e.get(r.BrowserService),o={taskId:n.taskId,success:n.success,result:n.result,error:n.error},s=i.submitResult(o);return s?(s.browserId&&a.recordBrowserActivity(s.browserId,s.pageId),t.json({success:!0})):t.json({success:!1,error:`Task ${n.taskId} not found or already completed`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/status`,t=>{try{let n=e.get(r.ExtensionTaskQueue),i=n.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:n.queueSize};return t.json(a)}catch(e){return t.json({connected:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/register`,async t=>{try{let n=await t.req.json();if(!n.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).register(n);return t.json({success:!0,session:{id:i.id,browserId:i.browserId,controlMode:i.controlMode,createdAt:i.createdAt.toISOString()}})}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/heartbeat`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).heartbeat(n);return i?t.json({success:!0,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested,lastHeartbeatAt:i.lastHeartbeatAt.toISOString()}}):t.json({success:!1,error:`Session ${n.sessionId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/tab-mapped`,async t=>{try{let n=await t.req.json();if(!n.pageId||typeof n.tabId!=`number`)return t.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=e.get(r.PageRegistry),a=e.get(r.BrowserService),o=i.get(n.pageId);return o?(o.extensionTabId=n.tabId,a.recordBrowserActivity(o.browserId,n.pageId),t.json({success:!0})):t.json({success:!1,error:`Page ${n.pageId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/recording`,async t=>{try{let n=await t.req.json();if(!n.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(r.BrowserService),a=await i.persistExtensionRecordingArtifact(n.browserId,n.videoBase64);return Ce(`artifact received`,{browserId:n.browserId,videoBase64Size:n.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(n.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${n.browserId}" has no active recording target`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/recording/chunk`,async t=>{try{let i=await t.req.json();if(!i.browserId||!i.chunkBase64)return t.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=e.get(r.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return Ce(`chunk received`,{browserId:i.browserId,chunkIndex:i.chunkIndex,mimeType:i.mimeType,persisted:!!o,chunkBase64Size:i.chunkBase64.length}),o?(n.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":i.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof i.chunkIndex==`number`?{"browse_tool.recording.chunk_index":i.chunkIndex}:{},...typeof i.mimeType==`string`?{"browse_tool.recording.mime_type":i.mimeType}:{}}}),a.recordBrowserActivity(i.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${i.browserId}" has no active recording target`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/browser-log`,async e=>{try{let t=await e.req.json();return!t.message||typeof t.message!=`string`?e.json({success:!1,error:`Missing message in request body`},400):(n.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),Ce(`browser log relayed`,{level:t.level??`info`,message:t.message,attributes:t.attributes}),e.json({success:!0}))}catch(t){return e.json({success:!1,error:t instanceof Error?t.message:String(t)},500)}}),t.post(`/handoff`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).requestHandoff(n);return i?t.json({success:!0,message:`Handoff requested. AI will take control when ready.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):t.json({success:!1,error:`Session ${n.sessionId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/handoff/acknowledge`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).acknowledgeHandoff(n.sessionId);return i?t.json({success:!0,message:`Handoff acknowledged. AI now has control.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):t.json({success:!1,error:`Session ${n.sessionId} not found or no handoff pending`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/sessions`,t=>{try{let n=e.get(r.ExtensionSessionRegistry).listSessions();return t.json({sessions:n.map(e=>({id:e.id,browserId:e.browserId,tabId:e.tabId,currentUrl:e.currentUrl,controlMode:e.controlMode,activeSpecPath:e.activeSpecPath,handoffRequested:e.handoffRequested,createdAt:e.createdAt.toISOString(),lastHeartbeatAt:e.lastHeartbeatAt.toISOString()}))})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t}function Te(e){if(e==null||e===``)return;let t=typeof e==`number`?e:typeof e==`string`?Number.parseInt(e,10):NaN;if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}const Ee=[`pnpm-workspace.yaml`,`nx.json`,`.git`],De=`browse-tool-chrome`,Oe=`tool`,ke=process.env.NODE_ENV||`development`,Ae=`127.0.0.1`;function je(e=process.cwd()){let t=A.resolve(e);for(;;){for(let e of Ee)if(j(A.join(t,e)))return t;let e=A.dirname(t);if(e===t)return process.cwd();t=e}}function Me(){return new O(process.env.PORT_REGISTRY_PATH)}async function Ne(e,t,n){let r=Me(),i=await r.reservePort({repositoryPath:e,serviceName:De,serviceType:Oe,environment:ke,preferredPort:t,portRange:n,pid:process.pid,host:Ae,force:!0,metadata:{transport:`stdio`,mode:`chrome-serve`}});if(!i.success||!i.record)throw Error(i.error||`Failed to reserve port ${t}`);let a=!1;return{port:i.record.port,release:async()=>{if(a)return;a=!0;let n=await r.releasePort({repositoryPath:e,serviceName:De,serviceType:Oe,environment:ke,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function Pe(){return new re(e=>{e.bind(r.ExtensionTaskQueue).to(p).inSingletonScope(),e.bind(r.ExtensionToolDelegator).to(g).inSingletonScope()})}function Fe(e){let t=new de({name:`browse-tool-chrome`,version:`0.1.0`},{capabilities:{tools:{}}}),n=e.getSupportedTools().map(e=>({name:e,description:`Browser automation tool (Chrome extension mode): ${e}`,inputSchema:{type:`object`,properties:{},additionalProperties:!0}}));return t.setRequestHandler(pe,async()=>({tools:n})),t.setRequestHandler(fe,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const Ie=new F(`chrome-serve`).description(`[DEPRECATED] Start MCP server with Chrome extension HTTP polling for bot-detection-free browser automation`).option(`-p, --port <port>`,`HTTP server port for extension polling`).option(`-v, --verbose`,`Enable verbose output`,!1).option(`--wait-for-extension`,`Wait for extension to connect before accepting MCP requests`,!1).action(async e=>{console.error(``),console.error(`╔════════════════════════════════════════════════════════════════╗`),console.error(`║ DEPRECATED: chrome-serve is deprecated. ║`),console.error(`║ Use mcp-serve instead - extension routes are now ║`),console.error(`║ automatically available in the HTTP server at /extension/*. ║`),console.error(`╚════════════════════════════════════════════════════════════════╝`),console.error(``);let t,n;try{let i=Te(e.port),a=je(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=ne(o,`processes.json`)),t=await Ne(a,i??D.min,i?{min:i,max:i}:D);let s=t.port;n=await te({repositoryPath:a,serviceName:De,serviceType:Oe,environment:ke,pid:process.pid,port:s,host:Ae,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:e.waitForExtension}}),e.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${s}`),console.error(` Wait for extension: ${e.waitForExtension}`));let c=new k({defaultScope:`Singleton`});c.load(Pe());let l=c.get(r.ExtensionTaskQueue),u=c.get(r.ExtensionToolDelegator),d=new I;d.use(`*`,_e({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=we(c);d.route(`/extension`,f),d.get(`/health`,e=>{let t=l.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let p=ge({fetch:d.fetch,port:s});p.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${s} is already in use.`),console.error(`Recovery: Try a different port with --port <port>`)):console.error(`Error [SERVER_ERROR]: HTTP server error: ${e.message}`),process.exit(1)}),console.error(`HTTP server started on port ${s}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${s}/extension/tasks`),e.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{l.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let m=Fe(u),h=new me;await m.connect(h),console.error(`Chrome extension MCP server started on stdio`);let g=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{l.clearAllTasks(`Server shutting down`),await h.close(),p.close(),await n.release({kill:!1}),await t.release(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>g(`SIGINT`)),process.on(`SIGTERM`,()=>g(`SIGTERM`))}catch(e){if(n||t)try{await n?.release(),await t?.release()}catch{}let r=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${r}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),Le=`BROWSE_TOOL_CONFIG`,Re=`.browse-tool`,ze=`config.json`,Be=[`json`,`text`,`quiet`],Ve={commands:{},tools:{}},He=N.record(N.string(),N.unknown()),Ue=N.object({mcpServe:N.object({type:N.string().optional(),browser:N.string().optional(),headless:N.boolean().optional(),profile:N.string().optional(),mode:N.string().optional(),host:N.string().optional(),port:N.coerce.number().int().positive().optional(),httpPort:N.coerce.number().int().positive().optional(),tags:N.string().optional(),exclude:N.string().optional(),customTools:N.string().optional(),chromeForTestingPath:N.string().optional(),snippetsDir:N.string().optional(),registryPath:N.string().optional(),registryDir:N.string().optional(),pidsDir:N.string().optional(),profilesDir:N.string().optional(),proxyConfigDir:N.string().optional()}).partial().optional(),httpServe:N.object({port:N.coerce.number().int().positive().optional(),headless:N.boolean().optional(),idleTimeout:N.coerce.number().positive().optional(),host:N.string().optional(),registryDir:N.string().optional(),registryPath:N.string().optional(),pidsDir:N.string().optional(),profilesDir:N.string().optional(),snippetsDir:N.string().optional(),proxyConfigDir:N.string().optional()}).partial().optional(),exec:N.object({format:N.enum(Be).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),tools:N.object({format:N.enum(Be).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),status:N.record(N.string(),N.unknown()).optional(),stop:N.record(N.string(),N.unknown()).optional()}),We=N.object({commands:Ue.default({}),tools:N.record(N.string(),He).default({})});let Ge={config:Ve};function Ke(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(n===`--config`)return e[t+1];if(n.startsWith(`--config=`))return n.slice(9)}}function qe(e){let t=A.resolve(e);if(!j(t))throw Error(`Config path not found: ${t}`);return ae(t).isDirectory()?A.join(t,ze):t}function Je(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??se(),a=Ke(e);if(a)return qe(a);if(n[Le])return qe(n[Le]);let o=A.join(r,Re,ze);if(j(o))return o;let s=A.join(i,Re,ze);if(j(s))return s}function Ye(e){let t=ie(e,`utf8`),n=JSON.parse(t);return We.parse(n)}function Xe(e,t={}){let n=Je(e,t);return n?{configPath:n,config:Ye(n)}:{config:Ve}}function Ze(e,t={}){return Ge=Xe(e,t),Ge}function z(e){return(Ge.config.commands??{})[e]??{}}function Qe(e){return Ge.config.tools?.[e]??{}}function B(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}var $e=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??u,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let e=await v().get(r.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!e.running||!e.port)throw Error(e.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=e.port,e.port}async getBaseUrl(){return`http://localhost:${await this.ensureServer()}`}async listTools(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/tools`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async listCustomTools(e){let t=await this.getBaseUrl(),n=new AbortController,r=setTimeout(()=>n.abort(),this.timeout);try{let r=new URL(`/custom-tools`,t);r.searchParams.set(`dir`,e);let i=await fetch(r,{method:`GET`,headers:{Accept:`application/json`},signal:n.signal});if(!i.ok)throw Error(`HTTP ${i.status}: ${i.statusText}`);let a=await i.json();if(a.error)throw Error(a.error);return a.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(r)}}async listBrowsers(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/browsers`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.browsers}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async execute(e,t){let n=await this.getBaseUrl(),r=e===`run_spec`?void 0:this.timeout,i=new AbortController,a=r===void 0?void 0:setTimeout(()=>i.abort(),r);try{let a=await fetch(`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:e,arguments:t}),signal:r===void 0?void 0:i.signal});if(!a.ok){let e=await a.text();throw Error(`HTTP ${a.status}: ${e||a.statusText}`)}let o=await a.json();return o.success?o.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:o.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{a&&clearTimeout(a)}}async executeCustomTool(e,t,n){let r=await this.getBaseUrl(),i=new AbortController,a=setTimeout(()=>i.abort(),this.timeout);try{let a=new URL(`/custom-tools`,r);a.searchParams.set(`dir`,e);let o=await fetch(a,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:t,arguments:n}),signal:i.signal});if(!o.ok){let e=await o.text();throw Error(`HTTP ${o.status}: ${e||o.statusText}`)}let s=await o.json();return s.success?s.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:s.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(a)}}wrapConnectionError(e){return e instanceof Error?e.message.includes(`ECONNREFUSED`)||e.message.includes(`fetch failed`)?Error(`Cannot connect to browse-tool HTTP server.\n\nTry one of the following:\n 1. Start the server: browse-tool http-serve\n 2. Check if another process is using port ${this.port}\n 3. Run with a different port: browse-tool --port 3201 <command>\n\nOriginal error: ${e.message}`,{cause:e}):e:Error(String(e),{cause:e})}};function V(e){return new $e(e)}const et={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function H(e,t,n){return n?`${et[t]}${e}${et.reset}`:e}function tt(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?nt(e):rt(e,r)}function nt(e){return JSON.stringify(e,null,2)}function rt(e,t){let n=[];e.isError&&n.push(H(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(it(r,t)):r.type===`image`?n.push(st(r,t)):n.push(H(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
2
|
+
import{C as e,D as t,E as n,O as r,S as i,T as a,_ as o,a as s,b as c,c as l,d as u,f as d,g as f,h as p,i as m,l as h,m as g,n as _,o as v,p as y,s as b,t as ee,u as x,v as S,w as C,x as w,y as T}from"./streamable-http-Cct_zaGc.mjs";import{stripTypeScriptTypes as E}from"node:module";import"reflect-metadata/lite";import{DEFAULT_PORT_RANGE as D,PortRegistryService as O}from"@agimon-ai/foundation-port-registry";import{createProcessLease as te,resolveSiblingRegistryPath as ne}from"@agimon-ai/foundation-process-registry";import{Container as k,ContainerModule as re}from"inversify";import A from"node:path";import{existsSync as j,readFileSync as ie,statSync as ae}from"node:fs";import{readFile as M,stat as oe}from"node:fs/promises";import{homedir as se}from"node:os";import{z as N}from"zod";import{SpanStatusCode as P}from"@opentelemetry/api";import{randomUUID as ce}from"node:crypto";import{coerceArgs as le,formatZodError as ue}from"@agimon-ai/foundation-validator";import{Server as de}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as fe,ListToolsRequestSchema as pe}from"@modelcontextprotocol/sdk/types.js";import{StdioServerTransport as me}from"@modelcontextprotocol/sdk/server/stdio.js";import{Command as F,Option as he}from"commander";import{serve as ge}from"@hono/node-server";import{Hono as I}from"hono";import{cors as _e}from"hono/cors";import{createNodeWebSocket as ve}from"@hono/node-ws";import{raw as ye}from"hono/html";import{Fragment as be}from"hono/jsx";import{jsx as L,jsxs as R}from"hono/jsx/jsx-runtime";var xe=`0.17.2`;const Se=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function Ce(e,t){if(!Se)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function we(e){let t=new I,n;try{n=e.get(r.TelemetryService)}catch{n=new f}return t.get(`/telemetry-config`,async e=>{try{let t=new URL(e.req.url).origin;return e.json(await o(process.env,t))}catch(t){return e.json({enabled:!1,error:t instanceof Error?t.message:String(t)},500)}}),t.get(`/tasks`,t=>{try{let n=e.get(r.ExtensionTaskQueue).getNextTask();return n?t.json({task:{id:n.id,tool:n.tool,arguments:n.arguments,telemetry:n.telemetry}}):t.json({})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/result`,async t=>{try{let n=await t.req.json();if(!n.taskId)return t.json({success:!1,error:`Missing taskId in request body`},400);let i=e.get(r.ExtensionTaskQueue),a=e.get(r.BrowserService),o={taskId:n.taskId,success:n.success,result:n.result,error:n.error},s=i.submitResult(o);return s?(s.browserId&&a.recordBrowserActivity(s.browserId,s.pageId),t.json({success:!0})):t.json({success:!1,error:`Task ${n.taskId} not found or already completed`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/status`,t=>{try{let n=e.get(r.ExtensionTaskQueue),i=n.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:n.queueSize};return t.json(a)}catch(e){return t.json({connected:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/register`,async t=>{try{let n=await t.req.json();if(!n.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).register(n);return t.json({success:!0,session:{id:i.id,browserId:i.browserId,controlMode:i.controlMode,createdAt:i.createdAt.toISOString()}})}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/heartbeat`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).heartbeat(n);return i?t.json({success:!0,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested,lastHeartbeatAt:i.lastHeartbeatAt.toISOString()}}):t.json({success:!1,error:`Session ${n.sessionId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/tab-mapped`,async t=>{try{let n=await t.req.json();if(!n.pageId||typeof n.tabId!=`number`)return t.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=e.get(r.PageRegistry),a=e.get(r.BrowserService),o=i.get(n.pageId);return o?(o.extensionTabId=n.tabId,a.recordBrowserActivity(o.browserId,n.pageId),t.json({success:!0})):t.json({success:!1,error:`Page ${n.pageId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/recording`,async t=>{try{let n=await t.req.json();if(!n.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(r.BrowserService),a=await i.persistExtensionRecordingArtifact(n.browserId,n.videoBase64);return Ce(`artifact received`,{browserId:n.browserId,videoBase64Size:n.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(n.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${n.browserId}" has no active recording target`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/recording/chunk`,async t=>{try{let i=await t.req.json();if(!i.browserId||!i.chunkBase64)return t.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=e.get(r.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return Ce(`chunk received`,{browserId:i.browserId,chunkIndex:i.chunkIndex,mimeType:i.mimeType,persisted:!!o,chunkBase64Size:i.chunkBase64.length}),o?(n.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":i.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof i.chunkIndex==`number`?{"browse_tool.recording.chunk_index":i.chunkIndex}:{},...typeof i.mimeType==`string`?{"browse_tool.recording.mime_type":i.mimeType}:{}}}),a.recordBrowserActivity(i.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${i.browserId}" has no active recording target`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/browser-log`,async e=>{try{let t=await e.req.json();return!t.message||typeof t.message!=`string`?e.json({success:!1,error:`Missing message in request body`},400):(n.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),Ce(`browser log relayed`,{level:t.level??`info`,message:t.message,attributes:t.attributes}),e.json({success:!0}))}catch(t){return e.json({success:!1,error:t instanceof Error?t.message:String(t)},500)}}),t.post(`/handoff`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).requestHandoff(n);return i?t.json({success:!0,message:`Handoff requested. AI will take control when ready.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):t.json({success:!1,error:`Session ${n.sessionId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/handoff/acknowledge`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).acknowledgeHandoff(n.sessionId);return i?t.json({success:!0,message:`Handoff acknowledged. AI now has control.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):t.json({success:!1,error:`Session ${n.sessionId} not found or no handoff pending`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/sessions`,t=>{try{let n=e.get(r.ExtensionSessionRegistry).listSessions();return t.json({sessions:n.map(e=>({id:e.id,browserId:e.browserId,tabId:e.tabId,currentUrl:e.currentUrl,controlMode:e.controlMode,activeSpecPath:e.activeSpecPath,handoffRequested:e.handoffRequested,createdAt:e.createdAt.toISOString(),lastHeartbeatAt:e.lastHeartbeatAt.toISOString()}))})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t}function Te(e){if(e==null||e===``)return;let t=typeof e==`number`?e:typeof e==`string`?Number.parseInt(e,10):NaN;if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}const Ee=[`pnpm-workspace.yaml`,`nx.json`,`.git`],De=`browse-tool-chrome`,Oe=`tool`,ke=process.env.NODE_ENV||`development`,Ae=`127.0.0.1`;function je(e=process.cwd()){let t=A.resolve(e);for(;;){for(let e of Ee)if(j(A.join(t,e)))return t;let e=A.dirname(t);if(e===t)return process.cwd();t=e}}function Me(){return new O(process.env.PORT_REGISTRY_PATH)}async function Ne(e,t,n){let r=Me(),i=await r.reservePort({repositoryPath:e,serviceName:De,serviceType:Oe,environment:ke,preferredPort:t,portRange:n,pid:process.pid,host:Ae,force:!0,metadata:{transport:`stdio`,mode:`chrome-serve`}});if(!i.success||!i.record)throw Error(i.error||`Failed to reserve port ${t}`);let a=!1;return{port:i.record.port,release:async()=>{if(a)return;a=!0;let n=await r.releasePort({repositoryPath:e,serviceName:De,serviceType:Oe,environment:ke,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function Pe(){return new re(e=>{e.bind(r.ExtensionTaskQueue).to(p).inSingletonScope(),e.bind(r.ExtensionToolDelegator).to(g).inSingletonScope()})}function Fe(e){let t=new de({name:`browse-tool-chrome`,version:`0.1.0`},{capabilities:{tools:{}}}),n=e.getSupportedTools().map(e=>({name:e,description:`Browser automation tool (Chrome extension mode): ${e}`,inputSchema:{type:`object`,properties:{},additionalProperties:!0}}));return t.setRequestHandler(pe,async()=>({tools:n})),t.setRequestHandler(fe,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const Ie=new F(`chrome-serve`).description(`[DEPRECATED] Start MCP server with Chrome extension HTTP polling for bot-detection-free browser automation`).option(`-p, --port <port>`,`HTTP server port for extension polling`).option(`-v, --verbose`,`Enable verbose output`,!1).option(`--wait-for-extension`,`Wait for extension to connect before accepting MCP requests`,!1).action(async e=>{console.error(``),console.error(`╔════════════════════════════════════════════════════════════════╗`),console.error(`║ DEPRECATED: chrome-serve is deprecated. ║`),console.error(`║ Use mcp-serve instead - extension routes are now ║`),console.error(`║ automatically available in the HTTP server at /extension/*. ║`),console.error(`╚════════════════════════════════════════════════════════════════╝`),console.error(``);let t,n;try{let i=Te(e.port),a=je(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=ne(o,`processes.json`)),t=await Ne(a,i??D.min,i?{min:i,max:i}:D);let s=t.port;n=await te({repositoryPath:a,serviceName:De,serviceType:Oe,environment:ke,pid:process.pid,port:s,host:Ae,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:e.waitForExtension}}),e.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${s}`),console.error(` Wait for extension: ${e.waitForExtension}`));let c=new k({defaultScope:`Singleton`});c.load(Pe());let l=c.get(r.ExtensionTaskQueue),u=c.get(r.ExtensionToolDelegator),d=new I;d.use(`*`,_e({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=we(c);d.route(`/extension`,f),d.get(`/health`,e=>{let t=l.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let p=ge({fetch:d.fetch,port:s});p.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${s} is already in use.`),console.error(`Recovery: Try a different port with --port <port>`)):console.error(`Error [SERVER_ERROR]: HTTP server error: ${e.message}`),process.exit(1)}),console.error(`HTTP server started on port ${s}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${s}/extension/tasks`),e.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{l.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let m=Fe(u),h=new me;await m.connect(h),console.error(`Chrome extension MCP server started on stdio`);let g=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{l.clearAllTasks(`Server shutting down`),await h.close(),p.close(),await n.release({kill:!1}),await t.release(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>g(`SIGINT`)),process.on(`SIGTERM`,()=>g(`SIGTERM`))}catch(e){if(n||t)try{await n?.release(),await t?.release()}catch{}let r=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${r}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),Le=`BROWSE_TOOL_CONFIG`,Re=`.browse-tool`,ze=`config.json`,Be=[`json`,`text`,`quiet`],Ve={commands:{},tools:{}},He=N.record(N.string(),N.unknown()),Ue=N.object({mcpServe:N.object({type:N.string().optional(),browser:N.string().optional(),headless:N.boolean().optional(),profile:N.string().optional(),mode:N.string().optional(),host:N.string().optional(),port:N.coerce.number().int().positive().optional(),httpPort:N.coerce.number().int().positive().optional(),tags:N.string().optional(),exclude:N.string().optional(),customTools:N.string().optional(),chromeForTestingPath:N.string().optional(),snippetsDir:N.string().optional(),registryPath:N.string().optional(),registryDir:N.string().optional(),pidsDir:N.string().optional(),profilesDir:N.string().optional(),proxyConfigDir:N.string().optional()}).partial().optional(),httpServe:N.object({port:N.coerce.number().int().positive().optional(),headless:N.boolean().optional(),idleTimeout:N.coerce.number().positive().optional(),host:N.string().optional(),registryDir:N.string().optional(),registryPath:N.string().optional(),pidsDir:N.string().optional(),profilesDir:N.string().optional(),snippetsDir:N.string().optional(),proxyConfigDir:N.string().optional()}).partial().optional(),exec:N.object({format:N.enum(Be).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),tools:N.object({format:N.enum(Be).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),status:N.record(N.string(),N.unknown()).optional(),stop:N.record(N.string(),N.unknown()).optional()}),We=N.object({commands:Ue.default({}),tools:N.record(N.string(),He).default({})});let Ge={config:Ve};function Ke(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(n===`--config`)return e[t+1];if(n.startsWith(`--config=`))return n.slice(9)}}function qe(e){let t=A.resolve(e);if(!j(t))throw Error(`Config path not found: ${t}`);return ae(t).isDirectory()?A.join(t,ze):t}function Je(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??se(),a=Ke(e);if(a)return qe(a);if(n[Le])return qe(n[Le]);let o=A.join(r,Re,ze);if(j(o))return o;let s=A.join(i,Re,ze);if(j(s))return s}function Ye(e){let t=ie(e,`utf8`),n=JSON.parse(t);return We.parse(n)}function Xe(e,t={}){let n=Je(e,t);return n?{configPath:n,config:Ye(n)}:{config:Ve}}function Ze(e,t={}){return Ge=Xe(e,t),Ge}function z(e){return(Ge.config.commands??{})[e]??{}}function Qe(e){return Ge.config.tools?.[e]??{}}function B(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}var $e=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??u,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let e=await v().get(r.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!e.running||!e.port)throw Error(e.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=e.port,e.port}async getBaseUrl(){return`http://localhost:${await this.ensureServer()}`}async listTools(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/tools`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async listCustomTools(e){let t=await this.getBaseUrl(),n=new AbortController,r=setTimeout(()=>n.abort(),this.timeout);try{let r=new URL(`/custom-tools`,t);r.searchParams.set(`dir`,e);let i=await fetch(r,{method:`GET`,headers:{Accept:`application/json`},signal:n.signal});if(!i.ok)throw Error(`HTTP ${i.status}: ${i.statusText}`);let a=await i.json();if(a.error)throw Error(a.error);return a.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(r)}}async listBrowsers(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/browsers`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.browsers}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async execute(e,t){let n=await this.getBaseUrl(),r=e===`run_spec`?void 0:this.timeout,i=new AbortController,a=r===void 0?void 0:setTimeout(()=>i.abort(),r);try{let a=await fetch(`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:e,arguments:t}),signal:r===void 0?void 0:i.signal});if(!a.ok){let e=await a.text();throw Error(`HTTP ${a.status}: ${e||a.statusText}`)}let o=await a.json();return o.success?o.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:o.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{a&&clearTimeout(a)}}async executeCustomTool(e,t,n){let r=await this.getBaseUrl(),i=new AbortController,a=setTimeout(()=>i.abort(),this.timeout);try{let a=new URL(`/custom-tools`,r);a.searchParams.set(`dir`,e);let o=await fetch(a,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:t,arguments:n}),signal:i.signal});if(!o.ok){let e=await o.text();throw Error(`HTTP ${o.status}: ${e||o.statusText}`)}let s=await o.json();return s.success?s.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:s.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(a)}}wrapConnectionError(e){return e instanceof Error?e.message.includes(`ECONNREFUSED`)||e.message.includes(`fetch failed`)?Error(`Cannot connect to browse-tool HTTP server.\n\nTry one of the following:\n 1. Start the server: browse-tool http-serve\n 2. Check if another process is using port ${this.port}\n 3. Run with a different port: browse-tool --port 3201 <command>\n\nOriginal error: ${e.message}`,{cause:e}):e:Error(String(e),{cause:e})}};function V(e){return new $e(e)}const et={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function H(e,t,n){return n?`${et[t]}${e}${et.reset}`:e}function tt(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?nt(e):rt(e,r)}function nt(e){return JSON.stringify(e,null,2)}function rt(e,t){let n=[];e.isError&&n.push(H(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(it(r,t)):r.type===`image`?n.push(st(r,t)):n.push(H(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
3
3
|
`)}function it(e,t){let n=e.text;try{return at(JSON.parse(n),t)}catch{return n}}function at(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=H(r,`cyan`,t),a=ot(i,t);n.push(`${e}: ${a}`)}return n.join(`
|
|
4
4
|
`)}function ot(e,t){return e===null?H(`null`,`gray`,t):e===void 0?H(`undefined`,`gray`,t):typeof e==`boolean`?H(String(e),e?`green`:`red`,t):typeof e==`number`?H(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?H(e,`blue`,t):e:Array.isArray(e)?e.length===0?H(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function st(e,t){let{mimeType:n,data:r}=e;return H(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function U(e,t){return H(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function ct(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=H(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
|
|
5
5
|
`)}function lt(e,t){let n=z(`tools`),r=B(e,`format`,t.format,n.format),i=B(e,`color`,t.color,n.color);return{port:B(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const ut=new F(`list-custom-tools`).description(`List custom tools from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(u)).action(async function(e,t){let{port:n,formatterOptions:r}=lt(this,t);try{let t=await V({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?ct(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(U(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),dt=new F(`exec-custom-tool`).description(`Execute a custom tool from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).argument(`<tool>`,`Custom tool name to execute`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(u)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=lt(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(U(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await V({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=tt(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(U(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new F(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(ut).addCommand(dt);const ft=new F(`docker-build-cft`).description(`Build the Chrome for Testing Docker image used by vm mode`).option(`--cft-version <version>`,`Chrome for Testing version to build`,t).option(`--image <image>`,`Docker image tag to produce`,a).option(`--platform <platform>`,`Docker target platform`,n).action(async t=>{try{let n=await i({version:t.cftVersion,image:t.image,platform:t.platform,stdio:`inherit`});console.log(`Built Docker image ${n.image}`),console.log(` Version: ${n.version}`),console.log(` Platform: ${n.platform}`),console.log(` Archive: ${e(n.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),pt=new F(`exec`).description(`Execute a tool directly with JSON arguments`).argument(`<tool>`,`Tool name to execute (e.g., browser_launch)`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(u)).action(async function(e,t,n){let r=z(`exec`),i=B(this,`format`,n.format,r.format),a=B(this,`color`,n.color,r.color),o=B(this,`port`,n.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),s=this.getOptionValueSourceWithGlobals(`port`),c=s!==void 0&&s!==`default`&&s!==`implied`,l={format:i,color:a};try{let n;try{n=JSON.parse(t)}catch{console.error(U(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await V({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=tt(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(U(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),W=`pageId`,G=`browserId`,mt=N.record(N.string(),N.unknown()),ht=N.object({type:N.literal(`object`),properties:N.record(N.string(),N.unknown()).optional(),required:N.array(N.string()).optional(),additionalProperties:N.boolean().optional()}).passthrough(),gt=N.object({name:N.string().min(1),description:N.string().min(1),script:N.string().min(1),suggestionActions:N.string().min(1).optional(),capabilities:mt,inputSchema:ht}),_t=N.object({tools:N.array(gt)}),vt=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function yt(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function bt(e,t){if(vt){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function K(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function xt(e){return K(e)&&Array.isArray(e.content)}function q(e){let t=e.trim();return t===``?``:t.startsWith(`"`)&&t.endsWith(`"`)||t.startsWith(`[`)&&t.endsWith(`]`)||t.startsWith(`{`)&&t.endsWith(`}`)?JSON.parse(t):t.startsWith(`'`)&&t.endsWith(`'`)?t.slice(1,-1):t===`true`?!0:t===`false`?!1:t===`null`?null:/^-?\d+(\.\d+)?$/.test(t)?Number(t):t}function St(e){if(!e||Object.keys(e).length===0)return;let t={};for(let[n,r]of Object.entries(e))if(r!=null){if(typeof r==`string`||typeof r==`number`||typeof r==`boolean`){t[n]=r;continue}t[n]=JSON.stringify(r)}return Object.keys(t).length>0?t:void 0}function Ct(e){return e.replace(/^\uFEFF/,``).split(/\r?\n/).map(e=>{if(e.includes(` `))throw Error(`Tab indentation is not supported in tools.yaml`);let t=e.replace(/\s+#.*$/,``);return t.trim().length===0?null:{indent:t.match(/^ */)?.[0].length??0,text:t.trim()}}).filter(e=>e!==null)}function J(e,t,n){let r=e[t];if(!r||r.indent!==n)throw Error(`Invalid indentation in tools.yaml at line ${t+1}`);return r.text.startsWith(`-`)?wt(e,t,n):Tt(e,t,n)}function wt(e,t,n){let r=[],i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||!t.text.startsWith(`-`))break;let a=t.text.slice(1).trim();if(a===``){let t=e[i+1];if(!t||t.indent<=n){r.push(null),i+=1;continue}let[a,o]=J(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=Et(a),s={};if(o===void 0){let r=e[i+1];if(!r||r.indent<=n)throw Error(`Expected nested value for "${t}" in tools.yaml`);let[a,o]=J(e,i+1,r.indent);s[t]=a,i=o}else s[t]=q(o),i+=1;for(;i<e.length&&e[i].indent>n;){let t=e[i];if(t.indent!==n+2||t.text.startsWith(`-`)){let[n,r]=J(e,i,t.indent);if(!K(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=Et(t.text);if(a===void 0){let n=e[i+1];if(!n||n.indent<=t.indent)throw Error(`Expected nested value for "${r}" in tools.yaml`);let[a,o]=J(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=q(a),i+=1}r.push(s);continue}r.push(q(a)),i+=1}return[r,i]}function Tt(e,t,n){let r={},i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||t.text.startsWith(`-`))break;let[a,o]=Et(t.text);if(o===void 0){let t=e[i+1];if(!t||t.indent<=n){r[a]=null,i+=1;continue}let[o,s]=J(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=q(o),i+=1}return[r,i]}function Et(e){let t=e.indexOf(`:`);if(t===-1)throw Error(`Invalid tools.yaml entry: "${e}"`);let n=e.slice(0,t).trim(),r=e.slice(t+1).trim();return[n,r===``?void 0:r]}function Dt(e){let t=Ct(e);if(t.length===0)return{};let[n]=J(t,0,t[0].indent);return n}function Ot(e){e.inputSchema.properties===void 0&&(e.inputSchema.properties={});let t=e.inputSchema.properties;if(!K(t))throw Error(`Custom tool "${e.name}" inputSchema.properties must be an object`);let n=t[W],r=t[G];if(n!==void 0&&!(K(n)&&n.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!(K(r)&&r.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`);n===void 0&&(t[W]={type:`string`,description:`Browse-tool page ID to run this custom tool against. Either pageId or browserId is required at call time.`}),r===void 0&&(t[G]={type:`string`,description:`Browse-tool browser ID to run this custom tool against when no pageId is supplied. The tool will use the browser's current page or open a new one.`})}async function kt(e){let t=E(await M(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function At(e,t){let n=typeof t.run==`function`?t.run:void 0;if(!n)throw Error(`Custom tool script "${e}" must export a "run" function`);return{execute:n}}function jt(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function Mt(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(K(r)){let i=jt(r,t);return{...e,content:[{...n,text:JSON.stringify(i,null,2)},...e.content.slice(1)]}}}catch{}return{...e,content:[...e.content,{type:`text`,text:`suggestionActions: ${t}`}]}}function Nt(e,t,n){if(xt(t))return n?Mt(t,n):t;if(typeof t==`string`)return n?{content:[{type:`text`,text:JSON.stringify({result:t,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:t}]};if(t===void 0)return n?{content:[{type:`text`,text:JSON.stringify({result:`Custom tool "${e}" completed successfully`,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:`Custom tool "${e}" completed successfully`}]};let r=K(t)&&n?jt(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var Pt=class{constructor(e,t,n=new f,r){this.pageRegistry=e,this.extensionTaskQueue=t,this.telemetry=n,this.browserService=r}resolveToolPage(e,t,n){if(n.page)return n.page;if(n.mode===`extension`&&this.extensionTaskQueue){let e=new S(this.extensionTaskQueue);return e.setTarget(t,n.browserId),e}throw Error(`Custom tool "${e}" requires a supported page context`)}toPageSummary(e,t){return{pageId:t.id,url:t.url,title:t.title,active:e===t.id}}async createPageForBrowser(e,t,n){if(!this.browserService)throw Error(`Custom tool "${e}" requires browser service support to create a page`);let r=this.browserService.getBrowser(t);if(!r)throw Error(`Browser "${t}" not found`);let i=n?.setAsCurrent!==!1;if(r.mode===`extension`||r.mode===`vm`){if(!this.extensionTaskQueue)throw Error(`Custom tool "${e}" requires extension task support to create a page`);let a=this.pageRegistry.registerExtensionPage(t,void 0,n?.url,!1);try{let o=await this.extensionTaskQueue.queueTask(`browser_new_page`,{browserId:t,pageId:a,url:n?.url,setAsCurrent:i},1e4,t);if(!o.success)throw Error(o.error??`Custom tool "${e}" failed to create a page`);let s=o.result?.content[0]?.type===`text`?o.result.content[0].text:void 0,c={};if(typeof s==`string`&&s.length>0)try{c=JSON.parse(s)}catch{c={}}let l=this.pageRegistry.get(a);if(!l)throw Error(`Page "${a}" was not registered`);l.url=c.url??l.url,l.title=c.title??l.title,l.extensionTabId=c.tabId??l.extensionTabId;let u=await this.waitForResolvedPageMetadata(a);return r.pageIds.add(a),(i||!r.currentPageId)&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a),{...this.toPageSummary(r.currentPageId,u),page:this.resolveToolPage(e,a,u)}}catch(e){throw this.pageRegistry.remove(a),e}}let{pageId:a,page:o}=await this.browserService.newPage(t);n?.url&&(await o.goto(n.url),await this.pageRegistry.updateMetadata(a)),i&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a);let s=this.pageRegistry.get(a);if(!s)throw Error(`Page "${a}" was not registered`);return{...this.toPageSummary(r.currentPageId,s),page:this.resolveToolPage(e,a,s)}}async waitForResolvedPageMetadata(e){let t=Date.now(),n=this.pageRegistry.get(e);for(;n&&Date.now()-t<1500;){let t=typeof n.url==`string`&&n.url.length>0,r=typeof n.title==`string`&&n.title.length>0&&n.title!==`Extension Tab`;if(t&&r)return n;await new Promise(e=>setTimeout(e,50)),n=this.pageRegistry.get(e)}if(!n)throw Error(`Page "${e}" was not registered`);return n}async resolveExecutionContext(e,t){let n=typeof t[W]==`string`&&t[W].length>0?t[W]:void 0,r=typeof t[G]==`string`&&t[G].length>0?t[G]:void 0;if(!n&&!r)throw Error(`Custom tool "${e}" requires a string pageId or browserId`);if(n){let t=this.pageRegistry.get(n);if(!t)throw Error(`Page "${n}" not found`);let i=c();if(i&&t.ownerId&&t.ownerId!==i)throw Error(`Custom tool "${e}" cannot use page "${n}" owned by another session`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let a=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:a}}let i=this.createBrowserHelper(e,r);try{let e=await i.getCurrentPage(),t=this.pageRegistry.get(e.pageId);if(!t)throw Error(`Page "${e.pageId}" not found`);return{pageId:e.pageId,pageEntry:t,page:e.page,browser:i}}catch(e){if(!(e instanceof Error?e.message:String(e)).includes(`has no pages`))throw e;let t=await i.newPage(),n=this.pageRegistry.get(t.pageId);if(!n)throw Error(`Page "${t.pageId}" not found`,{cause:e});return{pageId:t.pageId,pageEntry:n,page:t.page,browser:i}}}createBrowserHelper(e,t){return{browserId:t,mode:this.browserService?.getBrowser(t)?.mode??`extension`,listPages:async()=>{let e=this.browserService?.getBrowser(t);if(!e)throw Error(`Browser "${t}" not found`);return this.pageRegistry.findByBrowser(t).map(t=>this.toPageSummary(e.currentPageId,t))},getPage:async n=>{let r=this.pageRegistry.get(n);if(!r||r.browserId!==t)throw Error(`Page "${n}" not found in browser "${t}"`);let i=this.browserService?.getBrowser(t);return{...this.toPageSummary(i?.currentPageId??null,r),page:this.resolveToolPage(e,n,r)}},getCurrentPage:async()=>{let n=this.browserService?.getBrowser(t);if(!n)throw Error(`Browser "${t}" not found`);let r=c(),i;if(r){let e=this.pageRegistry.findByOwner(r).filter(e=>e.browserId===t);if(i=e[e.length-1]?.id,!i)throw Error(`Browser "${t}" has no pages owned by this session`)}else i=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!i)throw Error(`Browser "${t}" has no pages`);let a=this.pageRegistry.get(i);if(!a)throw Error(`Page "${i}" not found`);return{...this.toPageSummary(n.currentPageId,a),page:this.resolveToolPage(e,i,a)}},newPage:async n=>this.createPageForBrowser(e,t,n)}}createToolLogger(e,t,n){let r={"browse_tool.tool.name":e,"browse_tool.page.id":t,"browse_tool.browser.id":n.browserId,"browse_tool.execution.mode":n.mode},i=(e,t,n)=>{this.telemetry.log(e,t,{attributes:St({...r,...n?.attributes??{}}),exception:n?.exception})};return{getTraceContext:()=>this.telemetry.getActiveTraceContext(),trace:(e,t)=>i(`trace`,e,t),debug:(e,t)=>i(`debug`,e,t),info:(e,t)=>i(`info`,e,t),warn:(e,t)=>i(`warn`,e,t),error:(e,t)=>i(`error`,e,t),fatal:(e,t)=>i(`fatal`,e,t)}}async listTools(e){return(await this.loadTools(e)).map(({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i})=>({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i}))}async executeTool(e,t,n){let r=typeof n[W]==`string`?n[W]:void 0,i=typeof n[G]==`string`?n[G]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":A.resolve(e),"browse_tool.page.id":r,"browse_tool.browser.id":i}},async r=>{let i=(await this.loadTools(e)).find(e=>e.name===t);if(!i)throw r?.setStatus({code:P.ERROR,message:`Custom tool "${t}" not found`}),Error(`Custom tool "${t}" not found`);let{pageId:a,pageEntry:o,page:s,browser:c}=await this.resolveExecutionContext(t,n),l=this.createToolLogger(t,a,o);r?.setAttributes({"browse_tool.browser.id":o.browserId,"browse_tool.execution.mode":o.mode,"browse_tool.page.id":a}),bt(`Executing custom tool`,{toolName:t,directory:A.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:yt(n),scriptPath:i.scriptPath});let u;try{u=await i.execute?.({page:s,browser:c,input:n,logger:l})}catch(e){let n=e instanceof Error?e.message:String(e);throw bt(`Custom tool execution failed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,error:n,stack:e instanceof Error?e.stack:void 0}),Error(`Custom tool "${t}" failed on page "${a}": ${n}`,{cause:e})}bt(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:yt(u)});let d=Nt(t,u,i.suggestionActions),f=d.isError?d.content[0]?.text:void 0;return f&&r?.setStatus({code:P.ERROR,message:f}),d})}async loadTools(e){let t=A.resolve(e),n=A.join(t,`tools.yaml`),r=Dt(await M(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=_t.parse(r);return Promise.all(i.tools.map(async e=>{Ot(e);let n=A.resolve(t,e.script);await oe(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=At(n,await kt(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function Ft(e){let t=new I;return t.get(`/browsers`,async t=>{try{let n=e.get(r.BrowserService),i=e.get(r.PageRegistry),a=n.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString(),pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt.toISOString()}))}}),c={totalBrowsers:a.length,totalPages:o.length};return t.json({browsers:s,stats:c})}catch(e){return console.error(`Failed to list browsers:`,e),t.json({error:`Failed to list browsers`,message:e instanceof Error?e.message:String(e)},500)}}),t.delete(`/browsers`,async t=>{try{return await e.get(r.BrowserService).closeAll(),t.json({success:!0,message:`All browsers closed`})}catch(e){return console.error(`Failed to close all browsers:`,e),t.json({error:`Failed to close all browsers`,message:e instanceof Error?e.message:String(e)},500)}}),t.delete(`/browsers/:id`,async t=>{try{let n=t.req.param(`id`),i=e.get(r.BrowserService);return i.getBrowser(n)?(await i.closeBrowser(n),t.json({success:!0,message:`Browser "${n}" closed`})):t.json({error:`Browser "${n}" not found`},404)}catch(e){return console.error(`Failed to close browser:`,e),t.json({error:`Failed to close browser`,message:e instanceof Error?e.message:String(e)},500)}}),t.delete(`/pages/:id`,async t=>{try{let n=t.req.param(`id`),i=e.get(r.PageRegistry).get(n);return i?i.page?(await i.page.close(),t.json({success:!0,message:`Page "${n}" closed`})):t.json({error:`Page "${n}" is in extension mode and cannot be closed via API`},400):t.json({error:`Page "${n}" not found`},404)}catch(e){return console.error(`Failed to close page:`,e),t.json({error:`Failed to close page`,message:e instanceof Error?e.message:String(e)},500)}}),t}function It(e){return e.toLocaleString(`en-US`,{year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1})}function Lt(e){let t=Math.floor(e/1e3),n=Math.floor(t/60),r=Math.floor(n/60);return r>0?`${r}h ${n%60}m`:n>0?`${n}m ${t%60}s`:`${t}s`}function Rt(e){return Lt(new Date().getTime()-e.getTime())}const zt=`table-container`,Bt=`stat-card`,Vt=`browser-row`,Ht=`page-row`,Y=`status-active`,Ut=`url-cell`,X=`timestamp-cell`,Wt=`actions-cell`,Z=`kill-btn`,Gt=`empty-state`;function Kt({browsers:e}){return e.length===0?L(`div`,{class:zt,children:R(`div`,{class:Gt,children:[L(`h3`,{children:`No Active Browsers`}),L(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):L(`div`,{class:zt,children:R(`table`,{class:`browser-table`,children:[L(`thead`,{children:R(`tr`,{children:[L(`th`,{children:`ID`}),L(`th`,{children:`Status`}),L(`th`,{children:`URL / Title`}),L(`th`,{children:`Created`}),L(`th`,{children:`Age`}),L(`th`,{children:`Actions`})]})}),L(`tbody`,{id:`browser-table-body`,children:e.map(e=>R(be,{children:[R(`tr`,{class:Vt,children:[L(`td`,{children:e.id}),L(`td`,{children:R(`span`,{class:Y,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),L(`td`,{children:e.profileName||`Default Profile`}),L(`td`,{class:X,children:It(e.createdAt)}),L(`td`,{class:X,children:Rt(e.createdAt)}),L(`td`,{class:Wt,children:L(`button`,{type:`button`,class:Z,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>R(`tr`,{class:Ht,children:[R(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),L(`td`,{children:L(`span`,{class:Y,children:`Open`})}),L(`td`,{class:Ut,title:t.url,children:t.title||t.url||`about:blank`}),L(`td`,{class:X,children:It(t.createdAt)}),L(`td`,{class:X,children:Rt(t.createdAt)}),L(`td`,{class:Wt,children:L(`button`,{type:`button`,class:Z,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]},t.id))]},e.id))})]})})}function qt({stats:e}){return R(`div`,{class:`stats-container`,children:[R(`div`,{class:Bt,children:[L(`h3`,{children:`Active Browsers`}),L(`div`,{class:`value`,children:e.totalBrowsers})]}),R(`div`,{class:Bt,children:[L(`h3`,{children:`Active Pages`}),L(`div`,{class:`value`,children:e.totalPages})]})]})}function Jt({browsers:e,stats:t}){return R(`div`,{class:`dashboard-container`,children:[R(`div`,{class:`dashboard-header`,children:[R(`div`,{children:[L(`h1`,{children:`Playwright MCP Dashboard`}),L(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),R(`div`,{children:[L(`button`,{type:`button`,id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),L(`button`,{type:`button`,id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),L(qt,{stats:t}),L(Kt,{browsers:e}),L(`script`,{children:ye(`
|
|
@@ -428,7 +428,7 @@ window.addEventListener('beforeunload', () => {
|
|
|
428
428
|
margin-bottom: 0.5rem;
|
|
429
429
|
color: #999;
|
|
430
430
|
}
|
|
431
|
-
`)})]}),L(`body`,{children:t})]})}function Xt(e){let t=new I;return t.get(`/`,async t=>{try{let n=e.get(r.BrowserService),i=e.get(r.PageRegistry),a=n.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt,pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt}))}}),c={totalBrowsers:a.length,totalPages:o.length};return t.html(L(Yt,{title:`Playwright MCP Dashboard`,children:L(Jt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),t.html(L(Yt,{title:`Playwright MCP Dashboard - Error`,children:R(`div`,{style:`padding: 2rem; text-align: center;`,children:[L(`h1`,{children:`Failed to load dashboard`}),L(`p`,{children:e instanceof Error?e.message:String(e)}),L(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),t}function Zt(e){if(!e.isError)return;let t=e.content[0];return t?.type===`text`&&typeof t.text==`string`?t.text:`Unknown tool execution error`}function Qt(e){try{return e.get(r.TelemetryService)}catch{return new f}}function $t(e){return e.startsWith(`browser-`)||e===`run-spec`||e===`discover-specs`?e.replaceAll(`-`,`_`):e}function en(e){let t=new I,n=e.get(r.PageRegistry),i=e.get(r.BrowserService),a=e.get(r.ExtensionTaskQueue),o=Qt(e),s=new Pt(n,a,o,i);t.use(`*`,_e({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`DELETE`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`,`Authorization`]})),t.get(`/health`,e=>{try{let t=i.listBrowsers(),n={status:`healthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,timestamp:new Date().toISOString(),browsers:{count:t.length,instances:t.map(e=>({id:e.id,pageCount:e.pageIds.size,createdAt:e.createdAt.toISOString()}))}};return e.json(n)}catch(t){return e.json({status:`unhealthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,error:t instanceof Error?t.message:String(t)},503)}}),t.post(`/execute`,async t=>{try{let i=await t.req.json();if(!i.tool)return t.json({success:!1,error:`Missing "tool" field in request body`},400);let a=typeof i.arguments?.pageId==`string`?i.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.execute`,{attributes:{"http.method":`POST`,"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":a}},async a=>{let s=e.getAll(r.Tool),c=$t(i.tool),l=s.find(e=>e.getDefinition().name===c);if(process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`&&i.tool===`browser_launch`){let e=typeof i.arguments?.videoDir==`string`?i.arguments.videoDir:typeof i.arguments?.video_dir==`string`?i.arguments.video_dir:``;console.log(`[ExtensionRecordingDebug] http execute browser_launch mode=${String(i.arguments?.mode??``)} videoDir=${e}`)}if(!l)return a?.setStatus({code:P.ERROR,message:`Tool "${i.tool}" not found`}),o.log(`warn`,`browse-tool HTTP execute rejected unknown tool`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool}}),t.json({success:!1,error:`Tool "${i.tool}" not found`},404);let u=i.arguments||{},d;try{d=l.getInputSchema().parse(le(u,l.getInputSchema()))}catch(e){if(e instanceof N.ZodError){let n=ue(e,{schemaName:c,schema:l.getInputSchema()});return a?.setStatus({code:P.ERROR,message:n}),t.json({success:!1,result:{content:[{type:`text`,text:n}],isError:!0},error:n})}throw e}let f=await w(t.req.header(T),()=>l.execute(d)),p=d,m=typeof p.pageId==`string`?p.pageId:void 0;if(m){let t=e.get(r.BrowserService),i=n.get(m);i&&t.recordBrowserActivity(i.browserId,m)}let h=Zt(f);return h?(a?.setStatus({code:P.ERROR,message:h}),o.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":c,"browse_tool.page.id":m}})):o.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":c,"browse_tool.page.id":m}}),t.json({success:!f.isError,result:f,error:h})})}catch(e){return o.log(`error`,`browse-tool HTTP execute request crashed`,{attributes:{"http.route":`/execute`},exception:e}),t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/browsers`,t=>{try{let n=e.get(r.BrowserService).listBrowsers();return t.json({browsers:n.map(e=>({id:e.id,profileName:e.profileName,pageIds:Array.from(e.pageIds),currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString()}))})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/tools`,t=>{try{let n=e.getAll(r.Tool);return t.json({tools:n.map(e=>e.getDefinition())})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/custom-tools`,async e=>{let t=e.req.query(`dir`);if(!t)return e.json({tools:[],error:`Missing "dir" query parameter`},400);try{let n=await s.listTools(t);return e.json({tools:n})}catch(t){return e.json({tools:[],error:t instanceof Error?t.message:String(t)},400)}}),t.post(`/custom-tools`,async t=>{let i=t.req.query(`dir`);if(!i)return t.json({success:!1,error:`Missing "dir" query parameter`},400);try{let a=await t.req.json();if(!a.tool)return t.json({success:!1,error:`Missing "tool" field in request body`},400);let c=typeof a.arguments?.pageId==`string`?a.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.custom_tool.execute`,{attributes:{"http.method":`POST`,"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c,"browse_tool.custom_tools.directory":i}},async l=>{let u=await w(t.req.header(T),()=>s.executeTool(i,a.tool,a.arguments||{}));if(c){let t=e.get(r.BrowserService),i=n.get(c);i&&t.recordBrowserActivity(i.browserId,c)}let d=Zt(u);return d?(l?.setStatus({code:P.ERROR,message:d}),o.log(`error`,`browse-tool custom tool execution failed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}})):o.log(`info`,`browse-tool custom tool execution succeeded`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}}),t.json({success:!u.isError,result:u,error:d})})}catch(e){return o.log(`error`,`browse-tool custom tool request crashed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.custom_tools.directory":i},exception:e}),t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}});let c=we(e);t.route(`/extension`,c);let l=Ft(e);t.route(`/api`,l);let u=Xt(e);return t.route(`/`,u),t}function tn(e,t,n){e.get(`/ws/extension/:browserId`,n(e=>{let n=e.req.param(`browserId`),i=null;return{onOpen(e,a){if(!n){a.close(1008,`Missing browserId`);return}try{i=t.get(r.WebSocketHub).addConnection(a,n)}catch{a.close(1011,`Internal server error`)}},onMessage(e){if(i)try{let n=t.get(r.WebSocketHub),a=typeof e.data==`string`?e.data:e.data.toString();n.handleMessage(i,a)}catch{}},onClose(){if(i)try{t.get(r.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{t.get(r.WebSocketHub).removeConnection(i)}catch{}}}})),e.get(`/ws/stats`,e=>{try{let n=t.get(r.WebSocketHub).getStats();return e.json({totalConnections:n.totalConnections,browserCount:n.browserCount,connections:n.connections.map(e=>({id:e.id,browserId:e.browserId,connectedAt:e.connectedAt.toISOString()}))})}catch(t){return e.json({error:t instanceof Error?t.message:String(t)},500)}})}const nn=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function rn(e=process.cwd()){let t=A.resolve(e);for(;;){for(let e of nn)if(j(A.join(t,e)))return t;let e=A.dirname(t);if(e===t)return process.cwd();t=e}}const an=new F(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(u)).option(`--host <host>`,`Host to bind`,y()).option(`--headless`,`Run browsers in headless mode by default`,!0).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before closing browsers`,`30`).option(`--registry-dir <path>`,`Custom registry path or directory for service discovery`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking (deprecated)`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--snippets-dir <path>`,`Directory used by browser_run_code to save and load reusable snippets`).option(`--proxy-config-dir <path>`,`Custom proxy config directory`).action(async function(e){let t=z(`httpServe`),n={port:B(this,`port`,e.port,t.port===void 0?void 0:String(t.port),process.env.PLAYWRIGHT_PORT),headless:B(this,`headless`,e.headless,t.headless),idleTimeout:B(this,`idleTimeout`,e.idleTimeout,t.idleTimeout===void 0?void 0:String(t.idleTimeout),process.env[x]),host:B(this,`host`,e.host,t.host,process.env.PLAYWRIGHT_HOST),registryDir:B(this,`registryDir`,e.registryDir,t.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:B(this,`registryPath`,e.registryPath,t.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:B(this,`pidsDir`,e.pidsDir,t.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:B(this,`profilesDir`,e.profilesDir,t.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:B(this,`snippetsDir`,e.snippetsDir,t.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR),proxyConfigDir:B(this,`proxyConfigDir`,e.proxyConfigDir,t.proxyConfigDir,process.env[b])},i;try{let e=Number.parseInt(n.port,10),t=`browse-tool-http`,a=process.env.NODE_ENV||`development`,o=rn(process.cwd()),c=n.registryPath||n.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;c&&(process.env.PLAYWRIGHT_REGISTRY_DIR=c,process.env.PORT_REGISTRY_PATH=c,process.env.PROCESS_REGISTRY_PATH=ne(c,`processes.json`)),process.env.PLAYWRIGHT_HOST=n.host,process.env.PLAYWRIGHT_PORT=n.port,process.env[x]=n.idleTimeout,n.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=n.pidsDir),n.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=n.profilesDir),n.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=A.resolve(n.snippetsDir)),n.proxyConfigDir&&(process.env[b]=A.resolve(n.proxyConfigDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${e}`),console.log(` Host: ${n.host}`),console.log(` Headless: ${n.headless}`),console.log(` Idle Timeout: ${n.idleTimeout} minutes`),process.env.BROWSE_TOOL_SNIPPETS_DIR&&console.log(` Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`),process.env.BROWSE_TOOL_PROXY_CONFIG_DIR&&console.log(` Proxy Config Dir: ${process.env[b]}`);let l=s(),u=l.get(r.ProcessRegistryService),f=en(l),{injectWebSocket:p,upgradeWebSocket:m}=ve({app:f});tn(f,l,m);let h=l.get(r.WebSocketHub),g=l.get(r.ExtensionTaskQueue),_=l.get(r.BrowserService),v=l.get(r.ExtensionSessionRegistry),y=l.get(r.PageRegistry),ee=l.get(r.IdleCleanupService);ee.start(),h.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=_.getBrowser(n),i,a;if(r&&(r.mode===`extension`||r.mode===`vm`)){i=r.id;let t=y.findByBrowser(i);a=r.currentPageId||t[0]?.id||``,h.reassignConnection(e.id,i),a||(a=y.registerExtensionPage(i),r.pageIds.add(a),r.currentPageId=a),console.log(`[WebSocket] Extension connected to existing browser: ${i}, pageId: ${a}`)}else{i=n,_.registerExtensionBrowserWithId(i),a=y.registerExtensionPage(i);let e=_.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=y.get(a);o&&(o.extensionTabId=t.payload.tabId,o.url=t.payload.url??o.url);let s=v.register({browserId:i,tabId:t.payload.tabId,url:t.payload.url,metadata:{transport:`websocket`}});h.sendSessionAck(e,t.id??``,s.id,s.controlMode),h.broadcastPageCreated(i,a,y.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=g.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&_.recordBrowserActivity(e.browserId,e.pageId)}},onTabMapped:(e,t)=>{if(t.type===`tab:mapped`){let e=y.get(t.payload.pageId);if(!e)return;e.extensionTabId=t.payload.tabId,_.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&v.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let S=new O(process.env.PORT_REGISTRY_PATH);console.log(c?` Registry path: ${c}`:` Registry path: default (~/.port-registry/ports.json)`);let C=await S.reservePort({repositoryPath:o,serviceName:t,serviceType:`tool`,environment:a,preferredPort:e,pid:process.pid,host:n.host,force:!0,portRange:D,metadata:{headless:n.headless,idleTimeout:n.idleTimeout}});if(!C.success||C.port===void 0)throw Error(C.error||`Failed to reserve port in range ${D.min}-${D.max}`);let w=C.port;C.record.metadata={...C.record.metadata,healthCheckUrl:`${d(n.host,w)}/health`};let T=ge({fetch:f.fetch,port:w,hostname:n.host});p(T),i=await te({repositoryPath:o,serviceName:t,serviceType:`service`,environment:a,pid:process.pid,port:w,host:n.host,metadata:{healthCheckUrl:`${d(n.host,w)}/health`,headless:n.headless,idleTimeout:n.idleTimeout,transport:`http`}},u);let E=d(n.host,w);console.log(`HTTP server listening on ${E}`),console.log(` Health check: ${E}/health`),console.log(` Execute tool: POST ${E}/execute`),console.log(`
|
|
432
|
-
Press Ctrl+C to stop the server`);let k=async e=>{console.log(`\n\n${e} received. Shutting down gracefully...`);try{ee.stop(),T.close(),console.log(`Server closed`);try{await l.get(r.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{i&&await i.release({kill:!1}),console.log(`Service deregistered`)}catch(e){console.error(`Process registry cleanup error:`,e)}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>k(`SIGINT`)),process.on(`SIGTERM`,()=>k(`SIGTERM`)),await new Promise(()=>{})}catch(e){if(i)try{await i.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),on={browser_click:[`input`],browser_fill:[`input`],browser_type:[`input`],browser_select:[`input`],browser_hover:[`input`],browser_drag:[`input`],browser_press_key:[`input`],browser_upload_file:[`input`],browser_navigate:[`navigation`],browser_go_back:[`navigation`],browser_go_forward:[`navigation`],browser_reload:[`navigation`],browser_wait_for:[`navigation`],browser_snapshot:[`snapshot`],browser_screenshot:[`snapshot`],browser_pdf:[`snapshot`],browser_list_pages:[`page`],browser_new_page:[`page`],browser_select_page:[`page`],browser_close_page:[`page`],browser_resize_page:[`page`,`emulation`],browser_handle_dialog:[`dialog`],browser_list_network_requests:[`network`],browser_get_network_request:[`network`],browser_list_console_messages:[`console`],browser_evaluate_script:[`script`],browser_emulate:[`emulation`],browser_expect:[`testing`],browser_start_trace:[`tracing`],browser_stop_trace:[`tracing`],browser_list_profiles:[`profile`],browser_delete_profile:[`profile`],run_spec:[`spec`,`testing`],discover_specs:[`spec`,`testing`],browser_launch:[`browser`],browser_close:[`browser`],browser_run_code:[`code`,`script`]};function sn(e){let t=new Set;for(let[n,r]of Object.entries(on))r.some(t=>e.includes(t))&&t.add(n);return t}const cn=3e3,ln=[`browser_launch`],un=[`browser_new_page`],dn=[`browser_close`],fn=[`browser_close_page`];function pn(e,t){let n=e.replaceAll(`-`,`_`);return t&&(n===`browser_list`||n===`browser_list_session`||n===`browser_list_sessions`)?`browser_list_session`:e.startsWith(`browser-`)||e===`run-spec`||e===`discover-specs`?n:e}function mn(e){if(!e?.content?.[0]?.text)return null;try{let t=JSON.parse(e.content[0].text);return{browserId:t.browserId,pageId:t.pageId,mode:t.mode,url:t.url,closedBrowserId:t.closedBrowserId,closedPageId:t.closedPageId,browserKeptOpen:t.browserKeptOpen}}catch{return null}}async function hn(e){await new Promise(t=>setTimeout(t,e))}async function gn(e){let t=new AbortController,n=setTimeout(()=>t.abort(),cn);try{let n=await fetch(`${e}/tools`,{signal:t.signal});if(!n.ok)throw Error(`HTTP error ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);if(!Array.isArray(r.tools))throw Error(`Invalid /tools response: missing tools array`);if(r.tools.length===0)throw Error(`HTTP server returned an empty tool list`);return r.tools}finally{clearTimeout(n)}}function _n(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function vn(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),cn);try{let r=await fetch(_n(e,t),{signal:n.signal});if(!r.ok)throw Error(`HTTP error ${r.status}: ${r.statusText}`);let i=await r.json();if(i.error)throw Error(i.error);if(!Array.isArray(i.tools))throw Error(`Invalid /custom-tools response: missing tools array`);return i.tools}finally{clearTimeout(r)}}async function yn(e){let t;for(let n=1;n<=4;n+=1)try{return await gn(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await hn(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function bn(e,t){let n;for(let r=1;r<=4;r+=1)try{return await vn(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await hn(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function xn(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?sn(e.tags):null,n=new Set(e.exclude??[]);if(!t)return n.size>0?n:null;for(let e of n)t.delete(e);return t}function Sn(e){let t=new Map,n=(e,n)=>{let r=t.get(e)??new Set;r.add(n),t.set(e,r)};return e.defaultMode&&n(`browser_launch`,`mode`),e.enforcedProfileName&&n(`browser_launch`,`profileName`),t}function Cn(e,t){let n=t.get(e.name);if(!n?.size)return e;let r=e.inputSchema;if(!r.properties||typeof r.properties!=`object`)return e;let i=Object.fromEntries(Object.entries(r.properties).filter(([e])=>!n.has(e))),a={...r,properties:i};return Array.isArray(r.required)&&(a.required=r.required.filter(e=>typeof e!=`string`||!n.has(e))),{...e,inputSchema:a}}function wn(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:r,toolFilter:i,customToolsDir:a,enforcedProfileName:o,proxyConfigDir:s,ownerId:c}=e,u=i?.tags?.length?xn(i):null,d=new Set(i?.exclude??[]),f=u!==null||d.size>0,p=Sn({defaultMode:r,enforcedProfileName:o}),m=new de({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),h={name:`browser_list_session`,description:`List all browsers and pages created in this MCP session. Use this to see what resources you have available.`,inputSchema:{type:`object`,properties:{},required:[],additionalProperties:!1}},g=[],_=[],v=new Set;function y(e){return f?e.filter(e=>d.has(e.name)?!1:u===null?!0:u.has(e.name)):e}function b(e){return e.filter(e=>!d.has(e.name))}function ee(e){return o?{...e,profileName:o}:e}function x(e){let t=l({proxy:e.proxy,profileName:typeof e.profileName==`string`?e.profileName:void 0,proxyConfigDir:s});if(!t){let t={...e};return delete t.proxy,t}return{...e,proxy:t}}function S(e){if(!o)return e;let t=e?.content?.[0]?.text;if(!t)return e;try{let n=JSON.parse(t),r=(Array.isArray(n.profiles)?n.profiles:[]).filter(e=>e?.name===o);return{...e,content:[{...e.content[0],text:JSON.stringify({...n,profileCount:r.length,profiles:r},null,2)},...e.content.slice(1)]}}catch{return e}}function C(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function w(e){return d.has(e)?!1:u===null?!0:u.has(e)}return m.setRequestHandler(pe,async()=>{try{let e=await yn(t);g=e,v=new Set(e.map(e=>e.name));let r=a?await bn(t,a):[];_=r;let i=[...y(e).map(e=>Cn(e,p)),...b(r).map(e=>C(e))];return n&&i.push(h),{tools:i}}catch(e){if(g.length>0||_.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[...y(g).map(e=>Cn(e,p)),...b(_).map(e=>C(e))];return n&&t.push(h),{tools:t}}let r=e instanceof Error?e.message:String(e);throw Error(`Failed to list tools from HTTP server at ${t}: ${r}`,{cause:e})}}),m.setRequestHandler(fe,async e=>{let{name:i,arguments:o}=e.params,s=pn(i,!!n),l=new Set(_.map(e=>e.name));if(s!==`browser_list_session`&&!w(s))return{content:[{type:`text`,text:`Tool "${s}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(s===`browser_list_session`&&n){let e=n.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let u=o||{};s===`browser_launch`&&r&&u.mode===void 0&&(u={...u,mode:r}),s===`browser_launch`&&(u=ee(u),u=x(u));try{if(a&&!l.has(s)&&!v.has(s))try{_=await bn(t,a),l=new Set(_.map(e=>e.name))}catch(e){console.error(`Failed to refresh custom tools, using cached list: ${e instanceof Error?e.message:String(e)}`)}let e=l.has(s),r=await fetch(e&&a?_n(t,a):`${t}/execute`,{method:`POST`,headers:c?{"Content-Type":`application/json`,[T]:c}:{"Content-Type":`application/json`},body:JSON.stringify({tool:s,arguments:u})});if(!r.ok){let e=await r.text();return{content:[{type:`text`,text:`HTTP error ${r.status}: ${e}`}],isError:!0}}let i=await r.json();if(!i.success)return{content:[{type:`text`,text:i.error||i.result?.content?.[0]?.text||`Unknown error from HTTP server`}],isError:!0};if(n&&i.result){if(ln.includes(s)){let e=mn(i.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if(un.includes(s)){let e=mn(i.result);e?.pageId&&e?.browserId&&n.trackPage(e.pageId,e.browserId,e.url)}else if(dn.includes(s)){let e=mn(i.result);e?.closedBrowserId&&n.untrackBrowser(e.closedBrowserId)}else if(fn.includes(s)){let e=mn(i.result);e?.closedPageId&&n.untrackPage(e.closedPageId),e?.browserId&&e.browserKeptOpen===!1&&n.untrackBrowser(e.browserId)}}return s===`browser_list_profiles`&&i.result?S(i.result):i.result||{content:[{type:`text`,text:`No result returned`}]}}catch(e){return{content:[{type:`text`,text:`Failed to execute tool via HTTP server: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),m}const Q=`stdio`,Tn=`http`,$=`streamable-http`,En=new Set([Q,Tn,$]),Dn=`chromium`,On=new Set([Dn,`firefox`,`webkit`]),kn=new Set([`playwright`,`extension`,`vm`,`stealth`]),An=`SIGINT`,jn=`SIGTERM`,Mn=`PLAYWRIGHT_PORT`,Nn=[`pnpm-workspace.yaml`,`nx.json`,`.git`],Pn=`browse-tool-mcp-http`,Fn=`service`,In=D.min;var Ln=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...En].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},Rn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...On].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},zn=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...kn].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function Bn(e){let t=e.toLowerCase();if(!On.has(t))throw new Rn(e);return t}function Vn(e){let t=e.toLowerCase();if(!kn.has(t))throw new zn(e);return t}function Hn(e){let t=typeof e==`number`?e:Number.parseInt(e,10);if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}function Un(e){return e!=null&&String(e).trim()!==``}function Wn(e){let t=e.toLowerCase();return t===Tn?$:t}function Gn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function Kn(e=process.cwd()){let t=A.resolve(e);for(;;){for(let e of Nn)if(j(A.join(t,e)))return t;let e=A.dirname(t);if(e===t)return process.cwd();t=e}}function qn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function Jn(e){let t=e.tags?qn(e.tags):void 0,n=e.exclude?qn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var Yn=class extends Error{code=`BROWSER_CLEANUP_FAILED`;recovery=`Browsers may still be running; check the HTTP server or kill processes manually`;failedBrowserIds;httpBaseUrl;constructor(e,t,n){super(`Failed to close ${e.length} browser(s): ${e.join(`, `)}`,n),this.name=`BrowserCleanupError`,this.failedBrowserIds=e,this.httpBaseUrl=t}},Xn=class extends Error{code=`SESSION_SHUTDOWN_FAILED`;recovery=`Check logs for cleanup and transport errors; processes may need manual cleanup`;signal;constructor(e,t){super(`Shutdown failed for signal ${e}`,t),this.name=`SessionShutdownError`,this.signal=e}},Zn=class extends Error{code=`HTTP_SERVER_START_FAILED`;recovery=`Check if the HTTP server binary exists and the port is available`;constructor(e){super(`Failed to start HTTP server`,e),this.name=`HttpServerStartError`}},Qn=class extends Error{code=`MCP_SERVER_BOOTSTRAP_FAILED`;recovery=`Check if all dependencies are installed and try again`;constructor(e){super(`Failed to start MCP server`,e),this.name=`McpServerBootstrapError`}};async function $n(e,t,n){let r=e.getBrowserIds(),i=[],a;for(let e of r)try{let r=await fetch(`${t}/execute`,{method:`POST`,headers:n?{"Content-Type":`application/json`,[T]:n}:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:e}})});if(!r.ok){let e=await r.text().catch(()=>``);throw Error(`HTTP ${r.status} from ${t}/execute: ${e}`)}console.error(` Closed browser: ${e}`)}catch(t){let n=t instanceof Error?t:Error(String(t));console.error(` Failed to close browser: ${e}`,n),i.push(e),a??=n}if(i.length>0)throw new Yn(i,t,{cause:a});e.clear()}async function er(e,t,n,r,i){await e.start();let a=!1,o=async o=>{if(a)return;a=!0,console.error(`\nReceived ${o}, shutting down gracefully...`);let s;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await $n(t,n,r))}catch(e){s=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,s)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),s??=t}try{await i?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),s??=t}if(s){let e=new Xn(o,{cause:s});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(An,()=>o(An)),process.once(jn,()=>o(jn))}const tr=new F(`mcp-serve`).description(`Start Playwright MCP server for browser automation`).option(`-t, --type <type>`,`Transport type: ${[Q,$].join(`, `)}`,Q).option(`-b, --browser <browser>`,`Default browser type: ${[...On].join(`, `)}`,Dn).option(`--headless`,`Run browsers in headless mode by default`,!1).option(`--no-headless`,`Run browsers in headed mode (visible window)`).option(`--host <host>`,`Default host for HTTP services`,y()).option(`--port <port>`,`Port for streamable HTTP transport`,String(In)).option(`--http-port <port>`,`Port for the inner browse-tool HTTP service`).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...kn].join(`, `)}`).option(`--tags <tags>`,`Comma-separated tags to filter tools (e.g. input,navigation,snapshot)`).option(`--exclude <tools>`,`Comma-separated tool names to exclude (applied after --tags)`).option(`--custom-tools <path>`,`Path to a folder containing tools.yaml and custom tool scripts`).option(`--chrome-for-testing-path <path>`,`Path to a Chrome for Testing executable for the inner HTTP server`).option(`--snippets-dir <path>`,`Path to a folder where browser_run_code snippets are stored`).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before auto-closing browsers`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--registry-dir <path>`,`Custom registry directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--proxy-config-dir <path>`,`Custom proxy config directory`).action(async function(e){let t=z(`mcpServe`),n={type:B(this,`type`,e.type,t.type),browser:B(this,`browser`,e.browser,t.browser),headless:B(this,`headless`,e.headless,t.headless),profile:B(this,`profile`,e.profile,t.profile),mode:B(this,`mode`,e.mode,t.mode),host:B(this,`host`,e.host,t.host,process.env.PLAYWRIGHT_HOST),port:B(this,`port`,e.port,t.port,process.env.PLAYWRIGHT_MCP_PORT),httpPort:B(this,`httpPort`,e.httpPort,t.httpPort,process.env[Mn]),tags:B(this,`tags`,e.tags,t.tags),exclude:B(this,`exclude`,e.exclude,t.exclude),customTools:B(this,`customTools`,e.customTools,t.customTools),chromeForTestingPath:B(this,`chromeForTestingPath`,e.chromeForTestingPath,t.chromeForTestingPath,process.env[C]),snippetsDir:B(this,`snippetsDir`,e.snippetsDir,t.snippetsDir),idleTimeout:B(this,`idleTimeout`,e.idleTimeout,t.idleTimeout===void 0?void 0:String(t.idleTimeout),process.env[x]),registryPath:B(this,`registryPath`,e.registryPath,t.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:B(this,`registryDir`,e.registryDir,t.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:B(this,`pidsDir`,e.pidsDir,t.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:B(this,`profilesDir`,e.profilesDir,t.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),proxyConfigDir:B(this,`proxyConfigDir`,e.proxyConfigDir,t.proxyConfigDir,process.env[b])},i=Wn(n.type),a=Kn(process.cwd()),o=process.env.NODE_ENV||`development`,s,c;try{if(!En.has(i))throw new Ln(i);let e=Bn(n.browser),t=n.mode?Vn(n.mode):void 0,l=Hn(n.port??In),u=Un(n.httpPort),f=u?Hn(n.httpPort):void 0,p=Jn(n),m=n.customTools?A.resolve(n.customTools):void 0,g=n.chromeForTestingPath?A.resolve(n.chromeForTestingPath):void 0,S=n.snippetsDir?A.resolve(n.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${i}`),i===$&&console.error(` MCP endpoint: http://${n.host??y()}:${l}/mcp`),console.error(` Default browser: ${e}`),console.error(` Headless: ${n.headless}`),t&&console.error(` Default mode: ${t}`),n.profile&&console.error(` Profile: ${n.profile}`),p?.tags?.length&&console.error(` Tags: ${p.tags.join(`, `)}`),p?.exclude?.length&&console.error(` Exclude: ${p.exclude.join(`, `)}`),m&&console.error(` Custom tools: ${m}`),g&&console.error(` Chrome for Testing path: ${g}`),S&&console.error(` Snippets dir: ${S}`),n.idleTimeout&&console.error(` Idle timeout: ${n.idleTimeout} minutes`);let w=n.registryPath||n.registryDir;w&&(process.env.PLAYWRIGHT_REGISTRY_DIR=w,process.env.PORT_REGISTRY_PATH=w,process.env.PROCESS_REGISTRY_PATH=ne(w,`processes.json`),console.error(` Registry path: ${w}`)),n.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=n.pidsDir,console.error(` PIDs dir: ${n.pidsDir}`)),n.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=n.profilesDir,console.error(` Profiles dir: ${n.profilesDir}`)),n.proxyConfigDir&&(process.env[b]=A.resolve(n.proxyConfigDir),console.error(` Proxy config dir: ${process.env[b]}`)),S&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=S),g&&(process.env[C]=g),n.idleTimeout&&(process.env[x]=n.idleTimeout),n.host&&(process.env.PLAYWRIGHT_HOST=n.host);let T=v(),E=T.get(r.ProcessRegistryService);c=await T.get(r.McpPortAllocationService).acquirePorts({repositoryPath:a,environment:o,host:n.host??y(),pid:process.pid,preferredMcpPort:i===$?l:void 0,preferredHttpPort:f,exactHttpPort:u,reserveMcpPort:i===$,mcpServiceName:Pn,mcpServiceType:`tool`,mcpMetadata:i===$?{healthCheckUrl:`${d(n.host??y(),l)}/health`,transport:$}:void 0});let D=c.httpPort;process.env[Mn]=D.toString();let O=await T.get(r.HttpServerManager).ensureRunning(D,{exactPort:!0});if(!O.running)throw new Zn;let k=d(y(),O.port),re=O.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${re} on port ${O.port}`),i===Q){let r=new h,i=ce(),l=new _(wn({httpBaseUrl:k,sessionTracker:r,defaultMode:t,toolFilter:p,customToolsDir:m,enforcedProfileName:n.profile,proxyConfigDir:process.env[b],ownerId:i}));s=await te({repositoryPath:a,serviceName:`browse-tool-mcp`,serviceType:Fn,environment:o,pid:process.pid,metadata:{transport:Q,browser:e,mode:t}},E),await er(l,r,k,i,async()=>{await s?.release({kill:!1}),await c?.release()});return}let j=c.mcpPort;if(!j)throw Error(`Failed to reserve ${$} port ${l}`);s=await te({repositoryPath:a,serviceName:Pn,serviceType:Fn,environment:o,pid:process.pid,port:j,host:n.host??y(),metadata:{healthCheckUrl:`${d(n.host??y(),j)}/health`,transport:$}},E);let ie=new ee(({headers:e})=>{let r=Gn(e,`x-profile`)??n.profile,i=new h,a=ce();return{server:wn({httpBaseUrl:k,sessionTracker:i,defaultMode:t,toolFilter:p,customToolsDir:m,enforcedProfileName:r,proxyConfigDir:process.env[b],ownerId:a}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await $n(i,k,a)}}},{host:n.host??y(),port:j});try{await ie.start()}catch(e){try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}throw await c.release(),c=void 0,e}let ae=!1,M=async e=>{if(!ae){ae=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await ie.stop();try{await s?.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}await c?.release(),c=void 0,process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(An,()=>M(An)),process.once(jn,()=>M(jn))}catch(e){if(s)try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}if(c)try{await c.release()}catch(e){console.error(`Port allocation cleanup error:`,e)}(e instanceof Ln||e instanceof Rn||e instanceof zn||e instanceof Zn)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new Qn({cause:e instanceof Error?e:Error(String(e))});console.error(`Error [${t.code}]: ${t.message}`,t.cause),console.error(`Recovery: ${t.recovery}`),process.exit(1)}}),nr=new F(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let e=z(`httpServe`),t=process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH??e.registryPath??e.registryDir;t&&(process.env.PLAYWRIGHT_REGISTRY_DIR=t,process.env.PLAYWRIGHT_REGISTRY_PATH=t,process.env.PORT_REGISTRY_PATH=t),!process.env.PLAYWRIGHT_PIDS_DIR&&e.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=e.pidsDir),console.log(`browse-tool Status
|
|
433
|
-
`),console.log(`-`.repeat(50));let n=await m().get(r.HttpServerManager).getStatus();if(n.running){let t=process.env.PLAYWRIGHT_HOST??e.host??y();console.log(`HTTP Server: Running`),console.log(` PID: ${n.pid}`),console.log(` Port: ${n.port}`),console.log(` Health: ${d(t,n.port)}/health`),n.browserCount!==void 0&&console.log(` Browsers: ${n.browserCount}`)}else console.log(`HTTP Server: Not Running`),n.error&&console.log(` Error: ${n.error}`);console.log(``),console.log(`-`.repeat(50)),process.exit(0)}catch(e){console.error(`Error getting status:`,e),process.exit(1)}}),
|
|
434
|
-
`)}}function
|
|
431
|
+
`)})]}),L(`body`,{children:t})]})}function Xt(e){let t=new I;return t.get(`/`,async t=>{try{let n=e.get(r.BrowserService),i=e.get(r.PageRegistry),a=n.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt,pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt}))}}),c={totalBrowsers:a.length,totalPages:o.length};return t.html(L(Yt,{title:`Playwright MCP Dashboard`,children:L(Jt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),t.html(L(Yt,{title:`Playwright MCP Dashboard - Error`,children:R(`div`,{style:`padding: 2rem; text-align: center;`,children:[L(`h1`,{children:`Failed to load dashboard`}),L(`p`,{children:e instanceof Error?e.message:String(e)}),L(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),t}function Zt(e){if(!e.isError)return;let t=e.content[0];return t?.type===`text`&&typeof t.text==`string`?t.text:`Unknown tool execution error`}function Qt(e){try{return e.get(r.TelemetryService)}catch{return new f}}function $t(e){return e.startsWith(`browser-`)||e===`run-spec`||e===`discover-specs`?e.replaceAll(`-`,`_`):e}function en(e){return{browser_console_logs:`browser_list_console_messages`,browser_console_messages:`browser_list_console_messages`,browser_evaluate:`browser_evaluate_script`,browser_get_console_logs:`browser_list_console_messages`,browser_resize:`browser_resize_page`}[e]}function tn(e){let t=new I,n=e.get(r.PageRegistry),i=e.get(r.BrowserService),a=e.get(r.ExtensionTaskQueue),o=Qt(e),s=new Pt(n,a,o,i);t.use(`*`,_e({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`DELETE`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`,`Authorization`]})),t.get(`/health`,e=>{try{let t=i.listBrowsers(),n={status:`healthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,timestamp:new Date().toISOString(),browsers:{count:t.length,instances:t.map(e=>({id:e.id,pageCount:e.pageIds.size,createdAt:e.createdAt.toISOString()}))}};return e.json(n)}catch(t){return e.json({status:`unhealthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,error:t instanceof Error?t.message:String(t)},503)}}),t.post(`/execute`,async t=>{try{let i=await t.req.json();if(!i.tool)return t.json({success:!1,error:`Missing "tool" field in request body`},400);let a=typeof i.arguments?.pageId==`string`?i.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.execute`,{attributes:{"http.method":`POST`,"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":a}},async a=>{let s=e.getAll(r.Tool),c=$t(i.tool),l=s.find(e=>e.getDefinition().name===c);if(process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`&&i.tool===`browser_launch`){let e=typeof i.arguments?.videoDir==`string`?i.arguments.videoDir:typeof i.arguments?.video_dir==`string`?i.arguments.video_dir:``;console.log(`[ExtensionRecordingDebug] http execute browser_launch mode=${String(i.arguments?.mode??``)} videoDir=${e}`)}if(!l){let e=en(c),n=e?`Tool "${i.tool}" not found. Use "${e}" instead.`:`Tool "${i.tool}" not found`;return a?.setStatus({code:P.ERROR,message:n}),o.log(`warn`,`browse-tool HTTP execute rejected unknown tool`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.tool.normalized_name":c,"browse_tool.tool.suggested_name":e}}),t.json({success:!1,error:n},404)}let u=i.arguments||{},d;try{d=l.getInputSchema().parse(le(u,l.getInputSchema()))}catch(e){if(e instanceof N.ZodError){let n=ue(e,{schemaName:c,schema:l.getInputSchema()});return a?.setStatus({code:P.ERROR,message:n}),t.json({success:!1,result:{content:[{type:`text`,text:n}],isError:!0},error:n})}throw e}let f=await w(t.req.header(T),()=>l.execute(d)),p=d,m=typeof p.pageId==`string`?p.pageId:void 0;if(m){let t=e.get(r.BrowserService),i=n.get(m);i&&t.recordBrowserActivity(i.browserId,m)}let h=Zt(f);return h?(a?.setStatus({code:P.ERROR,message:h}),o.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":c,"browse_tool.page.id":m}})):o.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":c,"browse_tool.page.id":m}}),t.json({success:!f.isError,result:f,error:h})})}catch(e){return o.log(`error`,`browse-tool HTTP execute request crashed`,{attributes:{"http.route":`/execute`},exception:e}),t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/browsers`,t=>{try{let n=e.get(r.BrowserService).listBrowsers();return t.json({browsers:n.map(e=>({id:e.id,profileName:e.profileName,pageIds:Array.from(e.pageIds),currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString()}))})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/tools`,t=>{try{let n=e.getAll(r.Tool);return t.json({tools:n.map(e=>e.getDefinition())})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/custom-tools`,async e=>{let t=e.req.query(`dir`);if(!t)return e.json({tools:[],error:`Missing "dir" query parameter`},400);try{let n=await s.listTools(t);return e.json({tools:n})}catch(t){return e.json({tools:[],error:t instanceof Error?t.message:String(t)},400)}}),t.post(`/custom-tools`,async t=>{let i=t.req.query(`dir`);if(!i)return t.json({success:!1,error:`Missing "dir" query parameter`},400);try{let a=await t.req.json();if(!a.tool)return t.json({success:!1,error:`Missing "tool" field in request body`},400);let c=typeof a.arguments?.pageId==`string`?a.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.custom_tool.execute`,{attributes:{"http.method":`POST`,"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c,"browse_tool.custom_tools.directory":i}},async l=>{let u=await w(t.req.header(T),()=>s.executeTool(i,a.tool,a.arguments||{}));if(c){let t=e.get(r.BrowserService),i=n.get(c);i&&t.recordBrowserActivity(i.browserId,c)}let d=Zt(u);return d?(l?.setStatus({code:P.ERROR,message:d}),o.log(`error`,`browse-tool custom tool execution failed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}})):o.log(`info`,`browse-tool custom tool execution succeeded`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}}),t.json({success:!u.isError,result:u,error:d})})}catch(e){return o.log(`error`,`browse-tool custom tool request crashed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.custom_tools.directory":i},exception:e}),t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}});let c=we(e);t.route(`/extension`,c);let l=Ft(e);t.route(`/api`,l);let u=Xt(e);return t.route(`/`,u),t}function nn(e,t,n){e.get(`/ws/extension/:browserId`,n(e=>{let n=e.req.param(`browserId`),i=null;return{onOpen(e,a){if(!n){a.close(1008,`Missing browserId`);return}try{i=t.get(r.WebSocketHub).addConnection(a,n)}catch{a.close(1011,`Internal server error`)}},onMessage(e){if(i)try{let n=t.get(r.WebSocketHub),a=typeof e.data==`string`?e.data:e.data.toString();n.handleMessage(i,a)}catch{}},onClose(){if(i)try{t.get(r.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{t.get(r.WebSocketHub).removeConnection(i)}catch{}}}})),e.get(`/ws/stats`,e=>{try{let n=t.get(r.WebSocketHub).getStats();return e.json({totalConnections:n.totalConnections,browserCount:n.browserCount,connections:n.connections.map(e=>({id:e.id,browserId:e.browserId,connectedAt:e.connectedAt.toISOString()}))})}catch(t){return e.json({error:t instanceof Error?t.message:String(t)},500)}})}const rn=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function an(e=process.cwd()){let t=A.resolve(e);for(;;){for(let e of rn)if(j(A.join(t,e)))return t;let e=A.dirname(t);if(e===t)return process.cwd();t=e}}const on=new F(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(u)).option(`--host <host>`,`Host to bind`,y()).option(`--headless`,`Run browsers in headless mode by default`,!0).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before closing browsers`,`30`).option(`--registry-dir <path>`,`Custom registry path or directory for service discovery`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking (deprecated)`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--snippets-dir <path>`,`Directory used by browser_run_code to save and load reusable snippets`).option(`--proxy-config-dir <path>`,`Custom proxy config directory`).action(async function(e){let t=z(`httpServe`),n={port:B(this,`port`,e.port,t.port===void 0?void 0:String(t.port),process.env.PLAYWRIGHT_PORT),headless:B(this,`headless`,e.headless,t.headless),idleTimeout:B(this,`idleTimeout`,e.idleTimeout,t.idleTimeout===void 0?void 0:String(t.idleTimeout),process.env[x]),host:B(this,`host`,e.host,t.host,process.env.PLAYWRIGHT_HOST),registryDir:B(this,`registryDir`,e.registryDir,t.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:B(this,`registryPath`,e.registryPath,t.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:B(this,`pidsDir`,e.pidsDir,t.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:B(this,`profilesDir`,e.profilesDir,t.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:B(this,`snippetsDir`,e.snippetsDir,t.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR),proxyConfigDir:B(this,`proxyConfigDir`,e.proxyConfigDir,t.proxyConfigDir,process.env[b])},i;try{let e=Number.parseInt(n.port,10),t=`browse-tool-http`,a=process.env.NODE_ENV||`development`,o=an(process.cwd()),c=n.registryPath||n.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;c&&(process.env.PLAYWRIGHT_REGISTRY_DIR=c,process.env.PORT_REGISTRY_PATH=c,process.env.PROCESS_REGISTRY_PATH=ne(c,`processes.json`)),process.env.PLAYWRIGHT_HOST=n.host,process.env.PLAYWRIGHT_PORT=n.port,process.env[x]=n.idleTimeout,n.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=n.pidsDir),n.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=n.profilesDir),n.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=A.resolve(n.snippetsDir)),n.proxyConfigDir&&(process.env[b]=A.resolve(n.proxyConfigDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${e}`),console.log(` Host: ${n.host}`),console.log(` Headless: ${n.headless}`),console.log(` Idle Timeout: ${n.idleTimeout} minutes`),process.env.BROWSE_TOOL_SNIPPETS_DIR&&console.log(` Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`),process.env.BROWSE_TOOL_PROXY_CONFIG_DIR&&console.log(` Proxy Config Dir: ${process.env[b]}`);let l=s(),u=l.get(r.ProcessRegistryService),f=tn(l),{injectWebSocket:p,upgradeWebSocket:m}=ve({app:f});nn(f,l,m);let h=l.get(r.WebSocketHub),g=l.get(r.ExtensionTaskQueue),_=l.get(r.BrowserService),v=l.get(r.ExtensionSessionRegistry),y=l.get(r.PageRegistry),ee=l.get(r.IdleCleanupService);ee.start(),h.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=_.getBrowser(n),i,a;if(r&&(r.mode===`extension`||r.mode===`vm`)){i=r.id;let t=y.findByBrowser(i);a=r.currentPageId||t[0]?.id||``,h.reassignConnection(e.id,i),a||(a=y.registerExtensionPage(i),r.pageIds.add(a),r.currentPageId=a),console.log(`[WebSocket] Extension connected to existing browser: ${i}, pageId: ${a}`)}else{i=n,_.registerExtensionBrowserWithId(i),a=y.registerExtensionPage(i);let e=_.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=y.get(a);o&&(o.extensionTabId=t.payload.tabId,o.url=t.payload.url??o.url);let s=v.register({browserId:i,tabId:t.payload.tabId,url:t.payload.url,metadata:{transport:`websocket`}});h.sendSessionAck(e,t.id??``,s.id,s.controlMode),h.broadcastPageCreated(i,a,y.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=g.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&_.recordBrowserActivity(e.browserId,e.pageId)}},onTabMapped:(e,t)=>{if(t.type===`tab:mapped`){let e=y.get(t.payload.pageId);if(!e)return;e.extensionTabId=t.payload.tabId,_.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&v.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let S=new O(process.env.PORT_REGISTRY_PATH);console.log(c?` Registry path: ${c}`:` Registry path: default (~/.port-registry/ports.json)`);let C=await S.reservePort({repositoryPath:o,serviceName:t,serviceType:`tool`,environment:a,preferredPort:e,pid:process.pid,host:n.host,force:!0,portRange:D,metadata:{headless:n.headless,idleTimeout:n.idleTimeout}});if(!C.success||C.port===void 0)throw Error(C.error||`Failed to reserve port in range ${D.min}-${D.max}`);let w=C.port;C.record.metadata={...C.record.metadata,healthCheckUrl:`${d(n.host,w)}/health`};let T=ge({fetch:f.fetch,port:w,hostname:n.host});p(T),i=await te({repositoryPath:o,serviceName:t,serviceType:`service`,environment:a,pid:process.pid,port:w,host:n.host,metadata:{healthCheckUrl:`${d(n.host,w)}/health`,headless:n.headless,idleTimeout:n.idleTimeout,transport:`http`}},u);let E=d(n.host,w);console.log(`HTTP server listening on ${E}`),console.log(` Health check: ${E}/health`),console.log(` Execute tool: POST ${E}/execute`),console.log(`
|
|
432
|
+
Press Ctrl+C to stop the server`);let k=async e=>{console.log(`\n\n${e} received. Shutting down gracefully...`);try{ee.stop(),T.close(),console.log(`Server closed`);try{await l.get(r.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{i&&await i.release({kill:!1}),console.log(`Service deregistered`)}catch(e){console.error(`Process registry cleanup error:`,e)}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>k(`SIGINT`)),process.on(`SIGTERM`,()=>k(`SIGTERM`)),await new Promise(()=>{})}catch(e){if(i)try{await i.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),sn={browser_click:[`input`],browser_fill:[`input`],browser_type:[`input`],browser_select:[`input`],browser_hover:[`input`],browser_drag:[`input`],browser_press_key:[`input`],browser_upload_file:[`input`],browser_navigate:[`navigation`],browser_go_back:[`navigation`],browser_go_forward:[`navigation`],browser_reload:[`navigation`],browser_wait_for:[`navigation`],browser_snapshot:[`snapshot`],browser_screenshot:[`snapshot`],browser_pdf:[`snapshot`],browser_list_pages:[`page`],browser_new_page:[`page`],browser_select_page:[`page`],browser_close_page:[`page`],browser_resize_page:[`page`,`emulation`],browser_handle_dialog:[`dialog`],browser_list_network_requests:[`network`],browser_get_network_request:[`network`],browser_list_console_messages:[`console`],browser_evaluate_script:[`script`],browser_emulate:[`emulation`],browser_expect:[`testing`],browser_start_trace:[`tracing`],browser_stop_trace:[`tracing`],browser_list_profiles:[`profile`],browser_delete_profile:[`profile`],run_spec:[`spec`,`testing`],discover_specs:[`spec`,`testing`],browser_launch:[`browser`],browser_close:[`browser`],browser_run_code:[`code`,`script`]};function cn(e){let t=new Set;for(let[n,r]of Object.entries(sn))r.some(t=>e.includes(t))&&t.add(n);return t}const ln=3e3,un=[`browser_launch`],dn=[`browser_new_page`],fn=[`browser_close`],pn=[`browser_close_page`];function mn(e,t){let n=e.replaceAll(`-`,`_`);return t&&(n===`browser_list`||n===`browser_list_session`||n===`browser_list_sessions`)?`browser_list_session`:e.startsWith(`browser-`)||e===`run-spec`||e===`discover-specs`?n:e}function hn(e){if(!e?.content?.[0]?.text)return null;try{let t=JSON.parse(e.content[0].text);return{browserId:t.browserId,pageId:t.pageId,mode:t.mode,url:t.url,closedBrowserId:t.closedBrowserId,closedPageId:t.closedPageId,browserKeptOpen:t.browserKeptOpen}}catch{return null}}async function gn(e){await new Promise(t=>setTimeout(t,e))}async function _n(e){let t=new AbortController,n=setTimeout(()=>t.abort(),ln);try{let n=await fetch(`${e}/tools`,{signal:t.signal});if(!n.ok)throw Error(`HTTP error ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);if(!Array.isArray(r.tools))throw Error(`Invalid /tools response: missing tools array`);if(r.tools.length===0)throw Error(`HTTP server returned an empty tool list`);return r.tools}finally{clearTimeout(n)}}function vn(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function yn(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),ln);try{let r=await fetch(vn(e,t),{signal:n.signal});if(!r.ok)throw Error(`HTTP error ${r.status}: ${r.statusText}`);let i=await r.json();if(i.error)throw Error(i.error);if(!Array.isArray(i.tools))throw Error(`Invalid /custom-tools response: missing tools array`);return i.tools}finally{clearTimeout(r)}}async function bn(e){let t;for(let n=1;n<=4;n+=1)try{return await _n(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await gn(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function xn(e,t){let n;for(let r=1;r<=4;r+=1)try{return await yn(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await gn(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function Sn(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?cn(e.tags):null,n=new Set(e.exclude??[]);if(!t)return n.size>0?n:null;for(let e of n)t.delete(e);return t}function Cn(e){let t=new Map,n=(e,n)=>{let r=t.get(e)??new Set;r.add(n),t.set(e,r)};return e.defaultMode&&n(`browser_launch`,`mode`),e.enforcedProfileName&&n(`browser_launch`,`profileName`),t}function wn(e,t){let n=t.get(e.name);if(!n?.size)return e;let r=e.inputSchema;if(!r.properties||typeof r.properties!=`object`)return e;let i=Object.fromEntries(Object.entries(r.properties).filter(([e])=>!n.has(e))),a={...r,properties:i};return Array.isArray(r.required)&&(a.required=r.required.filter(e=>typeof e!=`string`||!n.has(e))),{...e,inputSchema:a}}function Tn(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:r,toolFilter:i,customToolsDir:a,enforcedProfileName:o,proxyConfigDir:s,ownerId:c}=e,u=i?.tags?.length?Sn(i):null,d=new Set(i?.exclude??[]),f=u!==null||d.size>0,p=Cn({defaultMode:r,enforcedProfileName:o}),m=new de({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),h={name:`browser_list_session`,description:`List all browsers and pages created in this MCP session. Use this to see what resources you have available.`,inputSchema:{type:`object`,properties:{},required:[],additionalProperties:!1}},g=[],_=[],v=new Set;function y(e){return f?e.filter(e=>d.has(e.name)?!1:u===null?!0:u.has(e.name)):e}function b(e){return e.filter(e=>!d.has(e.name))}function ee(e){return o?{...e,profileName:o}:e}function x(e){let t=l({proxy:e.proxy,profileName:typeof e.profileName==`string`?e.profileName:void 0,proxyConfigDir:s});if(!t){let t={...e};return delete t.proxy,t}return{...e,proxy:t}}function S(e){if(!o)return e;let t=e?.content?.[0]?.text;if(!t)return e;try{let n=JSON.parse(t),r=(Array.isArray(n.profiles)?n.profiles:[]).filter(e=>e?.name===o);return{...e,content:[{...e.content[0],text:JSON.stringify({...n,profileCount:r.length,profiles:r},null,2)},...e.content.slice(1)]}}catch{return e}}function C(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function w(e){return d.has(e)?!1:u===null?!0:u.has(e)}return m.setRequestHandler(pe,async()=>{try{let e=await bn(t);g=e,v=new Set(e.map(e=>e.name));let r=a?await xn(t,a):[];_=r;let i=[...y(e).map(e=>wn(e,p)),...b(r).map(e=>C(e))];return n&&i.push(h),{tools:i}}catch(e){if(g.length>0||_.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[...y(g).map(e=>wn(e,p)),...b(_).map(e=>C(e))];return n&&t.push(h),{tools:t}}let r=e instanceof Error?e.message:String(e);throw Error(`Failed to list tools from HTTP server at ${t}: ${r}`,{cause:e})}}),m.setRequestHandler(fe,async e=>{let{name:i,arguments:o}=e.params,s=mn(i,!!n),l=new Set(_.map(e=>e.name));if(s!==`browser_list_session`&&!w(s))return{content:[{type:`text`,text:`Tool "${s}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(s===`browser_list_session`&&n){let e=n.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let u=o||{};s===`browser_launch`&&r&&u.mode===void 0&&(u={...u,mode:r}),s===`browser_launch`&&(u=ee(u),u=x(u));try{if(a&&!l.has(s)&&!v.has(s))try{_=await xn(t,a),l=new Set(_.map(e=>e.name))}catch(e){console.error(`Failed to refresh custom tools, using cached list: ${e instanceof Error?e.message:String(e)}`)}let e=l.has(s),r=await fetch(e&&a?vn(t,a):`${t}/execute`,{method:`POST`,headers:c?{"Content-Type":`application/json`,[T]:c}:{"Content-Type":`application/json`},body:JSON.stringify({tool:s,arguments:u})});if(!r.ok){let e=await r.text();return{content:[{type:`text`,text:`HTTP error ${r.status}: ${e}`}],isError:!0}}let i=await r.json();if(!i.success)return{content:[{type:`text`,text:i.error||i.result?.content?.[0]?.text||`Unknown error from HTTP server`}],isError:!0};if(n&&i.result){if(un.includes(s)){let e=hn(i.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if(dn.includes(s)){let e=hn(i.result);e?.pageId&&e?.browserId&&n.trackPage(e.pageId,e.browserId,e.url)}else if(fn.includes(s)){let e=hn(i.result);e?.closedBrowserId&&n.untrackBrowser(e.closedBrowserId)}else if(pn.includes(s)){let e=hn(i.result);e?.closedPageId&&n.untrackPage(e.closedPageId),e?.browserId&&e.browserKeptOpen===!1&&n.untrackBrowser(e.browserId)}}return s===`browser_list_profiles`&&i.result?S(i.result):i.result||{content:[{type:`text`,text:`No result returned`}]}}catch(e){return{content:[{type:`text`,text:`Failed to execute tool via HTTP server: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),m}const Q=`stdio`,En=`http`,$=`streamable-http`,Dn=new Set([Q,En,$]),On=`chromium`,kn=new Set([On,`firefox`,`webkit`]),An=new Set([`playwright`,`extension`,`vm`,`stealth`]),jn=`SIGINT`,Mn=`SIGTERM`,Nn=`PLAYWRIGHT_PORT`,Pn=[`pnpm-workspace.yaml`,`nx.json`,`.git`],Fn=`browse-tool-mcp-http`,In=`service`,Ln=D.min;var Rn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...Dn].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},zn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...kn].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},Bn=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...An].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function Vn(e){let t=e.toLowerCase();if(!kn.has(t))throw new zn(e);return t}function Hn(e){let t=e.toLowerCase();if(!An.has(t))throw new Bn(e);return t}function Un(e){let t=typeof e==`number`?e:Number.parseInt(e,10);if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}function Wn(e){return e!=null&&String(e).trim()!==``}function Gn(e){let t=e.toLowerCase();return t===En?$:t}function Kn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function qn(e=process.cwd()){let t=A.resolve(e);for(;;){for(let e of Pn)if(j(A.join(t,e)))return t;let e=A.dirname(t);if(e===t)return process.cwd();t=e}}function Jn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function Yn(e){let t=e.tags?Jn(e.tags):void 0,n=e.exclude?Jn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var Xn=class extends Error{code=`BROWSER_CLEANUP_FAILED`;recovery=`Browsers may still be running; check the HTTP server or kill processes manually`;failedBrowserIds;httpBaseUrl;constructor(e,t,n){super(`Failed to close ${e.length} browser(s): ${e.join(`, `)}`,n),this.name=`BrowserCleanupError`,this.failedBrowserIds=e,this.httpBaseUrl=t}},Zn=class extends Error{code=`SESSION_SHUTDOWN_FAILED`;recovery=`Check logs for cleanup and transport errors; processes may need manual cleanup`;signal;constructor(e,t){super(`Shutdown failed for signal ${e}`,t),this.name=`SessionShutdownError`,this.signal=e}},Qn=class extends Error{code=`HTTP_SERVER_START_FAILED`;recovery=`Check if the HTTP server binary exists and the port is available`;constructor(e){super(`Failed to start HTTP server`,e),this.name=`HttpServerStartError`}},$n=class extends Error{code=`MCP_SERVER_BOOTSTRAP_FAILED`;recovery=`Check if all dependencies are installed and try again`;constructor(e){super(`Failed to start MCP server`,e),this.name=`McpServerBootstrapError`}};async function er(e,t,n){let r=e.getBrowserIds(),i=[],a;for(let e of r)try{let r=await fetch(`${t}/execute`,{method:`POST`,headers:n?{"Content-Type":`application/json`,[T]:n}:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:e}})});if(!r.ok){let e=await r.text().catch(()=>``);throw Error(`HTTP ${r.status} from ${t}/execute: ${e}`)}console.error(` Closed browser: ${e}`)}catch(t){let n=t instanceof Error?t:Error(String(t));console.error(` Failed to close browser: ${e}`,n),i.push(e),a??=n}if(i.length>0)throw new Xn(i,t,{cause:a});e.clear()}async function tr(e,t,n,r,i){await e.start();let a=!1,o=async o=>{if(a)return;a=!0,console.error(`\nReceived ${o}, shutting down gracefully...`);let s;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await er(t,n,r))}catch(e){s=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,s)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),s??=t}try{await i?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),s??=t}if(s){let e=new Zn(o,{cause:s});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(jn,()=>o(jn)),process.once(Mn,()=>o(Mn))}const nr=new F(`mcp-serve`).description(`Start Playwright MCP server for browser automation`).option(`-t, --type <type>`,`Transport type: ${[Q,$].join(`, `)}`,Q).option(`-b, --browser <browser>`,`Default browser type: ${[...kn].join(`, `)}`,On).option(`--headless`,`Run browsers in headless mode by default`,!1).option(`--no-headless`,`Run browsers in headed mode (visible window)`).option(`--host <host>`,`Default host for HTTP services`,y()).option(`--port <port>`,`Port for streamable HTTP transport`,String(Ln)).option(`--http-port <port>`,`Port for the inner browse-tool HTTP service`).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...An].join(`, `)}`).option(`--tags <tags>`,`Comma-separated tags to filter tools (e.g. input,navigation,snapshot)`).option(`--exclude <tools>`,`Comma-separated tool names to exclude (applied after --tags)`).option(`--custom-tools <path>`,`Path to a folder containing tools.yaml and custom tool scripts`).option(`--chrome-for-testing-path <path>`,`Path to a Chrome for Testing executable for the inner HTTP server`).option(`--snippets-dir <path>`,`Path to a folder where browser_run_code snippets are stored`).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before auto-closing browsers`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--registry-dir <path>`,`Custom registry directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--proxy-config-dir <path>`,`Custom proxy config directory`).action(async function(e){let t=z(`mcpServe`),n={type:B(this,`type`,e.type,t.type),browser:B(this,`browser`,e.browser,t.browser),headless:B(this,`headless`,e.headless,t.headless),profile:B(this,`profile`,e.profile,t.profile),mode:B(this,`mode`,e.mode,t.mode),host:B(this,`host`,e.host,t.host,process.env.PLAYWRIGHT_HOST),port:B(this,`port`,e.port,t.port,process.env.PLAYWRIGHT_MCP_PORT),httpPort:B(this,`httpPort`,e.httpPort,t.httpPort,process.env[Nn]),tags:B(this,`tags`,e.tags,t.tags),exclude:B(this,`exclude`,e.exclude,t.exclude),customTools:B(this,`customTools`,e.customTools,t.customTools),chromeForTestingPath:B(this,`chromeForTestingPath`,e.chromeForTestingPath,t.chromeForTestingPath,process.env[C]),snippetsDir:B(this,`snippetsDir`,e.snippetsDir,t.snippetsDir),idleTimeout:B(this,`idleTimeout`,e.idleTimeout,t.idleTimeout===void 0?void 0:String(t.idleTimeout),process.env[x]),registryPath:B(this,`registryPath`,e.registryPath,t.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:B(this,`registryDir`,e.registryDir,t.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:B(this,`pidsDir`,e.pidsDir,t.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:B(this,`profilesDir`,e.profilesDir,t.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),proxyConfigDir:B(this,`proxyConfigDir`,e.proxyConfigDir,t.proxyConfigDir,process.env[b])},i=Gn(n.type),a=qn(process.cwd()),o=process.env.NODE_ENV||`development`,s,c;try{if(!Dn.has(i))throw new Rn(i);let e=Vn(n.browser),t=n.mode?Hn(n.mode):void 0,l=Un(n.port??Ln),u=Wn(n.httpPort),f=u?Un(n.httpPort):void 0,p=Yn(n),m=n.customTools?A.resolve(n.customTools):void 0,g=n.chromeForTestingPath?A.resolve(n.chromeForTestingPath):void 0,S=n.snippetsDir?A.resolve(n.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${i}`),i===$&&console.error(` MCP endpoint: http://${n.host??y()}:${l}/mcp`),console.error(` Default browser: ${e}`),console.error(` Headless: ${n.headless}`),t&&console.error(` Default mode: ${t}`),n.profile&&console.error(` Profile: ${n.profile}`),p?.tags?.length&&console.error(` Tags: ${p.tags.join(`, `)}`),p?.exclude?.length&&console.error(` Exclude: ${p.exclude.join(`, `)}`),m&&console.error(` Custom tools: ${m}`),g&&console.error(` Chrome for Testing path: ${g}`),S&&console.error(` Snippets dir: ${S}`),n.idleTimeout&&console.error(` Idle timeout: ${n.idleTimeout} minutes`);let w=n.registryPath||n.registryDir;w&&(process.env.PLAYWRIGHT_REGISTRY_DIR=w,process.env.PORT_REGISTRY_PATH=w,process.env.PROCESS_REGISTRY_PATH=ne(w,`processes.json`),console.error(` Registry path: ${w}`)),n.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=n.pidsDir,console.error(` PIDs dir: ${n.pidsDir}`)),n.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=n.profilesDir,console.error(` Profiles dir: ${n.profilesDir}`)),n.proxyConfigDir&&(process.env[b]=A.resolve(n.proxyConfigDir),console.error(` Proxy config dir: ${process.env[b]}`)),S&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=S),g&&(process.env[C]=g),n.idleTimeout&&(process.env[x]=n.idleTimeout),n.host&&(process.env.PLAYWRIGHT_HOST=n.host);let T=v(),E=T.get(r.ProcessRegistryService);c=await T.get(r.McpPortAllocationService).acquirePorts({repositoryPath:a,environment:o,host:n.host??y(),pid:process.pid,preferredMcpPort:i===$?l:void 0,preferredHttpPort:f,exactHttpPort:u,reserveMcpPort:i===$,mcpServiceName:Fn,mcpServiceType:`tool`,mcpMetadata:i===$?{healthCheckUrl:`${d(n.host??y(),l)}/health`,transport:$}:void 0});let D=c.httpPort;process.env[Nn]=D.toString();let O=await T.get(r.HttpServerManager).ensureRunning(D,{exactPort:!0});if(!O.running)throw new Qn;let k=d(y(),O.port),re=O.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${re} on port ${O.port}`),i===Q){let r=new h,i=ce(),l=new _(Tn({httpBaseUrl:k,sessionTracker:r,defaultMode:t,toolFilter:p,customToolsDir:m,enforcedProfileName:n.profile,proxyConfigDir:process.env[b],ownerId:i}));s=await te({repositoryPath:a,serviceName:`browse-tool-mcp`,serviceType:In,environment:o,pid:process.pid,metadata:{transport:Q,browser:e,mode:t}},E),await tr(l,r,k,i,async()=>{await s?.release({kill:!1}),await c?.release()});return}let j=c.mcpPort;if(!j)throw Error(`Failed to reserve ${$} port ${l}`);s=await te({repositoryPath:a,serviceName:Fn,serviceType:In,environment:o,pid:process.pid,port:j,host:n.host??y(),metadata:{healthCheckUrl:`${d(n.host??y(),j)}/health`,transport:$}},E);let ie=new ee(({headers:e})=>{let r=Kn(e,`x-profile`)??n.profile,i=new h,a=ce();return{server:Tn({httpBaseUrl:k,sessionTracker:i,defaultMode:t,toolFilter:p,customToolsDir:m,enforcedProfileName:r,proxyConfigDir:process.env[b],ownerId:a}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await er(i,k,a)}}},{host:n.host??y(),port:j});try{await ie.start()}catch(e){try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}throw await c.release(),c=void 0,e}let ae=!1,M=async e=>{if(!ae){ae=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await ie.stop();try{await s?.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}await c?.release(),c=void 0,process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(jn,()=>M(jn)),process.once(Mn,()=>M(Mn))}catch(e){if(s)try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}if(c)try{await c.release()}catch(e){console.error(`Port allocation cleanup error:`,e)}(e instanceof Rn||e instanceof zn||e instanceof Bn||e instanceof Qn)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new $n({cause:e instanceof Error?e:Error(String(e))});console.error(`Error [${t.code}]: ${t.message}`,t.cause),console.error(`Recovery: ${t.recovery}`),process.exit(1)}}),rr=new F(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let e=z(`httpServe`),t=process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH??e.registryPath??e.registryDir;t&&(process.env.PLAYWRIGHT_REGISTRY_DIR=t,process.env.PLAYWRIGHT_REGISTRY_PATH=t,process.env.PORT_REGISTRY_PATH=t),!process.env.PLAYWRIGHT_PIDS_DIR&&e.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=e.pidsDir),console.log(`browse-tool Status
|
|
433
|
+
`),console.log(`-`.repeat(50));let n=await m().get(r.HttpServerManager).getStatus();if(n.running){let t=process.env.PLAYWRIGHT_HOST??e.host??y();console.log(`HTTP Server: Running`),console.log(` PID: ${n.pid}`),console.log(` Port: ${n.port}`),console.log(` Health: ${d(t,n.port)}/health`),n.browserCount!==void 0&&console.log(` Browsers: ${n.browserCount}`)}else console.log(`HTTP Server: Not Running`),n.error&&console.log(` Error: ${n.error}`);console.log(``),console.log(`-`.repeat(50)),process.exit(0)}catch(e){console.error(`Error getting status:`,e),process.exit(1)}}),ir=new F(`stop`).description(`Stop HTTP server and clean up registry/PID files`).action(async()=>{try{let e=z(`httpServe`),t=process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH??e.registryPath??e.registryDir;t&&(process.env.PLAYWRIGHT_REGISTRY_DIR=t,process.env.PLAYWRIGHT_REGISTRY_PATH=t,process.env.PORT_REGISTRY_PATH=t),!process.env.PLAYWRIGHT_PIDS_DIR&&e.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=e.pidsDir),console.log(`Stopping browse-tool services...`),await m().get(r.HttpServerManager).stop()?(console.log(`HTTP server stopped`),console.log(`Registry and PID files cleaned up`)):console.log(`No HTTP server was running`),console.log(`Done!`),process.exit(0)}catch(e){console.error(`Error stopping services:`,e),process.exit(1)}}),ar=new Set([`--config`]);function or(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(ar.has(n)){t+=1;continue}if(![...ar].some(e=>n.startsWith(`${e}=`))&&!n.startsWith(`-`))return n}}function sr(e,t=!1){if(t)return!1;let n=or(e);return n?n===`tools`?!0:n===`help`&&e.includes(`tools`):!1}function cr(e){return e.replace(/_/g,`-`)}function lr(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function ur(e){let t=new Set;e.type&&t.add(e.type);for(let n of e.oneOf??[])n.type&&t.add(n.type);for(let n of e.anyOf??[])n.type&&t.add(n.type);return t}function dr(e,t){let n=ur(t);if(n.has(`boolean`)){if(e===`true`||e===`1`)return!0;if(e===`false`||e===`0`)return!1}if(t.type===`number`||t.type===`integer`){let t=Number(e);if(Number.isNaN(t))throw Error(`Invalid number: ${e}`);return t}if(t.type===`boolean`){if(e===`true`||e===`1`)return!0;if(e===`false`||e===`0`)return!1;throw Error(`Invalid boolean: ${e}`)}if(n.has(`array`)||n.has(`object`))try{return JSON.parse(e)}catch{if(t.type===`array`||t.type===`object`)throw Error(`Invalid JSON: ${e}`)}if(n.has(`number`)||n.has(`integer`)){let t=Number(e);if(!Number.isNaN(t)&&e.trim()!==``)return t}return e}function fr(e,t){let n=lr(e),r=ur(t);return t.type===`boolean`?`--${n}`:r.has(`boolean`)&&r.has(`object`)?`--${n} [value]`:`--${n} <value>`}function pr(e){let{definition:t,execute:n}=e,r=new F(cr(t.name));r.description(t.description);let i=t.inputSchema,a=i.properties||{},o=new Set(i.required||[]);for(let[e,t]of Object.entries(a)){let n=t,i=fr(e,n),a=n.description||``;n.enum&&n.enum.length>0&&(a+=` (choices: ${n.enum.join(`, `)})`),n.default!==void 0&&(a+=` (default: ${JSON.stringify(n.default)})`),o.has(e)&&(a+=` [required]`),n.type===`boolean`?n.default===!0?r.option(`--no-${lr(e)}`,`Disable ${a}`):r.option(i,a,n.default):n.enum&&n.enum.length>0?r.addOption(new he(i,a).choices(n.enum)):r.option(i,a)}return r.action(async function(){let e=this.optsWithGlobals(),r=z(`tools`),i=B(this,`format`,e.format??`json`,r.format),s=B(this,`color`,e.color??!0,r.color),c=B(this,`port`,String(e.port??u),r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),l={format:i,color:s},d=Qe(t.name);try{let r={};for(let[t,n]of Object.entries(a)){let i=n,a=lr(t).replace(/-([a-z])/g,(e,t)=>t.toUpperCase()),o=this.getOptionValueSourceWithGlobals(a),s=e[a];i.type===`boolean`&&i.default===!0&&(s=e[a]),(o===void 0||o===`default`||o===`implied`)&&d[t]!==void 0&&(s=d[t]),s===void 0&&i.default!==void 0&&(s=i.default),s!==void 0&&(typeof s==`string`&&i.type!==`string`?r[t]=dr(s,i):r[t]=s)}for(let e of o)if(r[e]===void 0){let t=lr(e);console.error(U(`Missing required option: --${t}`,l.color)),process.exit(1)}let i=Number.parseInt(c,10),s=V({port:i}),u=n?await n(r,{command:this,formatterOptions:l,port:i}):await s.execute(t.name,r),f=tt(u,l);f&&console.log(f),u.isError&&process.exit(1)}catch(e){console.error(U(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),r}function mr(e){return e.map(pr)}const hr={name:`browser_list_session`,description:`List browsers and pages currently tracked by the browse-tool HTTP server.`,inputSchema:{type:`object`,properties:{},required:[],additionalProperties:!1}};function gr(){return new F(`tools`).description(`Execute browser automation tools`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(u))}function _r(e){return{browserCount:e.length,browsers:e.map(e=>({browserId:e.id,profileName:e.profileName,currentPageId:e.currentPageId,pageIds:e.pageIds,createdAt:e.createdAt}))}}function vr(){return{definition:hr,execute:async(e,t)=>{let n=await V({port:t.port}).listBrowsers();return{content:[{type:`text`,text:JSON.stringify(_r(n),null,2)}]}}}}function yr(e){return[...e.map(e=>({definition:e})),vr()]}async function br(e,t){let n=z(`tools`),r=Number(t?.port??process.env.PLAYWRIGHT_PORT??n.port??u);try{let t=mr(yr(await V({port:r}).listTools()));for(let n of t)e.addCommand(n)}catch(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(`${U(`Failed to load tool commands: ${t}`,!0)}\n`),process.stderr.write(`Make sure the HTTP server is running: browse-tool http-serve
|
|
434
|
+
`)}}function xr(e,t){let n=[...e];for(let e=0;e<n.length;e+=1){let t=n[e];if(t===`--port`||t===`-p`){let t=n[e+1],r=Number(t);if(!Number.isNaN(r)&&t)return r;continue}if(t.startsWith(`--port=`)){let e=Number(t.slice(7));if(!Number.isNaN(e))return e;continue}if(t.startsWith(`-p=`)){let e=Number(t.slice(3));if(!Number.isNaN(e))return e}}return Number(t)}async function Sr(){let e=Ze(process.argv.slice(2)),t=process.env.PLAYWRIGHT_PORT??e.config.commands.tools?.port??e.config.commands.exec?.port??u,n=new F;n.name(`browse-tool`).description(`MCP server for browser automation using Playwright with profile management, page registry, and multi-browser support`).option(`--config <path>`,`Path to browse-tool config file or config directory`).version(xe),n.addCommand(nr),n.addCommand(on),n.addCommand(Ie),n.addCommand(ft),n.addCommand(ir),n.addCommand(rr),n.addCommand(pt),n.addCommand(ut),n.addCommand(dt);let r=gr();sr(process.argv.slice(2),process.env.PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS===`1`)&&await br(r,{port:xr(process.argv.slice(2),t)}),n.addCommand(r),await n.parseAsync(process.argv)}Sr().catch(e=>{console.error(e instanceof Error?e.message:String(e)),process.exit(1)});export{};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/browse-tool",
|
|
3
3
|
"description": "MCP server for browser automation using Playwright with profile management, page registry, and multi-browser support",
|
|
4
|
-
"version": "0.17.
|
|
4
|
+
"version": "0.17.3",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"ws": "8.20.0",
|
|
45
45
|
"yaml": "2.8.3",
|
|
46
46
|
"zod": "4.4.1",
|
|
47
|
-
"@agimon-ai/foundation-port-registry": "0.17.
|
|
48
|
-
"@agimon-ai/foundation-process-registry": "0.17.
|
|
49
|
-
"@agimon-ai/log-sink-mcp": "0.17.
|
|
50
|
-
"@agimon-ai/foundation-validator": "0.14.
|
|
47
|
+
"@agimon-ai/foundation-port-registry": "0.17.3",
|
|
48
|
+
"@agimon-ai/foundation-process-registry": "0.17.3",
|
|
49
|
+
"@agimon-ai/log-sink-mcp": "0.17.3",
|
|
50
|
+
"@agimon-ai/foundation-validator": "0.14.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/chrome": "0.1.40",
|