@agimon-ai/browse-tool 0.8.5 → 0.8.7
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 +21 -21
- package/dist/cli.mjs +4 -4
- package/package.json +5 -5
package/dist/cli.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const e=require(`./streamable-http-C82CVPKg.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.E(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(`@modelcontextprotocol/sdk/server/index.js`),f=require(`@modelcontextprotocol/sdk/types.js`),p=require(`@modelcontextprotocol/sdk/server/stdio.js`),m=require(`commander`),h=require(`@hono/node-server`),g=require(`hono`),_=require(`hono/cors`),v=require(`@hono/node-ws`),y=require(`hono/html`),b=require(`hono/jsx`),x=require(`hono/jsx/jsx-runtime`);var S=`0.8.4`;const C=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function w(e,t){if(!C)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function T(t){let n=new g.Hono,r;try{r=t.get(e.T.TelemetryService)}catch{r=new e.g}return n.get(`/telemetry-config`,t=>{try{let n=new URL(t.req.url).origin;return t.json(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.T.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.T.ExtensionTaskQueue),a=t.get(e.T.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.T.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.T.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.T.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.T.PageRegistry),a=t.get(e.T.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.T.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return w(`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.T.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return w(`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:{}}}),w(`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.T.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.T.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.T.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 E(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 D=[`pnpm-workspace.yaml`,`nx.json`,`.git`],O=`browse-tool-chrome`,k=`tool`,ee=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 D)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:O,serviceType:k,environment:ee,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:O,serviceType:k,environment:ee,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.T.ExtensionTaskQueue).to(e.h).inSingletonScope(),t.bind(e.T.ExtensionToolDelegator).to(e.m).inSingletonScope()})}function oe(e){let t=new d.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(f.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(f.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const se=new m.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=E(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:O,serviceType:k,environment:ee,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.T.ExtensionTaskQueue),m=d.get(e.T.ExtensionToolDelegator),v=new g.Hono;v.use(`*`,(0,_.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let y=T(d);v.route(`/extension`,y),v.get(`/health`,e=>{let t=f.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let b=(0,h.serve)({fetch:v.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(m),S=new p.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 A={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 A=be(e,t),A}function j(e){return(A.config.commands??{})[e]??{}}function Se(e){return A.config.tools?.[e]??{}}function M(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.T.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 N(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 P(e,t,n){return n?`${we[t]}${e}${we.reset}`:e}function F(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Te(e):Ee(e,r)}function Te(e){return JSON.stringify(e,null,2)}function Ee(e,t){let n=[];e.isError&&n.push(P(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(De(r,t)):r.type===`image`?n.push(Ae(r,t)):n.push(P(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
2
|
+
const e=require(`./streamable-http-C82CVPKg.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.E(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(`@modelcontextprotocol/sdk/server/index.js`),f=require(`@modelcontextprotocol/sdk/types.js`),p=require(`@modelcontextprotocol/sdk/server/stdio.js`),m=require(`commander`),h=require(`@hono/node-server`),g=require(`hono`),_=require(`hono/cors`),v=require(`@hono/node-ws`),y=require(`hono/html`),b=require(`hono/jsx`),x=require(`hono/jsx/jsx-runtime`);var S=`0.8.6`;const C=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function w(e,t){if(!C)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function T(t){let n=new g.Hono,r;try{r=t.get(e.T.TelemetryService)}catch{r=new e.g}return n.get(`/telemetry-config`,t=>{try{let n=new URL(t.req.url).origin;return t.json(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.T.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.T.ExtensionTaskQueue),a=t.get(e.T.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.T.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.T.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.T.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.T.PageRegistry),a=t.get(e.T.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.T.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return w(`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.T.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return w(`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:{}}}),w(`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.T.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.T.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.T.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 E(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 D=[`pnpm-workspace.yaml`,`nx.json`,`.git`],O=`browse-tool-chrome`,k=`tool`,ee=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 D)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:O,serviceType:k,environment:ee,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:O,serviceType:k,environment:ee,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.T.ExtensionTaskQueue).to(e.h).inSingletonScope(),t.bind(e.T.ExtensionToolDelegator).to(e.m).inSingletonScope()})}function oe(e){let t=new d.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(f.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(f.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const se=new m.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=E(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:O,serviceType:k,environment:ee,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.T.ExtensionTaskQueue),m=d.get(e.T.ExtensionToolDelegator),v=new g.Hono;v.use(`*`,(0,_.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let y=T(d);v.route(`/extension`,y),v.get(`/health`,e=>{let t=f.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let b=(0,h.serve)({fetch:v.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(m),S=new p.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 A={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 A=be(e,t),A}function j(e){return(A.config.commands??{})[e]??{}}function Se(e){return A.config.tools?.[e]??{}}function M(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.T.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 N(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 P(e,t,n){return n?`${we[t]}${e}${we.reset}`:e}function F(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Te(e):Ee(e,r)}function Te(e){return JSON.stringify(e,null,2)}function Ee(e,t){let n=[];e.isError&&n.push(P(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(De(r,t)):r.type===`image`?n.push(Ae(r,t)):n.push(P(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
3
3
|
`)}function De(e,t){let n=e.text;try{return Oe(JSON.parse(n),t)}catch{return n}}function Oe(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=P(r,`cyan`,t),a=ke(i,t);n.push(`${e}: ${a}`)}return n.join(`
|
|
4
4
|
`)}function ke(e,t){return e===null?P(`null`,`gray`,t):e===void 0?P(`undefined`,`gray`,t):typeof e==`boolean`?P(String(e),e?`green`:`red`,t):typeof e==`number`?P(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?P(e,`blue`,t):e:Array.isArray(e)?e.length===0?P(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function Ae(e,t){let{mimeType:n,data:r}=e;return P(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function I(e,t){return P(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function je(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=P(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
|
|
5
|
-
`)}function Me(e,t){let n=j(`tools`),r=M(e,`format`,t.format,n.format),i=M(e,`color`,t.color,n.color);return{port:M(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const Ne=new m.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}=Me(this,t);try{let t=await N({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?je(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(I(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),Pe=new m.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}=Me(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(I(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await N({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=F(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(I(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new m.Command(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(Ne).addCommand(Pe);const Fe=new m.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.w).option(`--image <image>`,`Docker image tag to produce`,e.S).option(`--platform <platform>`,`Docker target platform`,e.C).action(async t=>{try{let n=await e.y({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.b(n.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),Ie=new m.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=j(`exec`),i=M(this,`format`,n.format,r.format),a=M(this,`color`,n.color,r.color),o=M(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(I(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await N({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=F(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(I(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),L=`pageId`,R=`browserId`,Le=l.z.record(l.z.string(),l.z.unknown()),Re=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(),ze=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:Le,inputSchema:Re}),Be=l.z.object({tools:l.z.array(ze)}),Ve=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function He(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function z(e,t){if(Ve){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function B(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Ue(e){return B(e)&&Array.isArray(e.content)}function V(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 We(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 Ge(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 H(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(`-`)?Ke(e,t,n):qe(e,t,n)}function Ke(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]=H(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=U(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]=H(e,i+1,r.indent);s[t]=a,i=o}else s[t]=V(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]=H(e,i,t.indent);if(!B(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=U(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]=H(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=V(a),i+=1}r.push(s);continue}r.push(V(a)),i+=1}return[r,i]}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,o]=U(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]=H(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=V(o),i+=1}return[r,i]}function U(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 Je(e){let t=Ge(e);if(t.length===0)return{};let[n]=H(t,0,t[0].indent);return n}function Ye(e){e.inputSchema.properties===void 0&&(e.inputSchema.properties={});let t=e.inputSchema.properties;if(!B(t))throw Error(`Custom tool "${e.name}" inputSchema.properties must be an object`);let n=t[L],r=t[R];if(n!==void 0&&!(B(n)&&n.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!(B(r)&&r.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`);n===void 0&&(t[L]={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[R]={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 Xe(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 Ze(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 Qe(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function $e(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(B(r)){let i=Qe(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 et(e,t,n){if(Ue(t))return n?$e(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=B(t)&&n?Qe(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var tt=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(e,t){let n=typeof t[L]==`string`&&t[L].length>0?t[L]:void 0,r=typeof t[R]==`string`&&t[R].length>0?t[R]: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`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let i=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:i}}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=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!r)throw Error(`Browser "${t}" has no pages`);let i=this.pageRegistry.get(r);if(!i)throw Error(`Page "${r}" not found`);return{...this.toPageSummary(n.currentPageId,i),page:this.resolveToolPage(e,r,i)}},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:We({...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[L]==`string`?n[L]:void 0,a=typeof n[R]==`string`?n[R]: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}),z(`Executing custom tool`,{toolName:t,directory:i.default.resolve(e),pageId:o,browserId:s.browserId,mode:s.mode,input:He(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 z(`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})}z(`Custom tool execution completed`,{toolName:t,pageId:o,browserId:s.browserId,mode:s.mode,result:He(f)});let p=et(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=Je(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=Be.parse(r);return Promise.all(a.tools.map(async e=>{Ye(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=Ze(n,await Xe(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function nt(t){let n=new g.Hono;return n.get(`/browsers`,async n=>{try{let r=t.get(e.T.BrowserService),i=t.get(e.T.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.T.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.T.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.T.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 rt(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 it(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 at(e){return it(new Date().getTime()-e.getTime())}const W=`table-container`,ot=`stat-card`,st=`browser-row`,ct=`page-row`,G=`status-active`,lt=`url-cell`,K=`timestamp-cell`,q=`actions-cell`,J=`kill-btn`,ut=`empty-state`;function dt({browsers:e}){return e.length===0?(0,x.jsx)(`div`,{class:W,children:(0,x.jsxs)(`div`,{class:ut,children:[(0,x.jsx)(`h3`,{children:`No Active Browsers`}),(0,x.jsx)(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):(0,x.jsx)(`div`,{class:W,children:(0,x.jsxs)(`table`,{class:`browser-table`,children:[(0,x.jsx)(`thead`,{children:(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`th`,{children:`ID`}),(0,x.jsx)(`th`,{children:`Status`}),(0,x.jsx)(`th`,{children:`URL / Title`}),(0,x.jsx)(`th`,{children:`Created`}),(0,x.jsx)(`th`,{children:`Age`}),(0,x.jsx)(`th`,{children:`Actions`})]})}),(0,x.jsx)(`tbody`,{id:`browser-table-body`,children:e.map(e=>(0,x.jsxs)(b.Fragment,{children:[(0,x.jsxs)(`tr`,{class:st,children:[(0,x.jsx)(`td`,{children:e.id}),(0,x.jsx)(`td`,{children:(0,x.jsxs)(`span`,{class:G,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),(0,x.jsx)(`td`,{children:e.profileName||`Default Profile`}),(0,x.jsx)(`td`,{class:K,children:rt(e.createdAt)}),(0,x.jsx)(`td`,{class:K,children:at(e.createdAt)}),(0,x.jsx)(`td`,{class:q,children:(0,x.jsx)(`button`,{type:`button`,class:J,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>(0,x.jsxs)(`tr`,{class:ct,children:[(0,x.jsxs)(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),(0,x.jsx)(`td`,{children:(0,x.jsx)(`span`,{class:G,children:`Open`})}),(0,x.jsx)(`td`,{class:lt,title:t.url,children:t.title||t.url||`about:blank`}),(0,x.jsx)(`td`,{class:K,children:rt(t.createdAt)}),(0,x.jsx)(`td`,{class:K,children:at(t.createdAt)}),(0,x.jsx)(`td`,{class:q,children:(0,x.jsx)(`button`,{type:`button`,class:J,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]},t.id))]},e.id))})]})})}function ft({stats:e}){return(0,x.jsxs)(`div`,{class:`stats-container`,children:[(0,x.jsxs)(`div`,{class:ot,children:[(0,x.jsx)(`h3`,{children:`Active Browsers`}),(0,x.jsx)(`div`,{class:`value`,children:e.totalBrowsers})]}),(0,x.jsxs)(`div`,{class:ot,children:[(0,x.jsx)(`h3`,{children:`Active Pages`}),(0,x.jsx)(`div`,{class:`value`,children:e.totalPages})]})]})}function pt({browsers:e,stats:t}){return(0,x.jsxs)(`div`,{class:`dashboard-container`,children:[(0,x.jsxs)(`div`,{class:`dashboard-header`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h1`,{children:`Playwright MCP Dashboard`}),(0,x.jsx)(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`button`,{type:`button`,id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),(0,x.jsx)(`button`,{type:`button`,id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),(0,x.jsx)(ft,{stats:t}),(0,x.jsx)(dt,{browsers:e}),(0,x.jsx)(`script`,{children:(0,y.raw)(`
|
|
5
|
+
`)}function Me(e,t){let n=j(`tools`),r=M(e,`format`,t.format,n.format),i=M(e,`color`,t.color,n.color);return{port:M(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const Ne=new m.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}=Me(this,t);try{let t=await N({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?je(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(I(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),Pe=new m.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}=Me(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(I(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await N({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=F(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(I(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new m.Command(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(Ne).addCommand(Pe);const Fe=new m.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.w).option(`--image <image>`,`Docker image tag to produce`,e.S).option(`--platform <platform>`,`Docker target platform`,e.C).action(async t=>{try{let n=await e.y({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.b(n.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),Ie=new m.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=j(`exec`),i=M(this,`format`,n.format,r.format),a=M(this,`color`,n.color,r.color),o=M(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(I(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await N({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=F(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(I(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),L=`pageId`,R=`browserId`,Le=l.z.record(l.z.string(),l.z.unknown()),Re=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(),ze=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:Le,inputSchema:Re}),Be=l.z.object({tools:l.z.array(ze)}),Ve=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function He(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function z(e,t){if(Ve){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function B(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Ue(e){return B(e)&&Array.isArray(e.content)}function V(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 We(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 Ge(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 H(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(`-`)?Ke(e,t,n):qe(e,t,n)}function Ke(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]=H(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=U(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]=H(e,i+1,r.indent);s[t]=a,i=o}else s[t]=V(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]=H(e,i,t.indent);if(!B(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=U(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]=H(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=V(a),i+=1}r.push(s);continue}r.push(V(a)),i+=1}return[r,i]}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,o]=U(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]=H(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=V(o),i+=1}return[r,i]}function U(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 Je(e){let t=Ge(e);if(t.length===0)return{};let[n]=H(t,0,t[0].indent);return n}function Ye(e){e.inputSchema.properties===void 0&&(e.inputSchema.properties={});let t=e.inputSchema.properties;if(!B(t))throw Error(`Custom tool "${e.name}" inputSchema.properties must be an object`);let n=t[L],r=t[R];if(n!==void 0&&!(B(n)&&n.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!(B(r)&&r.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`);n===void 0&&(t[L]={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[R]={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 Xe(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 Ze(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 Qe(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function $e(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(B(r)){let i=Qe(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 et(e,t,n){if(Ue(t))return n?$e(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=B(t)&&n?Qe(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var tt=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(e,t){let n=typeof t[L]==`string`&&t[L].length>0?t[L]:void 0,r=typeof t[R]==`string`&&t[R].length>0?t[R]: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`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let i=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:i}}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=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!r)throw Error(`Browser "${t}" has no pages`);let i=this.pageRegistry.get(r);if(!i)throw Error(`Page "${r}" not found`);return{...this.toPageSummary(n.currentPageId,i),page:this.resolveToolPage(e,r,i)}},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:We({...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[L]==`string`?n[L]:void 0,a=typeof n[R]==`string`?n[R]: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}),z(`Executing custom tool`,{toolName:t,directory:i.default.resolve(e),pageId:o,browserId:s.browserId,mode:s.mode,input:He(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 z(`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})}z(`Custom tool execution completed`,{toolName:t,pageId:o,browserId:s.browserId,mode:s.mode,result:He(f)});let p=et(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=Je(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=Be.parse(r);return Promise.all(a.tools.map(async e=>{Ye(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=Ze(n,await Xe(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function nt(t){let n=new g.Hono;return n.get(`/browsers`,async n=>{try{let r=t.get(e.T.BrowserService),i=t.get(e.T.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.T.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.T.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.T.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 rt(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 it(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 at(e){return it(new Date().getTime()-e.getTime())}const ot=`table-container`,st=`stat-card`,ct=`browser-row`,lt=`page-row`,W=`status-active`,ut=`url-cell`,G=`timestamp-cell`,K=`actions-cell`,q=`kill-btn`,dt=`empty-state`;function ft({browsers:e}){return e.length===0?(0,x.jsx)(`div`,{class:ot,children:(0,x.jsxs)(`div`,{class:dt,children:[(0,x.jsx)(`h3`,{children:`No Active Browsers`}),(0,x.jsx)(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):(0,x.jsx)(`div`,{class:ot,children:(0,x.jsxs)(`table`,{class:`browser-table`,children:[(0,x.jsx)(`thead`,{children:(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`th`,{children:`ID`}),(0,x.jsx)(`th`,{children:`Status`}),(0,x.jsx)(`th`,{children:`URL / Title`}),(0,x.jsx)(`th`,{children:`Created`}),(0,x.jsx)(`th`,{children:`Age`}),(0,x.jsx)(`th`,{children:`Actions`})]})}),(0,x.jsx)(`tbody`,{id:`browser-table-body`,children:e.map(e=>(0,x.jsxs)(b.Fragment,{children:[(0,x.jsxs)(`tr`,{class:ct,children:[(0,x.jsx)(`td`,{children:e.id}),(0,x.jsx)(`td`,{children:(0,x.jsxs)(`span`,{class:W,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),(0,x.jsx)(`td`,{children:e.profileName||`Default Profile`}),(0,x.jsx)(`td`,{class:G,children:rt(e.createdAt)}),(0,x.jsx)(`td`,{class:G,children:at(e.createdAt)}),(0,x.jsx)(`td`,{class:K,children:(0,x.jsx)(`button`,{type:`button`,class:q,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>(0,x.jsxs)(`tr`,{class:lt,children:[(0,x.jsxs)(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),(0,x.jsx)(`td`,{children:(0,x.jsx)(`span`,{class:W,children:`Open`})}),(0,x.jsx)(`td`,{class:ut,title:t.url,children:t.title||t.url||`about:blank`}),(0,x.jsx)(`td`,{class:G,children:rt(t.createdAt)}),(0,x.jsx)(`td`,{class:G,children:at(t.createdAt)}),(0,x.jsx)(`td`,{class:K,children:(0,x.jsx)(`button`,{type:`button`,class:q,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]},t.id))]},e.id))})]})})}function pt({stats:e}){return(0,x.jsxs)(`div`,{class:`stats-container`,children:[(0,x.jsxs)(`div`,{class:st,children:[(0,x.jsx)(`h3`,{children:`Active Browsers`}),(0,x.jsx)(`div`,{class:`value`,children:e.totalBrowsers})]}),(0,x.jsxs)(`div`,{class:st,children:[(0,x.jsx)(`h3`,{children:`Active Pages`}),(0,x.jsx)(`div`,{class:`value`,children:e.totalPages})]})]})}function mt({browsers:e,stats:t}){return(0,x.jsxs)(`div`,{class:`dashboard-container`,children:[(0,x.jsxs)(`div`,{class:`dashboard-header`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h1`,{children:`Playwright MCP Dashboard`}),(0,x.jsx)(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`button`,{type:`button`,id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),(0,x.jsx)(`button`,{type:`button`,id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),(0,x.jsx)(pt,{stats:t}),(0,x.jsx)(ft,{browsers:e}),(0,x.jsx)(`script`,{children:(0,y.raw)(`
|
|
6
6
|
class DashboardManager {
|
|
7
7
|
constructor() {
|
|
8
8
|
this.autoRefreshInterval = null;
|
|
@@ -40,7 +40,7 @@ class DashboardManager {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
updateStats(stats) {
|
|
43
|
-
const cards = document.querySelectorAll('.${
|
|
43
|
+
const cards = document.querySelectorAll('.${st} .value');
|
|
44
44
|
if (cards.length >= 2) {
|
|
45
45
|
cards[0].textContent = stats.totalBrowsers;
|
|
46
46
|
cards[1].textContent = stats.totalPages;
|
|
@@ -52,10 +52,10 @@ class DashboardManager {
|
|
|
52
52
|
if (!tbody) return;
|
|
53
53
|
|
|
54
54
|
if (browsers.length === 0) {
|
|
55
|
-
const container = tbody.closest('.${
|
|
55
|
+
const container = tbody.closest('.${ot}');
|
|
56
56
|
if (container) {
|
|
57
57
|
container.innerHTML = \`
|
|
58
|
-
<div class="${
|
|
58
|
+
<div class="${dt}">
|
|
59
59
|
<h3>No Active Browsers</h3>
|
|
60
60
|
<p>Launch a browser using MCP tools to see it here.</p>
|
|
61
61
|
</div>
|
|
@@ -69,29 +69,29 @@ class DashboardManager {
|
|
|
69
69
|
browsers.forEach(browser => {
|
|
70
70
|
// Browser row
|
|
71
71
|
const browserRow = document.createElement('tr');
|
|
72
|
-
browserRow.className = '${
|
|
72
|
+
browserRow.className = '${ct}';
|
|
73
73
|
browserRow.innerHTML = \`
|
|
74
74
|
<td>\${browser.id}</td>
|
|
75
|
-
<td><span class="${
|
|
75
|
+
<td><span class="${W}">\${browser.pages.length} page\${browser.pages.length !== 1 ? 's' : ''}</span></td>
|
|
76
76
|
<td>\${browser.profileName || 'Default Profile'}</td>
|
|
77
|
-
<td class="${
|
|
78
|
-
<td class="${
|
|
79
|
-
<td class="${
|
|
77
|
+
<td class="${G}">\${this.formatTimestamp(browser.createdAt)}</td>
|
|
78
|
+
<td class="${G}">\${this.formatAge(browser.createdAt)}</td>
|
|
79
|
+
<td class="${K}"><button class="${q}" onclick="dashboard.killBrowser('\${browser.id}')">Kill</button></td>
|
|
80
80
|
\`;
|
|
81
81
|
tbody.appendChild(browserRow);
|
|
82
82
|
|
|
83
83
|
// Page rows
|
|
84
84
|
browser.pages.forEach(page => {
|
|
85
85
|
const pageRow = document.createElement('tr');
|
|
86
|
-
pageRow.className = '${
|
|
86
|
+
pageRow.className = '${lt}';
|
|
87
87
|
const isActive = browser.currentPageId === page.id;
|
|
88
88
|
pageRow.innerHTML = \`
|
|
89
89
|
<td>\${page.id}\${isActive ? ' (active)' : ''}</td>
|
|
90
|
-
<td><span class="${
|
|
91
|
-
<td class="${
|
|
92
|
-
<td class="${
|
|
93
|
-
<td class="${
|
|
94
|
-
<td class="${
|
|
90
|
+
<td><span class="${W}">Open</span></td>
|
|
91
|
+
<td class="${ut}" title="\${this.escapeHtml(page.url)}">\${this.escapeHtml(page.title || page.url || 'about:blank')}</td>
|
|
92
|
+
<td class="${G}">\${this.formatTimestamp(page.createdAt)}</td>
|
|
93
|
+
<td class="${G}">\${this.formatAge(page.createdAt)}</td>
|
|
94
|
+
<td class="${K}"><button class="${q}" onclick="dashboard.killPage('\${page.id}')">Close</button></td>
|
|
95
95
|
\`;
|
|
96
96
|
tbody.appendChild(pageRow);
|
|
97
97
|
});
|
|
@@ -187,7 +187,7 @@ const dashboard = new DashboardManager();
|
|
|
187
187
|
window.addEventListener('beforeunload', () => {
|
|
188
188
|
dashboard.stopAutoRefresh();
|
|
189
189
|
});
|
|
190
|
-
`)})]})}function
|
|
190
|
+
`)})]})}function ht({title:e,children:t}){return(0,x.jsxs)(`html`,{lang:`en`,children:[(0,x.jsxs)(`head`,{children:[(0,x.jsx)(`meta`,{charset:`UTF-8`}),(0,x.jsx)(`meta`,{name:`viewport`,content:`width=device-width, initial-scale=1.0`}),(0,x.jsx)(`title`,{children:e}),(0,x.jsx)(`style`,{children:(0,y.raw)(`
|
|
191
191
|
* {
|
|
192
192
|
margin: 0;
|
|
193
193
|
padding: 0;
|
|
@@ -428,7 +428,7 @@ window.addEventListener('beforeunload', () => {
|
|
|
428
428
|
margin-bottom: 0.5rem;
|
|
429
429
|
color: #999;
|
|
430
430
|
}
|
|
431
|
-
`)})]}),(0,x.jsx)(`body`,{children:t})]})}function ht(t){let n=new g.Hono;return n.get(`/`,async n=>{try{let r=t.get(e.T.BrowserService),i=t.get(e.T.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,x.jsx)(mt,{title:`Playwright MCP Dashboard`,children:(0,x.jsx)(pt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html((0,x.jsx)(mt,{title:`Playwright MCP Dashboard - Error`,children:(0,x.jsxs)(`div`,{style:`padding: 2rem; text-align: center;`,children:[(0,x.jsx)(`h1`,{children:`Failed to load dashboard`}),(0,x.jsx)(`p`,{children:e instanceof Error?e.message:String(e)}),(0,x.jsx)(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function gt(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 _t(t){try{return t.get(e.T.TelemetryService)}catch{return new e.g}}function vt(t){let n=new g.Hono,r=t.get(e.T.PageRegistry),i=t.get(e.T.BrowserService),a=t.get(e.T.ExtensionTaskQueue),o=_t(t),s=new tt(r,a,o,i);n.use(`*`,(0,_.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.T.Tool).find(e=>e.getDefinition().name===i.tool);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(!s)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 c=await s.execute(i.arguments||{}),l=i.arguments||{},d=typeof l.pageId==`string`?l.pageId:void 0;if(d){let n=t.get(e.T.BrowserService),i=r.get(d);i&&n.recordBrowserActivity(i.browserId,d)}let f=gt(c);return f?(a?.setStatus({code:u.SpanStatusCode.ERROR,message:f}),o.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":d}})):o.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":d}}),n.json({success:!c.isError,result:c,error:f})})}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.T.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.T.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 s.executeTool(i,a.tool,a.arguments||{});if(c){let n=t.get(e.T.BrowserService),i=r.get(c);i&&n.recordBrowserActivity(i.browserId,c)}let f=gt(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=T(t);n.route(`/extension`,c);let l=nt(t);n.route(`/api`,l);let d=ht(t);return n.route(`/`,d),n}function yt(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.T.WebSocketHub).addConnection(a,r)}catch{a.close(1011,`Internal server error`)}},onMessage(t){if(i)try{let r=n.get(e.T.WebSocketHub),a=typeof t.data==`string`?t.data:t.data.toString();r.handleMessage(i,a)}catch{}},onClose(){if(i)try{n.get(e.T.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{n.get(e.T.WebSocketHub).removeConnection(i)}catch{}}}})),t.get(`/ws/stats`,t=>{try{let r=n.get(e.T.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 bt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function xt(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of bt)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 St=new m.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=j(`httpServe`),o={port:M(this,`port`,r.port,a.port===void 0?void 0:String(a.port),process.env.PLAYWRIGHT_PORT),headless:M(this,`headless`,r.headless,a.headless),idleTimeout:M(this,`idleTimeout`,r.idleTimeout,a.idleTimeout===void 0?void 0:String(a.idleTimeout),process.env[e.u]),host:M(this,`host`,r.host,a.host,process.env.PLAYWRIGHT_HOST),registryDir:M(this,`registryDir`,r.registryDir,a.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:M(this,`registryPath`,r.registryPath,a.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:M(this,`pidsDir`,r.pidsDir,a.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:M(this,`profilesDir`,r.profilesDir,a.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:M(this,`snippetsDir`,r.snippetsDir,a.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR),proxyConfigDir:M(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=xt(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.T.ProcessRegistryService),p=vt(d),{injectWebSocket:m,upgradeWebSocket:g}=(0,v.createNodeWebSocket)({app:p});yt(p,d,g);let _=d.get(e.T.WebSocketHub),y=d.get(e.T.ExtensionTaskQueue),b=d.get(e.T.BrowserService),x=d.get(e.T.ExtensionSessionRegistry),S=d.get(e.T.PageRegistry),C=d.get(e.T.IdleCleanupService);C.start(),_.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=b.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||``,_.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,b.registerExtensionBrowserWithId(i),a=S.registerExtensionPage(i);let e=b.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`}});_.sendSessionAck(e,t.id??``,s.id,s.controlMode),_.broadcastPageCreated(i,a,S.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=y.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&b.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,b.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,h.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.T.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)}}),Ct={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 wt(e){let t=new Set;for(let[n,r]of Object.entries(Ct))r.some(t=>e.includes(t))&&t.add(n);return t}const Tt=3e3,Et=[`browser_launch`],Dt=[`browser_new_page`];function Ot(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}}catch{return null}}async function kt(e){await new Promise(t=>setTimeout(t,e))}async function At(e){let t=new AbortController,n=setTimeout(()=>t.abort(),Tt);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 jt(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function Mt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),Tt);try{let r=await fetch(jt(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 Nt(e){let t;for(let n=1;n<=4;n+=1)try{return await At(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await kt(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function Pt(e,t){let n;for(let r=1;r<=4;r+=1)try{return await Mt(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await kt(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function Ft(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?wt(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 It(t){let{httpBaseUrl:n,sessionTracker:r,defaultMode:i,toolFilter:a,customToolsDir:o,enforcedProfileName:s,proxyConfigDir:c}=t,l=a?.tags?.length?Ft(a):null,u=new Set(a?.exclude??[]),p=l!==null||u.size>0,m=new d.Server({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 p?e.filter(e=>u.has(e.name)?!1:l===null?!0:l.has(e.name)):e}function b(e){return e.filter(e=>!u.has(e.name))}function x(e){return s?{...e,profileName:s}:e}function S(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 C(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 w(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function T(e){return u.has(e)?!1:l===null?!0:l.has(e)}return m.setRequestHandler(f.ListToolsRequestSchema,async()=>{try{let e=await Nt(n);g=e,v=new Set(e.map(e=>e.name));let t=o?await Pt(n,o):[];_=t;let i=[...y(e),...b(t).map(e=>w(e))];return r&&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),...b(_).map(e=>w(e))];return r&&t.push(h),{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})}}),m.setRequestHandler(f.CallToolRequestSchema,async e=>{let{name:t,arguments:a}=e.params,s=new Set(_.map(e=>e.name));if(t!==`browser_list_session`&&!T(t))return{content:[{type:`text`,text:`Tool "${t}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(t===`browser_list_session`&&r){let e=r.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let c=a||{};t===`browser_launch`&&i&&(c={...c,mode:i}),t===`browser_launch`&&(c=x(c),c=S(c));try{if(o&&!s.has(t)&&!v.has(t))try{_=await Pt(n,o),s=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=s.has(t),i=await fetch(e&&o?jt(n,o):`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:t,arguments:c})});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(Et.includes(t)){let e=Ot(a.result);e?.browserId&&(r.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&r.trackPage(e.pageId,e.browserId,e.url))}else if(Dt.includes(t)){let e=Ot(a.result);e?.pageId&&e?.browserId&&r.trackPage(e.pageId,e.browserId,e.url)}}return t===`browser_list_profiles`&&a.result?C(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}}}),m}const Y=`stdio`,Lt=`http`,X=`streamable-http`,Rt=new Set([Y,Lt,X]),zt=`chromium`,Bt=new Set([zt,`firefox`,`webkit`]),Vt=new Set([`playwright`,`extension`,`vm`,`stealth`]),Z=`SIGINT`,Q=`SIGTERM`,Ht=`PLAYWRIGHT_PORT`,Ut=[`pnpm-workspace.yaml`,`nx.json`,`.git`],Wt=`browse-tool-mcp-http`,Gt=`service`,Kt=t.DEFAULT_PORT_RANGE.min;var qt=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...Rt].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},Jt=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...Bt].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},Yt=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...Vt].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function Xt(e){let t=e.toLowerCase();if(!Bt.has(t))throw new Jt(e);return t}function Zt(e){let t=e.toLowerCase();if(!Vt.has(t))throw new Yt(e);return t}function Qt(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 $t(e){return e!=null&&String(e).trim()!==``}function en(e){let t=e.toLowerCase();return t===Lt?X:t}function tn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function nn(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of Ut)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 rn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function an(e){let t=e.tags?rn(e.tags):void 0,n=e.exclude?rn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var on=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}},sn=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}},cn=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`}},ln=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 un(e,t){let n=e.getBrowserIds(),r=[],i;for(let e of n)try{let n=await fetch(`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:e}})});if(!n.ok){let e=await n.text().catch(()=>``);throw Error(`HTTP ${n.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),r.push(e),i??=n}if(r.length>0)throw new on(r,t,{cause:i});e.clear()}async function dn(e,t,n,r){await e.start();let i=!1,a=async a=>{if(i)return;i=!0,console.error(`\nReceived ${a}, shutting down gracefully...`);let o;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await un(t,n))}catch(e){o=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,o)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),o??=t}try{await r?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),o??=t}if(o){let e=new sn(a,{cause:o});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(Z,()=>a(Z)),process.once(Q,()=>a(Q))}const fn=new m.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: ${[...Bt].join(`, `)}`,zt).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(Kt)).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: ${[...Vt].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=j(`mcpServe`),a={type:M(this,`type`,t.type,r.type),browser:M(this,`browser`,t.browser,r.browser),headless:M(this,`headless`,t.headless,r.headless),profile:M(this,`profile`,t.profile,r.profile),mode:M(this,`mode`,t.mode,r.mode),host:M(this,`host`,t.host,r.host,process.env.PLAYWRIGHT_HOST),port:M(this,`port`,t.port,r.port,process.env.PLAYWRIGHT_MCP_PORT),httpPort:M(this,`httpPort`,t.httpPort,r.httpPort,process.env[Ht]),tags:M(this,`tags`,t.tags,r.tags),exclude:M(this,`exclude`,t.exclude,r.exclude),customTools:M(this,`customTools`,t.customTools,r.customTools),chromeForTestingPath:M(this,`chromeForTestingPath`,t.chromeForTestingPath,r.chromeForTestingPath,process.env[e.x]),snippetsDir:M(this,`snippetsDir`,t.snippetsDir,r.snippetsDir),idleTimeout:M(this,`idleTimeout`,t.idleTimeout,r.idleTimeout===void 0?void 0:String(r.idleTimeout),process.env[e.u]),registryPath:M(this,`registryPath`,t.registryPath,r.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:M(this,`registryDir`,t.registryDir,r.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:M(this,`pidsDir`,t.pidsDir,r.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:M(this,`profilesDir`,t.profilesDir,r.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),proxyConfigDir:M(this,`proxyConfigDir`,t.proxyConfigDir,r.proxyConfigDir,process.env[e.s])},o=en(a.type),s=nn(process.cwd()),c=process.env.NODE_ENV||`development`,l,u;try{if(!Rt.has(o))throw new qt(o);let t=Xt(a.browser),r=a.mode?Zt(a.mode):void 0,d=Qt(a.port??Kt),f=$t(a.httpPort),p=f?Qt(a.httpPort):void 0,m=an(a),h=a.customTools?i.default.resolve(a.customTools):void 0,g=a.chromeForTestingPath?i.default.resolve(a.chromeForTestingPath):void 0,_=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()}:${d}/mcp`),console.error(` Default browser: ${t}`),console.error(` Headless: ${a.headless}`),r&&console.error(` Default mode: ${r}`),a.profile&&console.error(` Profile: ${a.profile}`),m?.tags?.length&&console.error(` Tags: ${m.tags.join(`, `)}`),m?.exclude?.length&&console.error(` Exclude: ${m.exclude.join(`, `)}`),h&&console.error(` Custom tools: ${h}`),g&&console.error(` Chrome for Testing path: ${g}`),_&&console.error(` Snippets dir: ${_}`),a.idleTimeout&&console.error(` Idle timeout: ${a.idleTimeout} minutes`);let v=a.registryPath||a.registryDir;v&&(process.env.PLAYWRIGHT_REGISTRY_DIR=v,process.env.PORT_REGISTRY_PATH=v,process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(v,`processes.json`),console.error(` Registry path: ${v}`)),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]}`)),_&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=_),g&&(process.env[e.x]=g),a.idleTimeout&&(process.env[e.u]=a.idleTimeout),a.host&&(process.env.PLAYWRIGHT_HOST=a.host);let y=e.o(),b=y.get(e.T.ProcessRegistryService);u=await y.get(e.T.McpPortAllocationService).acquirePorts({repositoryPath:s,environment:c,host:a.host??e.p(),pid:process.pid,preferredMcpPort:o===X?d:void 0,preferredHttpPort:p,exactHttpPort:f,reserveMcpPort:o===X,mcpServiceName:Wt,mcpServiceType:`tool`,mcpMetadata:o===X?{healthCheckUrl:`${e.f(a.host??e.p(),d)}/health`,transport:X}:void 0});let x=u.httpPort;process.env[Ht]=x.toString();let S=await y.get(e.T.HttpServerManager).ensureRunning(x,{exactPort:!0});if(!S.running)throw new cn;let C=e.f(e.p(),S.port),w=S.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${w} on port ${S.port}`),o===Y){let i=new e.l,o=new e.n(It({httpBaseUrl:C,sessionTracker:i,defaultMode:r,toolFilter:m,customToolsDir:h,enforcedProfileName:a.profile,proxyConfigDir:process.env[e.s]}));l=await(0,n.createProcessLease)({repositoryPath:s,serviceName:`browse-tool-mcp`,serviceType:Gt,environment:c,pid:process.pid,metadata:{transport:Y,browser:t,mode:r}},b),await dn(o,i,C,async()=>{await l?.release({kill:!1}),await u?.release()});return}let T=u.mcpPort;if(!T)throw Error(`Failed to reserve ${X} port ${d}`);l=await(0,n.createProcessLease)({repositoryPath:s,serviceName:Wt,serviceType:Gt,environment:c,pid:process.pid,port:T,host:a.host??e.p(),metadata:{healthCheckUrl:`${e.f(a.host??e.p(),T)}/health`,transport:X}},b);let E=new e.t(({headers:t})=>{let n=tn(t,`x-profile`)??a.profile,i=new e.l;return{server:It({httpBaseUrl:C,sessionTracker:i,defaultMode:r,toolFilter:m,customToolsDir:h,enforcedProfileName:n,proxyConfigDir:process.env[e.s]}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await un(i,C)}}},{host:a.host??e.p(),port:T});try{await E.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 D=!1,O=async e=>{if(!D){D=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await E.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,()=>O(Z)),process.once(Q,()=>O(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 qt||e instanceof Jt||e instanceof Yt||e instanceof cn)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new ln({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)}}),pn=new m.Command(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=j(`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.T.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,x.jsx)(`body`,{children:t})]})}function gt(t){let n=new g.Hono;return n.get(`/`,async n=>{try{let r=t.get(e.T.BrowserService),i=t.get(e.T.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,x.jsx)(ht,{title:`Playwright MCP Dashboard`,children:(0,x.jsx)(mt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html((0,x.jsx)(ht,{title:`Playwright MCP Dashboard - Error`,children:(0,x.jsxs)(`div`,{style:`padding: 2rem; text-align: center;`,children:[(0,x.jsx)(`h1`,{children:`Failed to load dashboard`}),(0,x.jsx)(`p`,{children:e instanceof Error?e.message:String(e)}),(0,x.jsx)(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function _t(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 vt(t){try{return t.get(e.T.TelemetryService)}catch{return new e.g}}function yt(t){let n=new g.Hono,r=t.get(e.T.PageRegistry),i=t.get(e.T.BrowserService),a=t.get(e.T.ExtensionTaskQueue),o=vt(t),s=new tt(r,a,o,i);n.use(`*`,(0,_.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.T.Tool).find(e=>e.getDefinition().name===i.tool);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(!s)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 c=await s.execute(i.arguments||{}),l=i.arguments||{},d=typeof l.pageId==`string`?l.pageId:void 0;if(d){let n=t.get(e.T.BrowserService),i=r.get(d);i&&n.recordBrowserActivity(i.browserId,d)}let f=_t(c);return f?(a?.setStatus({code:u.SpanStatusCode.ERROR,message:f}),o.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":d}})):o.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":d}}),n.json({success:!c.isError,result:c,error:f})})}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.T.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.T.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 s.executeTool(i,a.tool,a.arguments||{});if(c){let n=t.get(e.T.BrowserService),i=r.get(c);i&&n.recordBrowserActivity(i.browserId,c)}let f=_t(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=T(t);n.route(`/extension`,c);let l=nt(t);n.route(`/api`,l);let d=gt(t);return n.route(`/`,d),n}function bt(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.T.WebSocketHub).addConnection(a,r)}catch{a.close(1011,`Internal server error`)}},onMessage(t){if(i)try{let r=n.get(e.T.WebSocketHub),a=typeof t.data==`string`?t.data:t.data.toString();r.handleMessage(i,a)}catch{}},onClose(){if(i)try{n.get(e.T.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{n.get(e.T.WebSocketHub).removeConnection(i)}catch{}}}})),t.get(`/ws/stats`,t=>{try{let r=n.get(e.T.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 xt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function St(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of xt)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 Ct=new m.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=j(`httpServe`),o={port:M(this,`port`,r.port,a.port===void 0?void 0:String(a.port),process.env.PLAYWRIGHT_PORT),headless:M(this,`headless`,r.headless,a.headless),idleTimeout:M(this,`idleTimeout`,r.idleTimeout,a.idleTimeout===void 0?void 0:String(a.idleTimeout),process.env[e.u]),host:M(this,`host`,r.host,a.host,process.env.PLAYWRIGHT_HOST),registryDir:M(this,`registryDir`,r.registryDir,a.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:M(this,`registryPath`,r.registryPath,a.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:M(this,`pidsDir`,r.pidsDir,a.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:M(this,`profilesDir`,r.profilesDir,a.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:M(this,`snippetsDir`,r.snippetsDir,a.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR),proxyConfigDir:M(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=St(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.T.ProcessRegistryService),p=yt(d),{injectWebSocket:m,upgradeWebSocket:g}=(0,v.createNodeWebSocket)({app:p});bt(p,d,g);let _=d.get(e.T.WebSocketHub),y=d.get(e.T.ExtensionTaskQueue),b=d.get(e.T.BrowserService),x=d.get(e.T.ExtensionSessionRegistry),S=d.get(e.T.PageRegistry),C=d.get(e.T.IdleCleanupService);C.start(),_.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=b.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||``,_.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,b.registerExtensionBrowserWithId(i),a=S.registerExtensionPage(i);let e=b.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`}});_.sendSessionAck(e,t.id??``,s.id,s.controlMode),_.broadcastPageCreated(i,a,S.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=y.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&b.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,b.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,h.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.T.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)}}),wt={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 Tt(e){let t=new Set;for(let[n,r]of Object.entries(wt))r.some(t=>e.includes(t))&&t.add(n);return t}const Et=3e3,Dt=[`browser_launch`],Ot=[`browser_new_page`],kt=[`browser_close`],At=[`browser_close_page`];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 jt(e){await new Promise(t=>setTimeout(t,e))}async function Mt(e){let t=new AbortController,n=setTimeout(()=>t.abort(),Et);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 Nt(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function Pt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),Et);try{let r=await fetch(Nt(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 Ft(e){let t;for(let n=1;n<=4;n+=1)try{return await Mt(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await jt(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function It(e,t){let n;for(let r=1;r<=4;r+=1)try{return await Pt(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await jt(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function Lt(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?Tt(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 Rt(t){let{httpBaseUrl:n,sessionTracker:r,defaultMode:i,toolFilter:a,customToolsDir:o,enforcedProfileName:s,proxyConfigDir:c}=t,l=a?.tags?.length?Lt(a):null,u=new Set(a?.exclude??[]),p=l!==null||u.size>0,m=new d.Server({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 p?e.filter(e=>u.has(e.name)?!1:l===null?!0:l.has(e.name)):e}function b(e){return e.filter(e=>!u.has(e.name))}function x(e){return s?{...e,profileName:s}:e}function S(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 C(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 w(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function T(e){return u.has(e)?!1:l===null?!0:l.has(e)}return m.setRequestHandler(f.ListToolsRequestSchema,async()=>{try{let e=await Ft(n);g=e,v=new Set(e.map(e=>e.name));let t=o?await It(n,o):[];_=t;let i=[...y(e),...b(t).map(e=>w(e))];return r&&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),...b(_).map(e=>w(e))];return r&&t.push(h),{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})}}),m.setRequestHandler(f.CallToolRequestSchema,async e=>{let{name:t,arguments:a}=e.params,s=new Set(_.map(e=>e.name));if(t!==`browser_list_session`&&!T(t))return{content:[{type:`text`,text:`Tool "${t}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(t===`browser_list_session`&&r){let e=r.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let c=a||{};t===`browser_launch`&&i&&c.mode===void 0&&(c={...c,mode:i}),t===`browser_launch`&&(c=x(c),c=S(c));try{if(o&&!s.has(t)&&!v.has(t))try{_=await It(n,o),s=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=s.has(t),i=await fetch(e&&o?Nt(n,o):`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:t,arguments:c})});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(Dt.includes(t)){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(Ot.includes(t)){let e=J(a.result);e?.pageId&&e?.browserId&&r.trackPage(e.pageId,e.browserId,e.url)}else if(kt.includes(t)){let e=J(a.result);e?.closedBrowserId&&r.untrackBrowser(e.closedBrowserId)}else if(At.includes(t)){let e=J(a.result);e?.closedPageId&&r.untrackPage(e.closedPageId),e?.browserId&&e.browserKeptOpen===!1&&r.untrackBrowser(e.browserId)}}return t===`browser_list_profiles`&&a.result?C(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}}}),m}const Y=`stdio`,zt=`http`,X=`streamable-http`,Bt=new Set([Y,zt,X]),Vt=`chromium`,Ht=new Set([Vt,`firefox`,`webkit`]),Ut=new Set([`playwright`,`extension`,`vm`,`stealth`]),Z=`SIGINT`,Q=`SIGTERM`,Wt=`PLAYWRIGHT_PORT`,Gt=[`pnpm-workspace.yaml`,`nx.json`,`.git`],Kt=`browse-tool-mcp-http`,qt=`service`,Jt=t.DEFAULT_PORT_RANGE.min;var Yt=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...Bt].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},Xt=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...Ht].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},Zt=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...Ut].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function Qt(e){let t=e.toLowerCase();if(!Ht.has(t))throw new Xt(e);return t}function $t(e){let t=e.toLowerCase();if(!Ut.has(t))throw new Zt(e);return t}function en(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 tn(e){return e!=null&&String(e).trim()!==``}function nn(e){let t=e.toLowerCase();return t===zt?X:t}function rn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function an(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of Gt)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 on(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function sn(e){let t=e.tags?on(e.tags):void 0,n=e.exclude?on(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var cn=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}},ln=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}},un=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`}},dn=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 fn(e,t){let n=e.getBrowserIds(),r=[],i;for(let e of n)try{let n=await fetch(`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:e}})});if(!n.ok){let e=await n.text().catch(()=>``);throw Error(`HTTP ${n.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),r.push(e),i??=n}if(r.length>0)throw new cn(r,t,{cause:i});e.clear()}async function pn(e,t,n,r){await e.start();let i=!1,a=async a=>{if(i)return;i=!0,console.error(`\nReceived ${a}, shutting down gracefully...`);let o;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await fn(t,n))}catch(e){o=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,o)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),o??=t}try{await r?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),o??=t}if(o){let e=new ln(a,{cause:o});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(Z,()=>a(Z)),process.once(Q,()=>a(Q))}const mn=new m.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: ${[...Ht].join(`, `)}`,Vt).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(Jt)).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: ${[...Ut].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=j(`mcpServe`),a={type:M(this,`type`,t.type,r.type),browser:M(this,`browser`,t.browser,r.browser),headless:M(this,`headless`,t.headless,r.headless),profile:M(this,`profile`,t.profile,r.profile),mode:M(this,`mode`,t.mode,r.mode),host:M(this,`host`,t.host,r.host,process.env.PLAYWRIGHT_HOST),port:M(this,`port`,t.port,r.port,process.env.PLAYWRIGHT_MCP_PORT),httpPort:M(this,`httpPort`,t.httpPort,r.httpPort,process.env[Wt]),tags:M(this,`tags`,t.tags,r.tags),exclude:M(this,`exclude`,t.exclude,r.exclude),customTools:M(this,`customTools`,t.customTools,r.customTools),chromeForTestingPath:M(this,`chromeForTestingPath`,t.chromeForTestingPath,r.chromeForTestingPath,process.env[e.x]),snippetsDir:M(this,`snippetsDir`,t.snippetsDir,r.snippetsDir),idleTimeout:M(this,`idleTimeout`,t.idleTimeout,r.idleTimeout===void 0?void 0:String(r.idleTimeout),process.env[e.u]),registryPath:M(this,`registryPath`,t.registryPath,r.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:M(this,`registryDir`,t.registryDir,r.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:M(this,`pidsDir`,t.pidsDir,r.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:M(this,`profilesDir`,t.profilesDir,r.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),proxyConfigDir:M(this,`proxyConfigDir`,t.proxyConfigDir,r.proxyConfigDir,process.env[e.s])},o=nn(a.type),s=an(process.cwd()),c=process.env.NODE_ENV||`development`,l,u;try{if(!Bt.has(o))throw new Yt(o);let t=Qt(a.browser),r=a.mode?$t(a.mode):void 0,d=en(a.port??Jt),f=tn(a.httpPort),p=f?en(a.httpPort):void 0,m=sn(a),h=a.customTools?i.default.resolve(a.customTools):void 0,g=a.chromeForTestingPath?i.default.resolve(a.chromeForTestingPath):void 0,_=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()}:${d}/mcp`),console.error(` Default browser: ${t}`),console.error(` Headless: ${a.headless}`),r&&console.error(` Default mode: ${r}`),a.profile&&console.error(` Profile: ${a.profile}`),m?.tags?.length&&console.error(` Tags: ${m.tags.join(`, `)}`),m?.exclude?.length&&console.error(` Exclude: ${m.exclude.join(`, `)}`),h&&console.error(` Custom tools: ${h}`),g&&console.error(` Chrome for Testing path: ${g}`),_&&console.error(` Snippets dir: ${_}`),a.idleTimeout&&console.error(` Idle timeout: ${a.idleTimeout} minutes`);let v=a.registryPath||a.registryDir;v&&(process.env.PLAYWRIGHT_REGISTRY_DIR=v,process.env.PORT_REGISTRY_PATH=v,process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(v,`processes.json`),console.error(` Registry path: ${v}`)),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]}`)),_&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=_),g&&(process.env[e.x]=g),a.idleTimeout&&(process.env[e.u]=a.idleTimeout),a.host&&(process.env.PLAYWRIGHT_HOST=a.host);let y=e.o(),b=y.get(e.T.ProcessRegistryService);u=await y.get(e.T.McpPortAllocationService).acquirePorts({repositoryPath:s,environment:c,host:a.host??e.p(),pid:process.pid,preferredMcpPort:o===X?d:void 0,preferredHttpPort:p,exactHttpPort:f,reserveMcpPort:o===X,mcpServiceName:Kt,mcpServiceType:`tool`,mcpMetadata:o===X?{healthCheckUrl:`${e.f(a.host??e.p(),d)}/health`,transport:X}:void 0});let x=u.httpPort;process.env[Wt]=x.toString();let S=await y.get(e.T.HttpServerManager).ensureRunning(x,{exactPort:!0});if(!S.running)throw new un;let C=e.f(e.p(),S.port),w=S.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${w} on port ${S.port}`),o===Y){let i=new e.l,o=new e.n(Rt({httpBaseUrl:C,sessionTracker:i,defaultMode:r,toolFilter:m,customToolsDir:h,enforcedProfileName:a.profile,proxyConfigDir:process.env[e.s]}));l=await(0,n.createProcessLease)({repositoryPath:s,serviceName:`browse-tool-mcp`,serviceType:qt,environment:c,pid:process.pid,metadata:{transport:Y,browser:t,mode:r}},b),await pn(o,i,C,async()=>{await l?.release({kill:!1}),await u?.release()});return}let T=u.mcpPort;if(!T)throw Error(`Failed to reserve ${X} port ${d}`);l=await(0,n.createProcessLease)({repositoryPath:s,serviceName:Kt,serviceType:qt,environment:c,pid:process.pid,port:T,host:a.host??e.p(),metadata:{healthCheckUrl:`${e.f(a.host??e.p(),T)}/health`,transport:X}},b);let E=new e.t(({headers:t})=>{let n=rn(t,`x-profile`)??a.profile,i=new e.l;return{server:Rt({httpBaseUrl:C,sessionTracker:i,defaultMode:r,toolFilter:m,customToolsDir:h,enforcedProfileName:n,proxyConfigDir:process.env[e.s]}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await fn(i,C)}}},{host:a.host??e.p(),port:T});try{await E.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 D=!1,O=async e=>{if(!D){D=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await E.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,()=>O(Z)),process.once(Q,()=>O(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 Yt||e instanceof Xt||e instanceof Zt||e instanceof un)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new dn({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)}}),hn=new m.Command(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=j(`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.T.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)}}),gn=new m.Command(`stop`).description(`Stop HTTP server and clean up registry/PID files`).action(async()=>{try{let t=j(`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.T.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)}}),_n=new Set([`--config`]);function vn(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(_n.has(n)){t+=1;continue}if(![..._n].some(e=>n.startsWith(`${e}=`))&&!n.startsWith(`-`))return n}}function yn(e,t=!1){if(t)return!1;let n=vn(e);return n?n===`tools`?!0:n===`help`&&e.includes(`tools`):!1}function bn(e){return e.replace(/_/g,`-`)}function $(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function xn(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 Sn(e,t){let n=xn(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 Cn(e,t){let n=$(e),r=xn(t);return t.type===`boolean`?`--${n}`:r.has(`boolean`)&&r.has(`object`)?`--${n} [value]`:`--${n} <value>`}function wn(t){let{definition:n,execute:r}=t,i=new m.Command(bn(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=Cn(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 m.Option(r,a).choices(n.enum)):i.option(r,a)}return i.action(async function(){let t=this.optsWithGlobals(),i=j(`tools`),a=M(this,`format`,t.format??`json`,i.format),c=M(this,`color`,t.color??!0,i.color),l=M(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]=Sn(s,i):e[n]=s)}for(let t of s)if(e[t]===void 0){let e=$(t);console.error(I(`Missing required option: --${e}`,u.color)),process.exit(1)}let i=Number.parseInt(l,10),a=N({port:i}),c=r?await r(e,{command:this,formatterOptions:u,port:i}):await a.execute(n.name,e),f=F(c,u);f&&console.log(f),c.isError&&process.exit(1)}catch(e){console.error(I(e instanceof Error?e:String(e),u.color)),process.exit(1)}}),i}function Tn(e){return e.map(wn)}const En={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 Dn(){return new m.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 On(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 kn(){return{definition:En,execute:async(e,t)=>{let n=await N({port:t.port}).listBrowsers();return{content:[{type:`text`,text:JSON.stringify(On(n),null,2)}]}}}}function An(e){return[...e.map(e=>({definition:e})),kn()]}async function jn(t,n){let r=j(`tools`),i=Number(n?.port??process.env.PLAYWRIGHT_PORT??r.port??e.d);try{let e=Tn(An(await N({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(`${I(`Failed to load tool commands: ${t}`,!0)}\n`),process.stderr.write(`Make sure the HTTP server is running: browse-tool http-serve
|
|
434
|
+
`)}}function Mn(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 Nn(){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 m.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(S),r.addCommand(mn),r.addCommand(Ct),r.addCommand(se),r.addCommand(Fe),r.addCommand(gn),r.addCommand(hn),r.addCommand(Ie),r.addCommand(Ne),r.addCommand(Pe);let i=Dn();yn(process.argv.slice(2),process.env.PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS===`1`)&&await jn(i,{port:Mn(process.argv.slice(2),n)}),r.addCommand(i),await r.parseAsync(process.argv)}Nn().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,S as t,T as n,_ as r,a as i,b as a,c as o,d as s,f as c,g as l,h as u,i as d,l as f,m as p,n as m,o as h,p as g,s as _,t as v,u as y,v as b,w as x,x as S,y as C}from"./streamable-http-CuNMjfum.mjs";import{stripTypeScriptTypes as w}from"node:module";import"reflect-metadata/lite";import{DEFAULT_PORT_RANGE as T,PortRegistryService as E}from"@agimon-ai/foundation-port-registry";import{createProcessLease as ee,resolveSiblingRegistryPath as te}from"@agimon-ai/foundation-process-registry";import{Container as D,ContainerModule as O}from"inversify";import k from"node:path";import{existsSync as A,readFileSync as j,statSync as ne}from"node:fs";import{readFile as M,stat as re}from"node:fs/promises";import{homedir as ie}from"node:os";import{z as N}from"zod";import{SpanStatusCode as P}from"@opentelemetry/api";import{Server as ae}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as oe,ListToolsRequestSchema as se}from"@modelcontextprotocol/sdk/types.js";import{StdioServerTransport as ce}from"@modelcontextprotocol/sdk/server/stdio.js";import{Command as F,Option as le}from"commander";import{serve as ue}from"@hono/node-server";import{Hono as I}from"hono";import{cors as de}from"hono/cors";import{createNodeWebSocket as fe}from"@hono/node-ws";import{raw as pe}from"hono/html";import{Fragment as me}from"hono/jsx";import{jsx as L,jsxs as R}from"hono/jsx/jsx-runtime";var he=`0.8.4`;const ge=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function _e(e,t){if(!ge)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function ve(e){let t=new I,i;try{i=e.get(n.TelemetryService)}catch{i=new l}return t.get(`/telemetry-config`,e=>{try{let t=new URL(e.req.url).origin;return e.json(r(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 r=e.get(n.ExtensionTaskQueue).getNextTask();return r?t.json({task:{id:r.id,tool:r.tool,arguments:r.arguments,telemetry:r.telemetry}}):t.json({})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/result`,async t=>{try{let r=await t.req.json();if(!r.taskId)return t.json({success:!1,error:`Missing taskId in request body`},400);let i=e.get(n.ExtensionTaskQueue),a=e.get(n.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),t.json({success:!0})):t.json({success:!1,error:`Task ${r.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 r=e.get(n.ExtensionTaskQueue),i=r.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:r.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 r=await t.req.json();if(!r.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).register(r);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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).heartbeat(r);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 ${r.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 r=await t.req.json();if(!r.pageId||typeof r.tabId!=`number`)return t.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=e.get(n.PageRegistry),a=e.get(n.BrowserService),o=i.get(r.pageId);return o?(o.extensionTabId=r.tabId,a.recordBrowserActivity(o.browserId,r.pageId),t.json({success:!0})):t.json({success:!1,error:`Page ${r.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 r=await t.req.json();if(!r.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(n.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),t.json({success:!0})):t.json({success:!1,error:`Browser "${r.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 r=await t.req.json();if(!r.browserId||!r.chunkBase64)return t.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=e.get(n.BrowserService),o=await a.persistExtensionRecordingChunk(r.browserId,r.chunkBase64);return _e(`chunk received`,{browserId:r.browserId,chunkIndex:r.chunkIndex,mimeType:r.mimeType,persisted:!!o,chunkBase64Size:r.chunkBase64.length}),o?(i.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":r.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof r.chunkIndex==`number`?{"browse_tool.recording.chunk_index":r.chunkIndex}:{},...typeof r.mimeType==`string`?{"browse_tool.recording.mime_type":r.mimeType}:{}}}),a.recordBrowserActivity(r.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${r.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):(i.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)}}),t.post(`/handoff`,async t=>{try{let r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).requestHandoff(r);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 ${r.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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).acknowledgeHandoff(r.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 ${r.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 r=e.get(n.ExtensionSessionRegistry).listSessions();return t.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 t.json({error:e instanceof Error?e.message:String(e)},500)}}),t}function ye(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 be=[`pnpm-workspace.yaml`,`nx.json`,`.git`],xe=`browse-tool-chrome`,Se=`tool`,Ce=process.env.NODE_ENV||`development`,we=`127.0.0.1`;function Te(e=process.cwd()){let t=k.resolve(e);for(;;){for(let e of be)if(A(k.join(t,e)))return t;let e=k.dirname(t);if(e===t)return process.cwd();t=e}}function Ee(){return new E(process.env.PORT_REGISTRY_PATH)}async function De(e,t,n){let r=Ee(),i=await r.reservePort({repositoryPath:e,serviceName:xe,serviceType:Se,environment:Ce,preferredPort:t,portRange:n,pid:process.pid,host:we,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:xe,serviceType:Se,environment:Ce,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function Oe(){return new O(e=>{e.bind(n.ExtensionTaskQueue).to(u).inSingletonScope(),e.bind(n.ExtensionToolDelegator).to(p).inSingletonScope()})}function ke(e){let t=new ae({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(se,async()=>({tools:n})),t.setRequestHandler(oe,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const Ae=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,r;try{let i=ye(e.port),a=Te(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=te(o,`processes.json`)),t=await De(a,i??T.min,i?{min:i,max:i}:T);let s=t.port;r=await ee({repositoryPath:a,serviceName:xe,serviceType:Se,environment:Ce,pid:process.pid,port:s,host:we,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 D({defaultScope:`Singleton`});c.load(Oe());let l=c.get(n.ExtensionTaskQueue),u=c.get(n.ExtensionToolDelegator),d=new I;d.use(`*`,de({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=ve(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=ue({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=ke(u),h=new ce;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 r.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(r||t)try{await r?.release(),await t?.release()}catch{}let n=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${n}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),je=`BROWSE_TOOL_CONFIG`,Me=`.browse-tool`,Ne=`config.json`,Pe=[`json`,`text`,`quiet`],Fe={commands:{},tools:{}},Ie=N.record(N.string(),N.unknown()),Le=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(Pe).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),tools:N.object({format:N.enum(Pe).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()}),Re=N.object({commands:Le.default({}),tools:N.record(N.string(),Ie).default({})});let z={config:Fe};function ze(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 Be(e){let t=k.resolve(e);if(!A(t))throw Error(`Config path not found: ${t}`);return ne(t).isDirectory()?k.join(t,Ne):t}function Ve(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??ie(),a=ze(e);if(a)return Be(a);if(n[je])return Be(n[je]);let o=k.join(r,Me,Ne);if(A(o))return o;let s=k.join(i,Me,Ne);if(A(s))return s}function He(e){let t=j(e,`utf8`),n=JSON.parse(t);return Re.parse(n)}function Ue(e,t={}){let n=Ve(e,t);return n?{configPath:n,config:He(n)}:{config:Fe}}function We(e,t={}){return z=Ue(e,t),z}function B(e){return(z.config.commands??{})[e]??{}}function Ge(e){return z.config.tools?.[e]??{}}function V(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 Ke=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??s,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let e=await h().get(n.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 H(e){return new Ke(e)}const qe={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 U(e,t,n){return n?`${qe[t]}${e}${qe.reset}`:e}function Je(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Ye(e):Xe(e,r)}function Ye(e){return JSON.stringify(e,null,2)}function Xe(e,t){let n=[];e.isError&&n.push(U(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(Ze(r,t)):r.type===`image`?n.push(et(r,t)):n.push(U(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
2
|
+
import{C as e,S as t,T as n,_ as r,a as i,b as a,c as o,d as s,f as c,g as l,h as u,i as d,l as f,m as p,n as m,o as h,p as g,s as _,t as v,u as y,v as b,w as x,x as S,y as C}from"./streamable-http-CuNMjfum.mjs";import{stripTypeScriptTypes as w}from"node:module";import"reflect-metadata/lite";import{DEFAULT_PORT_RANGE as T,PortRegistryService as E}from"@agimon-ai/foundation-port-registry";import{createProcessLease as ee,resolveSiblingRegistryPath as te}from"@agimon-ai/foundation-process-registry";import{Container as D,ContainerModule as O}from"inversify";import k from"node:path";import{existsSync as A,readFileSync as j,statSync as ne}from"node:fs";import{readFile as M,stat as re}from"node:fs/promises";import{homedir as ie}from"node:os";import{z as N}from"zod";import{SpanStatusCode as P}from"@opentelemetry/api";import{Server as ae}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as oe,ListToolsRequestSchema as se}from"@modelcontextprotocol/sdk/types.js";import{StdioServerTransport as ce}from"@modelcontextprotocol/sdk/server/stdio.js";import{Command as F,Option as le}from"commander";import{serve as ue}from"@hono/node-server";import{Hono as I}from"hono";import{cors as de}from"hono/cors";import{createNodeWebSocket as fe}from"@hono/node-ws";import{raw as pe}from"hono/html";import{Fragment as me}from"hono/jsx";import{jsx as L,jsxs as R}from"hono/jsx/jsx-runtime";var he=`0.8.6`;const ge=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function _e(e,t){if(!ge)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function ve(e){let t=new I,i;try{i=e.get(n.TelemetryService)}catch{i=new l}return t.get(`/telemetry-config`,e=>{try{let t=new URL(e.req.url).origin;return e.json(r(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 r=e.get(n.ExtensionTaskQueue).getNextTask();return r?t.json({task:{id:r.id,tool:r.tool,arguments:r.arguments,telemetry:r.telemetry}}):t.json({})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/result`,async t=>{try{let r=await t.req.json();if(!r.taskId)return t.json({success:!1,error:`Missing taskId in request body`},400);let i=e.get(n.ExtensionTaskQueue),a=e.get(n.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),t.json({success:!0})):t.json({success:!1,error:`Task ${r.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 r=e.get(n.ExtensionTaskQueue),i=r.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:r.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 r=await t.req.json();if(!r.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).register(r);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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).heartbeat(r);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 ${r.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 r=await t.req.json();if(!r.pageId||typeof r.tabId!=`number`)return t.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=e.get(n.PageRegistry),a=e.get(n.BrowserService),o=i.get(r.pageId);return o?(o.extensionTabId=r.tabId,a.recordBrowserActivity(o.browserId,r.pageId),t.json({success:!0})):t.json({success:!1,error:`Page ${r.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 r=await t.req.json();if(!r.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(n.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),t.json({success:!0})):t.json({success:!1,error:`Browser "${r.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 r=await t.req.json();if(!r.browserId||!r.chunkBase64)return t.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=e.get(n.BrowserService),o=await a.persistExtensionRecordingChunk(r.browserId,r.chunkBase64);return _e(`chunk received`,{browserId:r.browserId,chunkIndex:r.chunkIndex,mimeType:r.mimeType,persisted:!!o,chunkBase64Size:r.chunkBase64.length}),o?(i.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":r.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof r.chunkIndex==`number`?{"browse_tool.recording.chunk_index":r.chunkIndex}:{},...typeof r.mimeType==`string`?{"browse_tool.recording.mime_type":r.mimeType}:{}}}),a.recordBrowserActivity(r.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${r.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):(i.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)}}),t.post(`/handoff`,async t=>{try{let r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).requestHandoff(r);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 ${r.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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).acknowledgeHandoff(r.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 ${r.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 r=e.get(n.ExtensionSessionRegistry).listSessions();return t.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 t.json({error:e instanceof Error?e.message:String(e)},500)}}),t}function ye(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 be=[`pnpm-workspace.yaml`,`nx.json`,`.git`],xe=`browse-tool-chrome`,Se=`tool`,Ce=process.env.NODE_ENV||`development`,we=`127.0.0.1`;function Te(e=process.cwd()){let t=k.resolve(e);for(;;){for(let e of be)if(A(k.join(t,e)))return t;let e=k.dirname(t);if(e===t)return process.cwd();t=e}}function Ee(){return new E(process.env.PORT_REGISTRY_PATH)}async function De(e,t,n){let r=Ee(),i=await r.reservePort({repositoryPath:e,serviceName:xe,serviceType:Se,environment:Ce,preferredPort:t,portRange:n,pid:process.pid,host:we,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:xe,serviceType:Se,environment:Ce,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function Oe(){return new O(e=>{e.bind(n.ExtensionTaskQueue).to(u).inSingletonScope(),e.bind(n.ExtensionToolDelegator).to(p).inSingletonScope()})}function ke(e){let t=new ae({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(se,async()=>({tools:n})),t.setRequestHandler(oe,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const Ae=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,r;try{let i=ye(e.port),a=Te(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=te(o,`processes.json`)),t=await De(a,i??T.min,i?{min:i,max:i}:T);let s=t.port;r=await ee({repositoryPath:a,serviceName:xe,serviceType:Se,environment:Ce,pid:process.pid,port:s,host:we,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 D({defaultScope:`Singleton`});c.load(Oe());let l=c.get(n.ExtensionTaskQueue),u=c.get(n.ExtensionToolDelegator),d=new I;d.use(`*`,de({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=ve(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=ue({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=ke(u),h=new ce;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 r.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(r||t)try{await r?.release(),await t?.release()}catch{}let n=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${n}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),je=`BROWSE_TOOL_CONFIG`,Me=`.browse-tool`,Ne=`config.json`,Pe=[`json`,`text`,`quiet`],Fe={commands:{},tools:{}},Ie=N.record(N.string(),N.unknown()),Le=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(Pe).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),tools:N.object({format:N.enum(Pe).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()}),Re=N.object({commands:Le.default({}),tools:N.record(N.string(),Ie).default({})});let z={config:Fe};function ze(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 Be(e){let t=k.resolve(e);if(!A(t))throw Error(`Config path not found: ${t}`);return ne(t).isDirectory()?k.join(t,Ne):t}function Ve(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??ie(),a=ze(e);if(a)return Be(a);if(n[je])return Be(n[je]);let o=k.join(r,Me,Ne);if(A(o))return o;let s=k.join(i,Me,Ne);if(A(s))return s}function He(e){let t=j(e,`utf8`),n=JSON.parse(t);return Re.parse(n)}function Ue(e,t={}){let n=Ve(e,t);return n?{configPath:n,config:He(n)}:{config:Fe}}function We(e,t={}){return z=Ue(e,t),z}function B(e){return(z.config.commands??{})[e]??{}}function Ge(e){return z.config.tools?.[e]??{}}function V(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 Ke=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??s,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let e=await h().get(n.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 H(e){return new Ke(e)}const qe={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 U(e,t,n){return n?`${qe[t]}${e}${qe.reset}`:e}function Je(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Ye(e):Xe(e,r)}function Ye(e){return JSON.stringify(e,null,2)}function Xe(e,t){let n=[];e.isError&&n.push(U(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(Ze(r,t)):r.type===`image`?n.push(et(r,t)):n.push(U(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
3
3
|
`)}function Ze(e,t){let n=e.text;try{return Qe(JSON.parse(n),t)}catch{return n}}function Qe(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=U(r,`cyan`,t),a=$e(i,t);n.push(`${e}: ${a}`)}return n.join(`
|
|
4
4
|
`)}function $e(e,t){return e===null?U(`null`,`gray`,t):e===void 0?U(`undefined`,`gray`,t):typeof e==`boolean`?U(String(e),e?`green`:`red`,t):typeof e==`number`?U(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?U(e,`blue`,t):e:Array.isArray(e)?e.length===0?U(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function et(e,t){let{mimeType:n,data:r}=e;return U(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function W(e,t){return U(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function tt(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=U(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
|
|
5
5
|
`)}function nt(e,t){let n=B(`tools`),r=V(e,`format`,t.format,n.format),i=V(e,`color`,t.color,n.color);return{port:V(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const rt=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(s)).action(async function(e,t){let{port:n,formatterOptions:r}=nt(this,t);try{let t=await H({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?tt(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(W(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),it=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(s)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=nt(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(W(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await H({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=Je(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(W(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(rt).addCommand(it);const at=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`,x).option(`--image <image>`,`Docker image tag to produce`,t).option(`--platform <platform>`,`Docker target platform`,e).action(async e=>{try{let t=await C({version:e.cftVersion,image:e.image,platform:e.platform,stdio:`inherit`});console.log(`Built Docker image ${t.image}`),console.log(` Version: ${t.version}`),console.log(` Platform: ${t.platform}`),console.log(` Archive: ${a(t.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),ot=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(s)).action(async function(e,t,n){let r=B(`exec`),i=V(this,`format`,n.format,r.format),a=V(this,`color`,n.color,r.color),o=V(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(W(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await H({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=Je(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(W(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),G=`pageId`,K=`browserId`,st=N.record(N.string(),N.unknown()),ct=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(),lt=N.object({name:N.string().min(1),description:N.string().min(1),script:N.string().min(1),suggestionActions:N.string().min(1).optional(),capabilities:st,inputSchema:ct}),ut=N.object({tools:N.array(lt)}),dt=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function ft(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function pt(e,t){if(dt){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function q(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function mt(e){return q(e)&&Array.isArray(e.content)}function J(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 ht(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 gt(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 Y(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(`-`)?_t(e,t,n):vt(e,t,n)}function _t(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]=Y(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=yt(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]=Y(e,i+1,r.indent);s[t]=a,i=o}else s[t]=J(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]=Y(e,i,t.indent);if(!q(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=yt(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]=Y(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=J(a),i+=1}r.push(s);continue}r.push(J(a)),i+=1}return[r,i]}function vt(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]=yt(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]=Y(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=J(o),i+=1}return[r,i]}function yt(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 bt(e){let t=gt(e);if(t.length===0)return{};let[n]=Y(t,0,t[0].indent);return n}function xt(e){e.inputSchema.properties===void 0&&(e.inputSchema.properties={});let t=e.inputSchema.properties;if(!q(t))throw Error(`Custom tool "${e.name}" inputSchema.properties must be an object`);let n=t[G],r=t[K];if(n!==void 0&&!(q(n)&&n.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!(q(r)&&r.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`);n===void 0&&(t[G]={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[K]={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 St(e){let t=w(await M(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function Ct(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 wt(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(q(r)){let i=wt(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 Et(e,t,n){if(mt(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=q(t)&&n?wt(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var Dt=class{constructor(e,t,n=new l,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 b(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[G]==`string`&&t[G].length>0?t[G]:void 0,r=typeof t[K]==`string`&&t[K].length>0?t[K]: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`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let i=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:i}}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=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!r)throw Error(`Browser "${t}" has no pages`);let i=this.pageRegistry.get(r);if(!i)throw Error(`Page "${r}" not found`);return{...this.toPageSummary(n.currentPageId,i),page:this.resolveToolPage(e,r,i)}},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:ht({...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[G]==`string`?n[G]:void 0,i=typeof n[K]==`string`?n[K]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":k.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}),pt(`Executing custom tool`,{toolName:t,directory:k.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:ft(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 pt(`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})}pt(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:ft(u)});let d=Et(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=k.resolve(e),n=k.join(t,`tools.yaml`),r=bt(await M(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=ut.parse(r);return Promise.all(i.tools.map(async e=>{xt(e);let n=k.resolve(t,e.script);await re(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=Ct(n,await St(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function Ot(e){let t=new I;return t.get(`/browsers`,async t=>{try{let r=e.get(n.BrowserService),i=e.get(n.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 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(n.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 r=t.req.param(`id`),i=e.get(n.BrowserService);return i.getBrowser(r)?(await i.closeBrowser(r),t.json({success:!0,message:`Browser "${r}" closed`})):t.json({error:`Browser "${r}" 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 r=t.req.param(`id`),i=e.get(n.PageRegistry).get(r);return i?i.page?(await i.page.close(),t.json({success:!0,message:`Page "${r}" closed`})):t.json({error:`Page "${r}" is in extension mode and cannot be closed via API`},400):t.json({error:`Page "${r}" 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 kt(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 At(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 jt(e){return At(new Date().getTime()-e.getTime())}const Mt=`table-container`,Nt=`stat-card`,Pt=`browser-row`,Ft=`page-row`,X=`status-active`,It=`url-cell`,Z=`timestamp-cell`,Lt=`actions-cell`,Rt=`kill-btn`,zt=`empty-state`;function Bt({browsers:e}){return e.length===0?L(`div`,{class:Mt,children:R(`div`,{class:zt,children:[L(`h3`,{children:`No Active Browsers`}),L(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):L(`div`,{class:Mt,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(me,{children:[R(`tr`,{class:Pt,children:[L(`td`,{children:e.id}),L(`td`,{children:R(`span`,{class:X,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),L(`td`,{children:e.profileName||`Default Profile`}),L(`td`,{class:Z,children:kt(e.createdAt)}),L(`td`,{class:Z,children:jt(e.createdAt)}),L(`td`,{class:Lt,children:L(`button`,{type:`button`,class:Rt,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>R(`tr`,{class:Ft,children:[R(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),L(`td`,{children:L(`span`,{class:X,children:`Open`})}),L(`td`,{class:It,title:t.url,children:t.title||t.url||`about:blank`}),L(`td`,{class:Z,children:kt(t.createdAt)}),L(`td`,{class:Z,children:jt(t.createdAt)}),L(`td`,{class:Lt,children:L(`button`,{type:`button`,class:Rt,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]},t.id))]},e.id))})]})})}function Vt({stats:e}){return R(`div`,{class:`stats-container`,children:[R(`div`,{class:Nt,children:[L(`h3`,{children:`Active Browsers`}),L(`div`,{class:`value`,children:e.totalBrowsers})]}),R(`div`,{class:Nt,children:[L(`h3`,{children:`Active Pages`}),L(`div`,{class:`value`,children:e.totalPages})]})]})}function Ht({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(Vt,{stats:t}),L(Bt,{browsers:e}),L(`script`,{children:pe(`
|
|
@@ -429,6 +429,6 @@ window.addEventListener('beforeunload', () => {
|
|
|
429
429
|
color: #999;
|
|
430
430
|
}
|
|
431
431
|
`)})]}),L(`body`,{children:t})]})}function Wt(e){let t=new I;return t.get(`/`,async t=>{try{let r=e.get(n.BrowserService),i=e.get(n.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 t.html(L(Ut,{title:`Playwright MCP Dashboard`,children:L(Ht,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),t.html(L(Ut,{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 Gt(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 Kt(e){try{return e.get(n.TelemetryService)}catch{return new l}}function qt(e){let t=new I,r=e.get(n.PageRegistry),i=e.get(n.BrowserService),a=e.get(n.ExtensionTaskQueue),o=Kt(e),s=new Dt(r,a,o,i);t.use(`*`,de({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(n.Tool).find(e=>e.getDefinition().name===i.tool);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(!s)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 c=await s.execute(i.arguments||{}),l=i.arguments||{},u=typeof l.pageId==`string`?l.pageId:void 0;if(u){let t=e.get(n.BrowserService),i=r.get(u);i&&t.recordBrowserActivity(i.browserId,u)}let d=Gt(c);return d?(a?.setStatus({code:P.ERROR,message:d}),o.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":u}})):o.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":u}}),t.json({success:!c.isError,result:c,error:d})})}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 r=e.get(n.BrowserService).listBrowsers();return t.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 t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/tools`,t=>{try{let r=e.getAll(n.Tool);return t.json({tools:r.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 s.executeTool(i,a.tool,a.arguments||{});if(c){let t=e.get(n.BrowserService),i=r.get(c);i&&t.recordBrowserActivity(i.browserId,c)}let d=Gt(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=ve(e);t.route(`/extension`,c);let l=Ot(e);t.route(`/api`,l);let u=Wt(e);return t.route(`/`,u),t}function Jt(e,t,r){e.get(`/ws/extension/:browserId`,r(e=>{let r=e.req.param(`browserId`),i=null;return{onOpen(e,a){if(!r){a.close(1008,`Missing browserId`);return}try{i=t.get(n.WebSocketHub).addConnection(a,r)}catch{a.close(1011,`Internal server error`)}},onMessage(e){if(i)try{let r=t.get(n.WebSocketHub),a=typeof e.data==`string`?e.data:e.data.toString();r.handleMessage(i,a)}catch{}},onClose(){if(i)try{t.get(n.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{t.get(n.WebSocketHub).removeConnection(i)}catch{}}}})),e.get(`/ws/stats`,e=>{try{let r=t.get(n.WebSocketHub).getStats();return e.json({totalConnections:r.totalConnections,browserCount:r.browserCount,connections:r.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 Yt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function Xt(e=process.cwd()){let t=k.resolve(e);for(;;){for(let e of Yt)if(A(k.join(t,e)))return t;let e=k.dirname(t);if(e===t)return process.cwd();t=e}}const Zt=new F(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(s)).option(`--host <host>`,`Host to bind`,g()).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=B(`httpServe`),r={port:V(this,`port`,e.port,t.port===void 0?void 0:String(t.port),process.env.PLAYWRIGHT_PORT),headless:V(this,`headless`,e.headless,t.headless),idleTimeout:V(this,`idleTimeout`,e.idleTimeout,t.idleTimeout===void 0?void 0:String(t.idleTimeout),process.env[y]),host:V(this,`host`,e.host,t.host,process.env.PLAYWRIGHT_HOST),registryDir:V(this,`registryDir`,e.registryDir,t.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:V(this,`registryPath`,e.registryPath,t.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:V(this,`pidsDir`,e.pidsDir,t.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:V(this,`profilesDir`,e.profilesDir,t.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:V(this,`snippetsDir`,e.snippetsDir,t.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR),proxyConfigDir:V(this,`proxyConfigDir`,e.proxyConfigDir,t.proxyConfigDir,process.env[_])},a;try{let e=Number.parseInt(r.port,10),t=`browse-tool-http`,o=process.env.NODE_ENV||`development`,s=Xt(process.cwd()),l=r.registryPath||r.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;l&&(process.env.PLAYWRIGHT_REGISTRY_DIR=l,process.env.PORT_REGISTRY_PATH=l,process.env.PROCESS_REGISTRY_PATH=te(l,`processes.json`)),process.env.PLAYWRIGHT_HOST=r.host,process.env.PLAYWRIGHT_PORT=r.port,process.env[y]=r.idleTimeout,r.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=r.pidsDir),r.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=r.profilesDir),r.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=k.resolve(r.snippetsDir)),r.proxyConfigDir&&(process.env[_]=k.resolve(r.proxyConfigDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${e}`),console.log(` Host: ${r.host}`),console.log(` Headless: ${r.headless}`),console.log(` Idle Timeout: ${r.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[_]}`);let u=i(),d=u.get(n.ProcessRegistryService),f=qt(u),{injectWebSocket:p,upgradeWebSocket:m}=fe({app:f});Jt(f,u,m);let h=u.get(n.WebSocketHub),g=u.get(n.ExtensionTaskQueue),v=u.get(n.BrowserService),b=u.get(n.ExtensionSessionRegistry),x=u.get(n.PageRegistry),S=u.get(n.IdleCleanupService);S.start(),h.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=v.getBrowser(n),i,a;if(r&&(r.mode===`extension`||r.mode===`vm`)){i=r.id;let t=x.findByBrowser(i);a=r.currentPageId||t[0]?.id||``,h.reassignConnection(e.id,i),a||(a=x.registerExtensionPage(i),r.pageIds.add(a),r.currentPageId=a),console.log(`[WebSocket] Extension connected to existing browser: ${i}, pageId: ${a}`)}else{i=n,v.registerExtensionBrowserWithId(i),a=x.registerExtensionPage(i);let e=v.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=x.get(a);o&&(o.extensionTabId=t.payload.tabId,o.url=t.payload.url??o.url);let s=b.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,x.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&&v.recordBrowserActivity(e.browserId,e.pageId)}},onTabMapped:(e,t)=>{if(t.type===`tab:mapped`){let e=x.get(t.payload.pageId);if(!e)return;e.extensionTabId=t.payload.tabId,v.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&b.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let C=new E(process.env.PORT_REGISTRY_PATH);console.log(l?` Registry path: ${l}`:` Registry path: default (~/.port-registry/ports.json)`);let w=await C.reservePort({repositoryPath:s,serviceName:t,serviceType:`tool`,environment:o,preferredPort:e,pid:process.pid,host:r.host,force:!0,portRange:T,metadata:{headless:r.headless,idleTimeout:r.idleTimeout}});if(!w.success||w.port===void 0)throw Error(w.error||`Failed to reserve port in range ${T.min}-${T.max}`);let D=w.port;w.record.metadata={...w.record.metadata,healthCheckUrl:`${c(r.host,D)}/health`};let O=ue({fetch:f.fetch,port:D,hostname:r.host});p(O),a=await ee({repositoryPath:s,serviceName:t,serviceType:`service`,environment:o,pid:process.pid,port:D,host:r.host,metadata:{healthCheckUrl:`${c(r.host,D)}/health`,headless:r.headless,idleTimeout:r.idleTimeout,transport:`http`}},d);let A=c(r.host,D);console.log(`HTTP server listening on ${A}`),console.log(` Health check: ${A}/health`),console.log(` Execute tool: POST ${A}/execute`),console.log(`
|
|
432
|
-
Press Ctrl+C to stop the server`);let j=async e=>{console.log(`\n\n${e} received. Shutting down gracefully...`);try{S.stop(),O.close(),console.log(`Server closed`);try{await u.get(n.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{a&&await a.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`,()=>j(`SIGINT`)),process.on(`SIGTERM`,()=>j(`SIGTERM`)),await new Promise(()=>{})}catch(e){if(a)try{await a.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Qt={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 $t(e){let t=new Set;for(let[n,r]of Object.entries(Qt))r.some(t=>e.includes(t))&&t.add(n);return t}const en=3e3,tn=[`browser_launch`],nn=[`browser_new_page`];function rn(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}}catch{return null}}async function an(e){await new Promise(t=>setTimeout(t,e))}async function on(e){let t=new AbortController,n=setTimeout(()=>t.abort(),en);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 sn(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function cn(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),en);try{let r=await fetch(sn(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 ln(e){let t;for(let n=1;n<=4;n+=1)try{return await on(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await an(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function un(e,t){let n;for(let r=1;r<=4;r+=1)try{return await cn(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await an(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function dn(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?$t(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 fn(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:r,toolFilter:i,customToolsDir:a,enforcedProfileName:s,proxyConfigDir:c}=e,l=i?.tags?.length?dn(i):null,u=new Set(i?.exclude??[]),d=l!==null||u.size>0,f=new ae({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),p={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}},m=[],h=[],g=new Set;function _(e){return d?e.filter(e=>u.has(e.name)?!1:l===null?!0:l.has(e.name)):e}function v(e){return e.filter(e=>!u.has(e.name))}function y(e){return s?{...e,profileName:s}:e}function b(e){let t=o({proxy:e.proxy,profileName:typeof e.profileName==`string`?e.profileName:void 0,proxyConfigDir:c});if(!t){let t={...e};return delete t.proxy,t}return{...e,proxy:t}}function x(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 S(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function C(e){return u.has(e)?!1:l===null?!0:l.has(e)}return f.setRequestHandler(se,async()=>{try{let e=await ln(t);m=e,g=new Set(e.map(e=>e.name));let r=a?await un(t,a):[];h=r;let i=[..._(e),...v(r).map(e=>S(e))];return n&&i.push(p),{tools:i}}catch(e){if(m.length>0||h.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[..._(m),...v(h).map(e=>S(e))];return n&&t.push(p),{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})}}),f.setRequestHandler(oe,async e=>{let{name:i,arguments:o}=e.params,s=new Set(h.map(e=>e.name));if(i!==`browser_list_session`&&!C(i))return{content:[{type:`text`,text:`Tool "${i}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(i===`browser_list_session`&&n){let e=n.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let c=o||{};i===`browser_launch`&&r&&(c={...c,mode:r}),i===`browser_launch`&&(c=y(c),c=b(c));try{if(a&&!s.has(i)&&!g.has(i))try{h=await un(t,a),s=new Set(h.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=s.has(i),r=await fetch(e&&a?sn(t,a):`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:i,arguments:c})});if(!r.ok){let e=await r.text();return{content:[{type:`text`,text:`HTTP error ${r.status}: ${e}`}],isError:!0}}let o=await r.json();if(!o.success)return{content:[{type:`text`,text:o.error||o.result?.content?.[0]?.text||`Unknown error from HTTP server`}],isError:!0};if(n&&o.result){if(tn.includes(i)){let e=rn(o.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if(nn.includes(i)){let e=rn(o.result);e?.pageId&&e?.browserId&&n.trackPage(e.pageId,e.browserId,e.url)}}return i===`browser_list_profiles`&&o.result?x(o.result):o.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}}}),f}const Q=`stdio`,pn=`http`,$=`streamable-http`,mn=new Set([Q,pn,$]),hn=`chromium`,gn=new Set([hn,`firefox`,`webkit`]),_n=new Set([`playwright`,`extension`,`vm`,`stealth`]),vn=`SIGINT`,yn=`SIGTERM`,bn=`PLAYWRIGHT_PORT`,xn=[`pnpm-workspace.yaml`,`nx.json`,`.git`],Sn=`browse-tool-mcp-http`,Cn=`service`,wn=T.min;var Tn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...mn].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},En=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...gn].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},Dn=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[..._n].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(!gn.has(t))throw new En(e);return t}function kn(e){let t=e.toLowerCase();if(!_n.has(t))throw new Dn(e);return t}function An(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 jn(e){return e!=null&&String(e).trim()!==``}function Mn(e){let t=e.toLowerCase();return t===pn?$:t}function Nn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function Pn(e=process.cwd()){let t=k.resolve(e);for(;;){for(let e of xn)if(A(k.join(t,e)))return t;let e=k.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 In(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 Ln=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}},Rn=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`}},Bn=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(e,t){let n=e.getBrowserIds(),r=[],i;for(let e of n)try{let n=await fetch(`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:e}})});if(!n.ok){let e=await n.text().catch(()=>``);throw Error(`HTTP ${n.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),r.push(e),i??=n}if(r.length>0)throw new Ln(r,t,{cause:i});e.clear()}async function Hn(e,t,n,r){await e.start();let i=!1,a=async a=>{if(i)return;i=!0,console.error(`\nReceived ${a}, shutting down gracefully...`);let o;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await Vn(t,n))}catch(e){o=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,o)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),o??=t}try{await r?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),o??=t}if(o){let e=new Rn(a,{cause:o});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(vn,()=>a(vn)),process.once(yn,()=>a(yn))}const Un=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: ${[...gn].join(`, `)}`,hn).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`,g()).option(`--port <port>`,`Port for streamable HTTP transport`,String(wn)).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: ${[..._n].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=B(`mcpServe`),r={type:V(this,`type`,e.type,t.type),browser:V(this,`browser`,e.browser,t.browser),headless:V(this,`headless`,e.headless,t.headless),profile:V(this,`profile`,e.profile,t.profile),mode:V(this,`mode`,e.mode,t.mode),host:V(this,`host`,e.host,t.host,process.env.PLAYWRIGHT_HOST),port:V(this,`port`,e.port,t.port,process.env.PLAYWRIGHT_MCP_PORT),httpPort:V(this,`httpPort`,e.httpPort,t.httpPort,process.env[bn]),tags:V(this,`tags`,e.tags,t.tags),exclude:V(this,`exclude`,e.exclude,t.exclude),customTools:V(this,`customTools`,e.customTools,t.customTools),chromeForTestingPath:V(this,`chromeForTestingPath`,e.chromeForTestingPath,t.chromeForTestingPath,process.env[S]),snippetsDir:V(this,`snippetsDir`,e.snippetsDir,t.snippetsDir),idleTimeout:V(this,`idleTimeout`,e.idleTimeout,t.idleTimeout===void 0?void 0:String(t.idleTimeout),process.env[y]),registryPath:V(this,`registryPath`,e.registryPath,t.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:V(this,`registryDir`,e.registryDir,t.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:V(this,`pidsDir`,e.pidsDir,t.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:V(this,`profilesDir`,e.profilesDir,t.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),proxyConfigDir:V(this,`proxyConfigDir`,e.proxyConfigDir,t.proxyConfigDir,process.env[_])},i=Mn(r.type),a=Pn(process.cwd()),o=process.env.NODE_ENV||`development`,s,l;try{if(!mn.has(i))throw new Tn(i);let e=On(r.browser),t=r.mode?kn(r.mode):void 0,u=An(r.port??wn),d=jn(r.httpPort),p=d?An(r.httpPort):void 0,b=In(r),x=r.customTools?k.resolve(r.customTools):void 0,C=r.chromeForTestingPath?k.resolve(r.chromeForTestingPath):void 0,w=r.snippetsDir?k.resolve(r.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${i}`),i===$&&console.error(` MCP endpoint: http://${r.host??g()}:${u}/mcp`),console.error(` Default browser: ${e}`),console.error(` Headless: ${r.headless}`),t&&console.error(` Default mode: ${t}`),r.profile&&console.error(` Profile: ${r.profile}`),b?.tags?.length&&console.error(` Tags: ${b.tags.join(`, `)}`),b?.exclude?.length&&console.error(` Exclude: ${b.exclude.join(`, `)}`),x&&console.error(` Custom tools: ${x}`),C&&console.error(` Chrome for Testing path: ${C}`),w&&console.error(` Snippets dir: ${w}`),r.idleTimeout&&console.error(` Idle timeout: ${r.idleTimeout} minutes`);let T=r.registryPath||r.registryDir;T&&(process.env.PLAYWRIGHT_REGISTRY_DIR=T,process.env.PORT_REGISTRY_PATH=T,process.env.PROCESS_REGISTRY_PATH=te(T,`processes.json`),console.error(` Registry path: ${T}`)),r.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=r.pidsDir,console.error(` PIDs dir: ${r.pidsDir}`)),r.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=r.profilesDir,console.error(` Profiles dir: ${r.profilesDir}`)),r.proxyConfigDir&&(process.env[_]=k.resolve(r.proxyConfigDir),console.error(` Proxy config dir: ${process.env[_]}`)),w&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=w),C&&(process.env[S]=C),r.idleTimeout&&(process.env[y]=r.idleTimeout),r.host&&(process.env.PLAYWRIGHT_HOST=r.host);let E=h(),D=E.get(n.ProcessRegistryService);l=await E.get(n.McpPortAllocationService).acquirePorts({repositoryPath:a,environment:o,host:r.host??g(),pid:process.pid,preferredMcpPort:i===$?u:void 0,preferredHttpPort:p,exactHttpPort:d,reserveMcpPort:i===$,mcpServiceName:Sn,mcpServiceType:`tool`,mcpMetadata:i===$?{healthCheckUrl:`${c(r.host??g(),u)}/health`,transport:$}:void 0});let O=l.httpPort;process.env[bn]=O.toString();let A=await E.get(n.HttpServerManager).ensureRunning(O,{exactPort:!0});if(!A.running)throw new zn;let j=c(g(),A.port),ne=A.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${ne} on port ${A.port}`),i===Q){let n=new f,i=new m(fn({httpBaseUrl:j,sessionTracker:n,defaultMode:t,toolFilter:b,customToolsDir:x,enforcedProfileName:r.profile,proxyConfigDir:process.env[_]}));s=await ee({repositoryPath:a,serviceName:`browse-tool-mcp`,serviceType:Cn,environment:o,pid:process.pid,metadata:{transport:Q,browser:e,mode:t}},D),await Hn(i,n,j,async()=>{await s?.release({kill:!1}),await l?.release()});return}let M=l.mcpPort;if(!M)throw Error(`Failed to reserve ${$} port ${u}`);s=await ee({repositoryPath:a,serviceName:Sn,serviceType:Cn,environment:o,pid:process.pid,port:M,host:r.host??g(),metadata:{healthCheckUrl:`${c(r.host??g(),M)}/health`,transport:$}},D);let re=new v(({headers:e})=>{let n=Nn(e,`x-profile`)??r.profile,i=new f;return{server:fn({httpBaseUrl:j,sessionTracker:i,defaultMode:t,toolFilter:b,customToolsDir:x,enforcedProfileName:n,proxyConfigDir:process.env[_]}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await Vn(i,j)}}},{host:r.host??g(),port:M});try{await re.start()}catch(e){try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}throw await l.release(),l=void 0,e}let ie=!1,N=async e=>{if(!ie){ie=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await re.stop();try{await s?.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}await l?.release(),l=void 0,process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(vn,()=>N(vn)),process.once(yn,()=>N(yn))}catch(e){if(s)try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}if(l)try{await l.release()}catch(e){console.error(`Port allocation cleanup error:`,e)}(e instanceof Tn||e instanceof En||e instanceof Dn||e instanceof zn)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new Bn({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)}}),Wn=new F(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let e=B(`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 r=await d().get(n.HttpServerManager).getStatus();if(r.running){let t=process.env.PLAYWRIGHT_HOST??e.host??g();console.log(`HTTP Server: Running`),console.log(` PID: ${r.pid}`),console.log(` Port: ${r.port}`),console.log(` Health: ${c(t,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
|
|
432
|
+
Press Ctrl+C to stop the server`);let j=async e=>{console.log(`\n\n${e} received. Shutting down gracefully...`);try{S.stop(),O.close(),console.log(`Server closed`);try{await u.get(n.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{a&&await a.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`,()=>j(`SIGINT`)),process.on(`SIGTERM`,()=>j(`SIGTERM`)),await new Promise(()=>{})}catch(e){if(a)try{await a.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Qt={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 $t(e){let t=new Set;for(let[n,r]of Object.entries(Qt))r.some(t=>e.includes(t))&&t.add(n);return t}const en=3e3,tn=[`browser_launch`],nn=[`browser_new_page`],rn=[`browser_close`],an=[`browser_close_page`];function on(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 sn(e){await new Promise(t=>setTimeout(t,e))}async function cn(e){let t=new AbortController,n=setTimeout(()=>t.abort(),en);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 ln(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function un(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),en);try{let r=await fetch(ln(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 dn(e){let t;for(let n=1;n<=4;n+=1)try{return await cn(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await sn(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function fn(e,t){let n;for(let r=1;r<=4;r+=1)try{return await un(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await sn(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function pn(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?$t(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 mn(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:r,toolFilter:i,customToolsDir:a,enforcedProfileName:s,proxyConfigDir:c}=e,l=i?.tags?.length?pn(i):null,u=new Set(i?.exclude??[]),d=l!==null||u.size>0,f=new ae({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),p={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}},m=[],h=[],g=new Set;function _(e){return d?e.filter(e=>u.has(e.name)?!1:l===null?!0:l.has(e.name)):e}function v(e){return e.filter(e=>!u.has(e.name))}function y(e){return s?{...e,profileName:s}:e}function b(e){let t=o({proxy:e.proxy,profileName:typeof e.profileName==`string`?e.profileName:void 0,proxyConfigDir:c});if(!t){let t={...e};return delete t.proxy,t}return{...e,proxy:t}}function x(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 S(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function C(e){return u.has(e)?!1:l===null?!0:l.has(e)}return f.setRequestHandler(se,async()=>{try{let e=await dn(t);m=e,g=new Set(e.map(e=>e.name));let r=a?await fn(t,a):[];h=r;let i=[..._(e),...v(r).map(e=>S(e))];return n&&i.push(p),{tools:i}}catch(e){if(m.length>0||h.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[..._(m),...v(h).map(e=>S(e))];return n&&t.push(p),{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})}}),f.setRequestHandler(oe,async e=>{let{name:i,arguments:o}=e.params,s=new Set(h.map(e=>e.name));if(i!==`browser_list_session`&&!C(i))return{content:[{type:`text`,text:`Tool "${i}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(i===`browser_list_session`&&n){let e=n.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let c=o||{};i===`browser_launch`&&r&&c.mode===void 0&&(c={...c,mode:r}),i===`browser_launch`&&(c=y(c),c=b(c));try{if(a&&!s.has(i)&&!g.has(i))try{h=await fn(t,a),s=new Set(h.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=s.has(i),r=await fetch(e&&a?ln(t,a):`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:i,arguments:c})});if(!r.ok){let e=await r.text();return{content:[{type:`text`,text:`HTTP error ${r.status}: ${e}`}],isError:!0}}let o=await r.json();if(!o.success)return{content:[{type:`text`,text:o.error||o.result?.content?.[0]?.text||`Unknown error from HTTP server`}],isError:!0};if(n&&o.result){if(tn.includes(i)){let e=on(o.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if(nn.includes(i)){let e=on(o.result);e?.pageId&&e?.browserId&&n.trackPage(e.pageId,e.browserId,e.url)}else if(rn.includes(i)){let e=on(o.result);e?.closedBrowserId&&n.untrackBrowser(e.closedBrowserId)}else if(an.includes(i)){let e=on(o.result);e?.closedPageId&&n.untrackPage(e.closedPageId),e?.browserId&&e.browserKeptOpen===!1&&n.untrackBrowser(e.browserId)}}return i===`browser_list_profiles`&&o.result?x(o.result):o.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}}}),f}const Q=`stdio`,hn=`http`,$=`streamable-http`,gn=new Set([Q,hn,$]),_n=`chromium`,vn=new Set([_n,`firefox`,`webkit`]),yn=new Set([`playwright`,`extension`,`vm`,`stealth`]),bn=`SIGINT`,xn=`SIGTERM`,Sn=`PLAYWRIGHT_PORT`,Cn=[`pnpm-workspace.yaml`,`nx.json`,`.git`],wn=`browse-tool-mcp-http`,Tn=`service`,En=T.min;var Dn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...gn].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},On=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...vn].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},kn=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...yn].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(!vn.has(t))throw new On(e);return t}function jn(e){let t=e.toLowerCase();if(!yn.has(t))throw new kn(e);return t}function Mn(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 Nn(e){return e!=null&&String(e).trim()!==``}function Pn(e){let t=e.toLowerCase();return t===hn?$:t}function Fn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function In(e=process.cwd()){let t=k.resolve(e);for(;;){for(let e of Cn)if(A(k.join(t,e)))return t;let e=k.dirname(t);if(e===t)return process.cwd();t=e}}function Ln(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function Rn(e){let t=e.tags?Ln(e.tags):void 0,n=e.exclude?Ln(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var zn=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}},Bn=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}},Vn=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`}},Hn=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 Un(e,t){let n=e.getBrowserIds(),r=[],i;for(let e of n)try{let n=await fetch(`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:e}})});if(!n.ok){let e=await n.text().catch(()=>``);throw Error(`HTTP ${n.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),r.push(e),i??=n}if(r.length>0)throw new zn(r,t,{cause:i});e.clear()}async function Wn(e,t,n,r){await e.start();let i=!1,a=async a=>{if(i)return;i=!0,console.error(`\nReceived ${a}, shutting down gracefully...`);let o;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await Un(t,n))}catch(e){o=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,o)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),o??=t}try{await r?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),o??=t}if(o){let e=new Bn(a,{cause:o});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(bn,()=>a(bn)),process.once(xn,()=>a(xn))}const Gn=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: ${[...vn].join(`, `)}`,_n).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`,g()).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: ${[...yn].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=B(`mcpServe`),r={type:V(this,`type`,e.type,t.type),browser:V(this,`browser`,e.browser,t.browser),headless:V(this,`headless`,e.headless,t.headless),profile:V(this,`profile`,e.profile,t.profile),mode:V(this,`mode`,e.mode,t.mode),host:V(this,`host`,e.host,t.host,process.env.PLAYWRIGHT_HOST),port:V(this,`port`,e.port,t.port,process.env.PLAYWRIGHT_MCP_PORT),httpPort:V(this,`httpPort`,e.httpPort,t.httpPort,process.env[Sn]),tags:V(this,`tags`,e.tags,t.tags),exclude:V(this,`exclude`,e.exclude,t.exclude),customTools:V(this,`customTools`,e.customTools,t.customTools),chromeForTestingPath:V(this,`chromeForTestingPath`,e.chromeForTestingPath,t.chromeForTestingPath,process.env[S]),snippetsDir:V(this,`snippetsDir`,e.snippetsDir,t.snippetsDir),idleTimeout:V(this,`idleTimeout`,e.idleTimeout,t.idleTimeout===void 0?void 0:String(t.idleTimeout),process.env[y]),registryPath:V(this,`registryPath`,e.registryPath,t.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:V(this,`registryDir`,e.registryDir,t.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:V(this,`pidsDir`,e.pidsDir,t.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:V(this,`profilesDir`,e.profilesDir,t.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),proxyConfigDir:V(this,`proxyConfigDir`,e.proxyConfigDir,t.proxyConfigDir,process.env[_])},i=Pn(r.type),a=In(process.cwd()),o=process.env.NODE_ENV||`development`,s,l;try{if(!gn.has(i))throw new Dn(i);let e=An(r.browser),t=r.mode?jn(r.mode):void 0,u=Mn(r.port??En),d=Nn(r.httpPort),p=d?Mn(r.httpPort):void 0,b=Rn(r),x=r.customTools?k.resolve(r.customTools):void 0,C=r.chromeForTestingPath?k.resolve(r.chromeForTestingPath):void 0,w=r.snippetsDir?k.resolve(r.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${i}`),i===$&&console.error(` MCP endpoint: http://${r.host??g()}:${u}/mcp`),console.error(` Default browser: ${e}`),console.error(` Headless: ${r.headless}`),t&&console.error(` Default mode: ${t}`),r.profile&&console.error(` Profile: ${r.profile}`),b?.tags?.length&&console.error(` Tags: ${b.tags.join(`, `)}`),b?.exclude?.length&&console.error(` Exclude: ${b.exclude.join(`, `)}`),x&&console.error(` Custom tools: ${x}`),C&&console.error(` Chrome for Testing path: ${C}`),w&&console.error(` Snippets dir: ${w}`),r.idleTimeout&&console.error(` Idle timeout: ${r.idleTimeout} minutes`);let T=r.registryPath||r.registryDir;T&&(process.env.PLAYWRIGHT_REGISTRY_DIR=T,process.env.PORT_REGISTRY_PATH=T,process.env.PROCESS_REGISTRY_PATH=te(T,`processes.json`),console.error(` Registry path: ${T}`)),r.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=r.pidsDir,console.error(` PIDs dir: ${r.pidsDir}`)),r.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=r.profilesDir,console.error(` Profiles dir: ${r.profilesDir}`)),r.proxyConfigDir&&(process.env[_]=k.resolve(r.proxyConfigDir),console.error(` Proxy config dir: ${process.env[_]}`)),w&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=w),C&&(process.env[S]=C),r.idleTimeout&&(process.env[y]=r.idleTimeout),r.host&&(process.env.PLAYWRIGHT_HOST=r.host);let E=h(),D=E.get(n.ProcessRegistryService);l=await E.get(n.McpPortAllocationService).acquirePorts({repositoryPath:a,environment:o,host:r.host??g(),pid:process.pid,preferredMcpPort:i===$?u:void 0,preferredHttpPort:p,exactHttpPort:d,reserveMcpPort:i===$,mcpServiceName:wn,mcpServiceType:`tool`,mcpMetadata:i===$?{healthCheckUrl:`${c(r.host??g(),u)}/health`,transport:$}:void 0});let O=l.httpPort;process.env[Sn]=O.toString();let A=await E.get(n.HttpServerManager).ensureRunning(O,{exactPort:!0});if(!A.running)throw new Vn;let j=c(g(),A.port),ne=A.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${ne} on port ${A.port}`),i===Q){let n=new f,i=new m(mn({httpBaseUrl:j,sessionTracker:n,defaultMode:t,toolFilter:b,customToolsDir:x,enforcedProfileName:r.profile,proxyConfigDir:process.env[_]}));s=await ee({repositoryPath:a,serviceName:`browse-tool-mcp`,serviceType:Tn,environment:o,pid:process.pid,metadata:{transport:Q,browser:e,mode:t}},D),await Wn(i,n,j,async()=>{await s?.release({kill:!1}),await l?.release()});return}let M=l.mcpPort;if(!M)throw Error(`Failed to reserve ${$} port ${u}`);s=await ee({repositoryPath:a,serviceName:wn,serviceType:Tn,environment:o,pid:process.pid,port:M,host:r.host??g(),metadata:{healthCheckUrl:`${c(r.host??g(),M)}/health`,transport:$}},D);let re=new v(({headers:e})=>{let n=Fn(e,`x-profile`)??r.profile,i=new f;return{server:mn({httpBaseUrl:j,sessionTracker:i,defaultMode:t,toolFilter:b,customToolsDir:x,enforcedProfileName:n,proxyConfigDir:process.env[_]}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await Un(i,j)}}},{host:r.host??g(),port:M});try{await re.start()}catch(e){try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}throw await l.release(),l=void 0,e}let ie=!1,N=async e=>{if(!ie){ie=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await re.stop();try{await s?.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}await l?.release(),l=void 0,process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(bn,()=>N(bn)),process.once(xn,()=>N(xn))}catch(e){if(s)try{await s.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}if(l)try{await l.release()}catch(e){console.error(`Port allocation cleanup error:`,e)}(e instanceof Dn||e instanceof On||e instanceof kn||e instanceof Vn)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new Hn({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)}}),Kn=new F(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let e=B(`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 r=await d().get(n.HttpServerManager).getStatus();if(r.running){let t=process.env.PLAYWRIGHT_HOST??e.host??g();console.log(`HTTP Server: Running`),console.log(` PID: ${r.pid}`),console.log(` Port: ${r.port}`),console.log(` Health: ${c(t,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)}}),qn=new F(`stop`).description(`Stop HTTP server and clean up registry/PID files`).action(async()=>{try{let e=B(`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 d().get(n.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)}}),Jn=new Set([`--config`]);function Yn(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(Jn.has(n)){t+=1;continue}if(![...Jn].some(e=>n.startsWith(`${e}=`))&&!n.startsWith(`-`))return n}}function Xn(e,t=!1){if(t)return!1;let n=Yn(e);return n?n===`tools`?!0:n===`help`&&e.includes(`tools`):!1}function Zn(e){return e.replace(/_/g,`-`)}function Qn(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function $n(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 er(e,t){let n=$n(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 tr(e,t){let n=Qn(e),r=$n(t);return t.type===`boolean`?`--${n}`:r.has(`boolean`)&&r.has(`object`)?`--${n} [value]`:`--${n} <value>`}function nr(e){let{definition:t,execute:n}=e,r=new F(Zn(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=tr(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-${Qn(e)}`,`Disable ${a}`):r.option(i,a,n.default):n.enum&&n.enum.length>0?r.addOption(new le(i,a).choices(n.enum)):r.option(i,a)}return r.action(async function(){let e=this.optsWithGlobals(),r=B(`tools`),i=V(this,`format`,e.format??`json`,r.format),c=V(this,`color`,e.color??!0,r.color),l=V(this,`port`,String(e.port??s),r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),u={format:i,color:c},d=Ge(t.name);try{let r={};for(let[t,n]of Object.entries(a)){let i=n,a=Qn(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]=er(s,i):r[t]=s)}for(let e of o)if(r[e]===void 0){let t=Qn(e);console.error(W(`Missing required option: --${t}`,u.color)),process.exit(1)}let i=Number.parseInt(l,10),s=H({port:i}),c=n?await n(r,{command:this,formatterOptions:u,port:i}):await s.execute(t.name,r),f=Je(c,u);f&&console.log(f),c.isError&&process.exit(1)}catch(e){console.error(W(e instanceof Error?e:String(e),u.color)),process.exit(1)}}),r}function rr(e){return e.map(nr)}const ir={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 ar(){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(s))}function or(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 sr(){return{definition:ir,execute:async(e,t)=>{let n=await H({port:t.port}).listBrowsers();return{content:[{type:`text`,text:JSON.stringify(or(n),null,2)}]}}}}function cr(e){return[...e.map(e=>({definition:e})),sr()]}async function lr(e,t){let n=B(`tools`),r=Number(t?.port??process.env.PLAYWRIGHT_PORT??n.port??s);try{let t=rr(cr(await H({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(`${W(`Failed to load tool commands: ${t}`,!0)}\n`),process.stderr.write(`Make sure the HTTP server is running: browse-tool http-serve
|
|
434
|
+
`)}}function ur(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 dr(){let e=We(process.argv.slice(2)),t=process.env.PLAYWRIGHT_PORT??e.config.commands.tools?.port??e.config.commands.exec?.port??s,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(he),n.addCommand(Gn),n.addCommand(Zt),n.addCommand(Ae),n.addCommand(at),n.addCommand(qn),n.addCommand(Kn),n.addCommand(ot),n.addCommand(rt),n.addCommand(it);let r=ar();Xn(process.argv.slice(2),process.env.PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS===`1`)&&await lr(r,{port:ur(process.argv.slice(2),t)}),n.addCommand(r),await n.parseAsync(process.argv)}dr().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.8.
|
|
4
|
+
"version": "0.8.7",
|
|
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/
|
|
48
|
-
"@agimon-ai/foundation-
|
|
49
|
-
"@agimon-ai/
|
|
50
|
-
"@agimon-ai/foundation-validator": "0.5.
|
|
47
|
+
"@agimon-ai/foundation-port-registry": "0.8.5",
|
|
48
|
+
"@agimon-ai/foundation-process-registry": "0.8.5",
|
|
49
|
+
"@agimon-ai/log-sink-mcp": "0.8.5",
|
|
50
|
+
"@agimon-ai/foundation-validator": "0.5.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/chrome": "0.1.40",
|