@agimon-ai/browse-tool 0.2.12 → 0.2.13

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.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import{C as e,S as t,_ as n,a as r,b as i,c as a,d as o,f as s,g as c,h as l,i as u,l as d,m as f,n as p,o as m,p as h,s as g,t as _,u as v,v as y,x as b,y as x}from"./streamable-http-CpuMUSOu.mjs";import"./playwright-test-yNsP599T.mjs";import{stripTypeScriptTypes as S}from"node:module";import"reflect-metadata/lite";import{Command as C,Option as w}from"commander";import{serve as ee}from"@hono/node-server";import{Server as T}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as te}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as ne,ListToolsRequestSchema as re}from"@modelcontextprotocol/sdk/types.js";import{Hono as E}from"hono";import{cors as ie}from"hono/cors";import{Container as ae,ContainerModule as oe}from"inversify";import{existsSync as D,readFileSync as se,statSync as ce}from"node:fs";import O from"node:path";import{SpanStatusCode as k}from"@opentelemetry/api";import{PortRegistryService as le}from"@agimon-ai/foundation-port-registry";import{homedir as ue}from"node:os";import{z as A}from"zod";import{readFile as de,stat as fe}from"node:fs/promises";import{createNodeWebSocket as pe}from"@hono/node-ws";import{raw as me}from"hono/html";import{Fragment as he,jsx as j,jsxs as M}from"hono/jsx/jsx-runtime";var ge=`0.2.11`;const _e=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function ve(e,t){if(!_e)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function ye(n){let r=new E,i;try{i=n.get(e.TelemetryService)}catch{i=new b}return r.get(`/telemetry-config`,e=>{try{let n=new URL(e.req.url).origin;return e.json(t(process.env,n))}catch(t){return e.json({enabled:!1,error:t instanceof Error?t.message:String(t)},500)}}),r.get(`/tasks`,t=>{try{let r=n.get(e.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)}}),r.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=n.get(e.ExtensionTaskQueue),a=n.get(e.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)}}),r.get(`/status`,t=>{try{let r=n.get(e.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)}}),r.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=n.get(e.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)}}),r.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=n.get(e.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)}}),r.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=n.get(e.PageRegistry),a=n.get(e.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)}}),r.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=n.get(e.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return ve(`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)}}),r.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=n.get(e.BrowserService),o=await a.persistExtensionRecordingChunk(r.browserId,r.chunkBase64);return ve(`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)}}),r.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:{}}}),ve(`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)}}),r.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=n.get(e.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)}}),r.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=n.get(e.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)}}),r.get(`/sessions`,t=>{try{let r=n.get(e.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)}}),r}function be(){return new oe(t=>{t.bind(e.ExtensionTaskQueue).to(i).inSingletonScope(),t.bind(e.ExtensionToolDelegator).to(x).inSingletonScope()})}function xe(e){let t=new T({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(re,async()=>({tools:n})),t.setRequestHandler(ne,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const Se=new C(`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`,`3200`).option(`-v, --verbose`,`Enable verbose output`,!1).option(`--wait-for-extension`,`Wait for extension to connect before accepting MCP requests`,!1).action(async t=>{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(``);try{let n=typeof t.port==`string`?Number.parseInt(t.port,10):t.port;t.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${n}`),console.error(` Wait for extension: ${t.waitForExtension}`));let r=new ae({defaultScope:`Singleton`});r.load(be());let i=r.get(e.ExtensionTaskQueue),a=r.get(e.ExtensionToolDelegator),o=new E;o.use(`*`,ie({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let s=ye(r);o.route(`/extension`,s),o.get(`/health`,e=>{let t=i.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let c=ee({fetch:o.fetch,port:n});c.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${n} 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 ${n}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${n}/extension/tasks`),t.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{i.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let l=xe(a),u=new te;await l.connect(u),console.error(`Chrome extension MCP server started on stdio`);let d=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{i.clearAllTasks(`Server shutting down`),await u.close(),c.close(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>d(`SIGINT`)),process.on(`SIGTERM`,()=>d(`SIGTERM`))}catch(e){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`,we=`.browse-tool`,Te=`config.json`,Ee=[`json`,`text`,`quiet`],De={commands:{},tools:{}},Oe=A.record(A.string(),A.unknown()),ke=A.object({mcpServe:A.object({type:A.string().optional(),browser:A.string().optional(),headless:A.boolean().optional(),profile:A.string().optional(),mode:A.string().optional(),host:A.string().optional(),port:A.coerce.number().int().positive().optional(),tags:A.string().optional(),exclude:A.string().optional(),customTools:A.string().optional(),snippetsDir:A.string().optional(),registryPath:A.string().optional(),registryDir:A.string().optional(),pidsDir:A.string().optional(),profilesDir:A.string().optional()}).partial().optional(),httpServe:A.object({port:A.coerce.number().int().positive().optional(),headless:A.boolean().optional(),idleTimeout:A.coerce.number().positive().optional(),host:A.string().optional(),registryDir:A.string().optional(),registryPath:A.string().optional(),pidsDir:A.string().optional(),profilesDir:A.string().optional(),snippetsDir:A.string().optional()}).partial().optional(),exec:A.object({format:A.enum(Ee).optional(),color:A.boolean().optional(),port:A.coerce.number().int().positive().optional()}).partial().optional(),tools:A.object({format:A.enum(Ee).optional(),color:A.boolean().optional(),port:A.coerce.number().int().positive().optional()}).partial().optional(),status:A.record(A.string(),A.unknown()).optional(),stop:A.record(A.string(),A.unknown()).optional()}),Ae=A.object({commands:ke.default({}),tools:A.record(A.string(),Oe).default({})});let N={config:De};function je(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 Me(e){let t=O.resolve(e);if(!D(t))throw Error(`Config path not found: ${t}`);return ce(t).isDirectory()?O.join(t,Te):t}function Ne(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??ue(),a=je(e);if(a)return Me(a);if(n[Ce])return Me(n[Ce]);let o=O.join(r,we,Te);if(D(o))return o;let s=O.join(i,we,Te);if(D(s))return s}function Pe(e){let t=se(e,`utf8`),n=JSON.parse(t);return Ae.parse(n)}function Fe(e,t={}){let n=Ne(e,t);return n?{configPath:n,config:Pe(n)}:{config:De}}function Ie(e,t={}){return N=Fe(e,t),N}function P(e){return(N.config.commands??{})[e]??{}}function Le(e){return N.config.tools?.[e]??{}}function F(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 Re=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??3200,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let t=await m().get(e.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 I(e){return new Re(e)}const ze={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 L(e,t,n){return n?`${ze[t]}${e}${ze.reset}`:e}function R(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Be(e):Ve(e,r)}function Be(e){return JSON.stringify(e,null,2)}function Ve(e,t){let n=[];e.isError&&n.push(L(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(He(r,t)):r.type===`image`?n.push(Ge(r,t)):n.push(L(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
3
- `)}function He(e,t){let n=e.text;try{return Ue(JSON.parse(n),t)}catch{return n}}function Ue(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=L(r,`cyan`,t),a=We(i,t);n.push(`${e}: ${a}`)}return n.join(`
4
- `)}function We(e,t){return e===null?L(`null`,`gray`,t):e===void 0?L(`undefined`,`gray`,t):typeof e==`boolean`?L(String(e),e?`green`:`red`,t):typeof e==`number`?L(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?L(e,`blue`,t):e:Array.isArray(e)?e.length===0?L(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function Ge(e,t){let{mimeType:n,data:r}=e;return L(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function z(e,t){return L(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function Ke(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=L(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
5
- `)}function qe(e,t){let n=P(`tools`),r=F(e,`format`,t.format,n.format),i=F(e,`color`,t.color,n.color);return{port:F(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const Je=new C(`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(d)).action(async function(e,t){let{port:n,formatterOptions:r}=qe(this,t);try{let t=await I({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?Ke(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(z(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),Ye=new C(`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(d)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=qe(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(z(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await I({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=R(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(z(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new C(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(Je).addCommand(Ye);const Xe=new C(`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`,y).option(`--image <image>`,`Docker image tag to produce`,c).option(`--platform <platform>`,`Docker target platform`,n).action(async e=>{try{let t=await f({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: ${l(t.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),Ze=new C(`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(d)).action(async function(e,t,n){let r=P(`exec`),i=F(this,`format`,n.format,r.format),a=F(this,`color`,n.color,r.color),o=F(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(z(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await I({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=R(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(z(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),B=`pageId`,V=`browserId`,Qe=A.record(A.string(),A.unknown()),$e=A.object({type:A.literal(`object`),properties:A.record(A.string(),A.unknown()).optional(),required:A.array(A.string()).optional(),additionalProperties:A.boolean().optional()}).passthrough(),et=A.object({name:A.string().min(1),description:A.string().min(1),script:A.string().min(1),suggestionActions:A.string().min(1).optional(),capabilities:Qe,inputSchema:$e}),tt=A.object({tools:A.array(et)}),nt=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function rt(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function it(e,t){if(nt){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function H(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function at(e){return H(e)&&Array.isArray(e.content)}function U(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 ot(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 st(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 W(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(`-`)?ct(e,t,n):lt(e,t,n)}function ct(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]=W(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=ut(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]=W(e,i+1,r.indent);s[t]=a,i=o}else s[t]=U(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]=W(e,i,t.indent);if(!H(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=ut(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]=W(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=U(a),i+=1}r.push(s);continue}r.push(U(a)),i+=1}return[r,i]}function lt(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]=ut(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]=W(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=U(o),i+=1}return[r,i]}function ut(e){let t=e.indexOf(`:`);if(t===-1)throw Error(`Invalid tools.yaml entry: "${e}"`);let n=e.slice(0,t).trim(),r=e.slice(t+1).trim();return[n,r===``?void 0:r]}function dt(e){let t=st(e);if(t.length===0)return{};let[n]=W(t,0,t[0].indent);return n}function ft(e){let t=e.inputSchema.properties;if(!t||!H(t))throw Error(`Custom tool "${e.name}" must define inputSchema.properties`);let n=t[B],r=t[V],i=n!==void 0&&H(n)&&n.type===`string`,a=r!==void 0&&H(r)&&r.type===`string`;if(!i&&!a)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId and/or inputSchema.properties.browserId as type "string"`);if(n!==void 0&&!i)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!a)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`)}async function pt(e){let t=S(await de(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function mt(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 ht(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function gt(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(H(r)){let i=ht(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 _t(e,t,n){if(at(t))return n?gt(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=H(t)&&n?ht(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var vt=class{constructor(e,t,n=new b,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 h(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[B]==`string`&&t[B].length>0?t[B]:void 0,r=typeof t[V]==`string`&&t[V].length>0?t[V]: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:ot({...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[B]==`string`?n[B]:void 0,i=typeof n[V]==`string`?n[V]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":O.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:k.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}),it(`Executing custom tool`,{toolName:t,directory:O.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:rt(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 it(`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})}it(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:rt(u)});let d=_t(t,u,i.suggestionActions),f=d.isError?d.content[0]?.text:void 0;return f&&r?.setStatus({code:k.ERROR,message:f}),d})}async loadTools(e){let t=O.resolve(e),n=O.join(t,`tools.yaml`),r=dt(await de(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=tt.parse(r);return Promise.all(i.tools.map(async e=>{ft(e);let n=O.resolve(t,e.script);await fe(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=mt(n,await pt(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function yt(t){let n=new E;return n.get(`/browsers`,async n=>{try{let r=t.get(e.BrowserService),i=t.get(e.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.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.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.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 bt(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 xt(e){let t=Math.floor(e/1e3),n=Math.floor(t/60),r=Math.floor(n/60);return r>0?`${r}h ${n%60}m`:n>0?`${n}m ${t%60}s`:`${t}s`}function St(e){return xt(new Date().getTime()-e.getTime())}const Ct=`table-container`,wt=`stat-card`,Tt=`browser-row`,Et=`page-row`,G=`status-active`,Dt=`url-cell`,K=`timestamp-cell`,q=`actions-cell`,J=`kill-btn`,Ot=`empty-state`;function kt({browsers:e}){return e.length===0?j(`div`,{class:Ct,children:M(`div`,{class:Ot,children:[j(`h3`,{children:`No Active Browsers`}),j(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):j(`div`,{class:Ct,children:M(`table`,{class:`browser-table`,children:[j(`thead`,{children:M(`tr`,{children:[j(`th`,{children:`ID`}),j(`th`,{children:`Status`}),j(`th`,{children:`URL / Title`}),j(`th`,{children:`Created`}),j(`th`,{children:`Age`}),j(`th`,{children:`Actions`})]})}),j(`tbody`,{id:`browser-table-body`,children:e.map(e=>M(he,{children:[M(`tr`,{class:Tt,children:[j(`td`,{children:e.id}),j(`td`,{children:M(`span`,{class:G,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),j(`td`,{children:e.profileName||`Default Profile`}),j(`td`,{class:K,children:bt(e.createdAt)}),j(`td`,{class:K,children:St(e.createdAt)}),j(`td`,{class:q,children:j(`button`,{class:J,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>M(`tr`,{class:Et,children:[M(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),j(`td`,{children:j(`span`,{class:G,children:`Open`})}),j(`td`,{class:Dt,title:t.url,children:t.title||t.url||`about:blank`}),j(`td`,{class:K,children:bt(t.createdAt)}),j(`td`,{class:K,children:St(t.createdAt)}),j(`td`,{class:q,children:j(`button`,{class:J,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]}))]}))})]})})}function At({stats:e}){return M(`div`,{class:`stats-container`,children:[M(`div`,{class:wt,children:[j(`h3`,{children:`Active Browsers`}),j(`div`,{class:`value`,children:e.totalBrowsers})]}),M(`div`,{class:wt,children:[j(`h3`,{children:`Active Pages`}),j(`div`,{class:`value`,children:e.totalPages})]})]})}function jt({browsers:e,stats:t}){return M(`div`,{class:`dashboard-container`,children:[M(`div`,{class:`dashboard-header`,children:[M(`div`,{children:[j(`h1`,{children:`Playwright MCP Dashboard`}),j(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),M(`div`,{children:[j(`button`,{id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),j(`button`,{id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),j(At,{stats:t}),j(kt,{browsers:e}),j(`script`,{children:me(`
2
+ import{C as e,S as t,_ as n,a as r,b as i,c as a,d as o,f as s,g as c,h as l,i as u,l as d,m as f,n as p,o as m,p as h,s as g,t as _,u as v,v as y,x as b,y as x}from"./streamable-http-_UIJOzAB.mjs";import"./playwright-test-yNsP599T.mjs";import{stripTypeScriptTypes as S}from"node:module";import"reflect-metadata/lite";import{Command as C,Option as w}from"commander";import{serve as ee}from"@hono/node-server";import{Server as T}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as E}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as D,ListToolsRequestSchema as te}from"@modelcontextprotocol/sdk/types.js";import{Hono as O}from"hono";import{cors as k}from"hono/cors";import{Container as ne,ContainerModule as re}from"inversify";import{existsSync as A,readFileSync as ie,statSync as ae}from"node:fs";import j from"node:path";import{SpanStatusCode as M}from"@opentelemetry/api";import{PortRegistryService as oe}from"@agimon-ai/foundation-port-registry";import{homedir as se}from"node:os";import{z as N}from"zod";import{readFile as ce,stat as le}from"node:fs/promises";import{createNodeWebSocket as ue}from"@hono/node-ws";import{raw as de}from"hono/html";import{Fragment as fe,jsx as P,jsxs as F}from"hono/jsx/jsx-runtime";var pe=`0.2.12`;const me=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function he(e,t){if(!me)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function ge(n){let r=new O,i;try{i=n.get(e.TelemetryService)}catch{i=new b}return r.get(`/telemetry-config`,e=>{try{let n=new URL(e.req.url).origin;return e.json(t(process.env,n))}catch(t){return e.json({enabled:!1,error:t instanceof Error?t.message:String(t)},500)}}),r.get(`/tasks`,t=>{try{let r=n.get(e.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)}}),r.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=n.get(e.ExtensionTaskQueue),a=n.get(e.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)}}),r.get(`/status`,t=>{try{let r=n.get(e.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)}}),r.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=n.get(e.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)}}),r.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=n.get(e.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)}}),r.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=n.get(e.PageRegistry),a=n.get(e.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)}}),r.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=n.get(e.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return he(`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)}}),r.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=n.get(e.BrowserService),o=await a.persistExtensionRecordingChunk(r.browserId,r.chunkBase64);return he(`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)}}),r.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:{}}}),he(`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)}}),r.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=n.get(e.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)}}),r.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=n.get(e.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)}}),r.get(`/sessions`,t=>{try{let r=n.get(e.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)}}),r}function _e(){return new re(t=>{t.bind(e.ExtensionTaskQueue).to(i).inSingletonScope(),t.bind(e.ExtensionToolDelegator).to(x).inSingletonScope()})}function ve(e){let t=new T({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(te,async()=>({tools:n})),t.setRequestHandler(D,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const ye=new C(`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`,`3200`).option(`-v, --verbose`,`Enable verbose output`,!1).option(`--wait-for-extension`,`Wait for extension to connect before accepting MCP requests`,!1).action(async t=>{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(``);try{let n=typeof t.port==`string`?Number.parseInt(t.port,10):t.port;t.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${n}`),console.error(` Wait for extension: ${t.waitForExtension}`));let r=new ne({defaultScope:`Singleton`});r.load(_e());let i=r.get(e.ExtensionTaskQueue),a=r.get(e.ExtensionToolDelegator),o=new O;o.use(`*`,k({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let s=ge(r);o.route(`/extension`,s),o.get(`/health`,e=>{let t=i.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let c=ee({fetch:o.fetch,port:n});c.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${n} 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 ${n}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${n}/extension/tasks`),t.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{i.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let l=ve(a),u=new E;await l.connect(u),console.error(`Chrome extension MCP server started on stdio`);let d=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{i.clearAllTasks(`Server shutting down`),await u.close(),c.close(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>d(`SIGINT`)),process.on(`SIGTERM`,()=>d(`SIGTERM`))}catch(e){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)}}),be=`BROWSE_TOOL_CONFIG`,xe=`.browse-tool`,Se=`config.json`,Ce=[`json`,`text`,`quiet`],we={commands:{},tools:{}},Te=N.record(N.string(),N.unknown()),Ee=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(),tags:N.string().optional(),exclude:N.string().optional(),customTools:N.string().optional(),snippetsDir:N.string().optional(),registryPath:N.string().optional(),registryDir:N.string().optional(),pidsDir:N.string().optional(),profilesDir: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()}).partial().optional(),exec:N.object({format:N.enum(Ce).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),tools:N.object({format:N.enum(Ce).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()}),De=N.object({commands:Ee.default({}),tools:N.record(N.string(),Te).default({})});let I={config:we};function Oe(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 ke(e){let t=j.resolve(e);if(!A(t))throw Error(`Config path not found: ${t}`);return ae(t).isDirectory()?j.join(t,Se):t}function Ae(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??se(),a=Oe(e);if(a)return ke(a);if(n[be])return ke(n[be]);let o=j.join(r,xe,Se);if(A(o))return o;let s=j.join(i,xe,Se);if(A(s))return s}function je(e){let t=ie(e,`utf8`),n=JSON.parse(t);return De.parse(n)}function Me(e,t={}){let n=Ae(e,t);return n?{configPath:n,config:je(n)}:{config:we}}function Ne(e,t={}){return I=Me(e,t),I}function L(e){return(I.config.commands??{})[e]??{}}function Pe(e){return I.config.tools?.[e]??{}}function R(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 Fe=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??3200,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let t=await m().get(e.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 z(e){return new Fe(e)}const Ie={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 B(e,t,n){return n?`${Ie[t]}${e}${Ie.reset}`:e}function Le(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Re(e):ze(e,r)}function Re(e){return JSON.stringify(e,null,2)}function ze(e,t){let n=[];e.isError&&n.push(B(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(Be(r,t)):r.type===`image`?n.push(Ue(r,t)):n.push(B(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
3
+ `)}function Be(e,t){let n=e.text;try{return Ve(JSON.parse(n),t)}catch{return n}}function Ve(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=B(r,`cyan`,t),a=He(i,t);n.push(`${e}: ${a}`)}return n.join(`
4
+ `)}function He(e,t){return e===null?B(`null`,`gray`,t):e===void 0?B(`undefined`,`gray`,t):typeof e==`boolean`?B(String(e),e?`green`:`red`,t):typeof e==`number`?B(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?B(e,`blue`,t):e:Array.isArray(e)?e.length===0?B(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function Ue(e,t){let{mimeType:n,data:r}=e;return B(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function V(e,t){return B(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function We(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=B(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
5
+ `)}function Ge(e,t){let n=L(`tools`),r=R(e,`format`,t.format,n.format),i=R(e,`color`,t.color,n.color);return{port:R(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const Ke=new C(`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(d)).action(async function(e,t){let{port:n,formatterOptions:r}=Ge(this,t);try{let t=await z({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?We(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(V(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),qe=new C(`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(d)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=Ge(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(V(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await z({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=Le(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(V(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new C(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(Ke).addCommand(qe);const Je=new C(`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`,y).option(`--image <image>`,`Docker image tag to produce`,c).option(`--platform <platform>`,`Docker target platform`,n).action(async e=>{try{let t=await f({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: ${l(t.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),Ye=new C(`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(d)).action(async function(e,t,n){let r=L(`exec`),i=R(this,`format`,n.format,r.format),a=R(this,`color`,n.color,r.color),o=R(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(V(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await z({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=Le(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(V(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),H=`pageId`,U=`browserId`,Xe=N.record(N.string(),N.unknown()),Ze=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(),Qe=N.object({name:N.string().min(1),description:N.string().min(1),script:N.string().min(1),suggestionActions:N.string().min(1).optional(),capabilities:Xe,inputSchema:Ze}),$e=N.object({tools:N.array(Qe)}),et=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function tt(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function nt(e,t){if(et){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function W(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function rt(e){return W(e)&&Array.isArray(e.content)}function G(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 it(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 at(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 K(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(`-`)?ot(e,t,n):st(e,t,n)}function ot(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]=K(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=ct(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]=K(e,i+1,r.indent);s[t]=a,i=o}else s[t]=G(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]=K(e,i,t.indent);if(!W(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=ct(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]=K(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=G(a),i+=1}r.push(s);continue}r.push(G(a)),i+=1}return[r,i]}function st(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]=ct(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]=K(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=G(o),i+=1}return[r,i]}function ct(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 lt(e){let t=at(e);if(t.length===0)return{};let[n]=K(t,0,t[0].indent);return n}function ut(e){let t=e.inputSchema.properties;if(!t||!W(t))throw Error(`Custom tool "${e.name}" must define inputSchema.properties`);let n=t[H],r=t[U],i=n!==void 0&&W(n)&&n.type===`string`,a=r!==void 0&&W(r)&&r.type===`string`;if(!i&&!a)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId and/or inputSchema.properties.browserId as type "string"`);if(n!==void 0&&!i)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!a)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`)}async function dt(e){let t=S(await ce(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function ft(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 pt(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function mt(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(W(r)){let i=pt(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 ht(e,t,n){if(rt(t))return n?mt(t,n):t;if(typeof t==`string`)return n?{content:[{type:`text`,text:JSON.stringify({result:t,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:t}]};if(t===void 0)return n?{content:[{type:`text`,text:JSON.stringify({result:`Custom tool "${e}" completed successfully`,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:`Custom tool "${e}" completed successfully`}]};let r=W(t)&&n?pt(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var gt=class{constructor(e,t,n=new b,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 h(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[H]==`string`&&t[H].length>0?t[H]:void 0,r=typeof t[U]==`string`&&t[U].length>0?t[U]: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:it({...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[H]==`string`?n[H]:void 0,i=typeof n[U]==`string`?n[U]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":j.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:M.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}),nt(`Executing custom tool`,{toolName:t,directory:j.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:tt(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 nt(`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})}nt(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:tt(u)});let d=ht(t,u,i.suggestionActions),f=d.isError?d.content[0]?.text:void 0;return f&&r?.setStatus({code:M.ERROR,message:f}),d})}async loadTools(e){let t=j.resolve(e),n=j.join(t,`tools.yaml`),r=lt(await ce(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=$e.parse(r);return Promise.all(i.tools.map(async e=>{ut(e);let n=j.resolve(t,e.script);await le(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=ft(n,await dt(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function _t(t){let n=new O;return n.get(`/browsers`,async n=>{try{let r=t.get(e.BrowserService),i=t.get(e.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.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.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.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 vt(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 yt(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 bt(e){return yt(new Date().getTime()-e.getTime())}const xt=`table-container`,St=`stat-card`,Ct=`browser-row`,wt=`page-row`,Tt=`status-active`,Et=`url-cell`,q=`timestamp-cell`,J=`actions-cell`,Y=`kill-btn`,Dt=`empty-state`;function Ot({browsers:e}){return e.length===0?P(`div`,{class:xt,children:F(`div`,{class:Dt,children:[P(`h3`,{children:`No Active Browsers`}),P(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):P(`div`,{class:xt,children:F(`table`,{class:`browser-table`,children:[P(`thead`,{children:F(`tr`,{children:[P(`th`,{children:`ID`}),P(`th`,{children:`Status`}),P(`th`,{children:`URL / Title`}),P(`th`,{children:`Created`}),P(`th`,{children:`Age`}),P(`th`,{children:`Actions`})]})}),P(`tbody`,{id:`browser-table-body`,children:e.map(e=>F(fe,{children:[F(`tr`,{class:Ct,children:[P(`td`,{children:e.id}),P(`td`,{children:F(`span`,{class:Tt,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),P(`td`,{children:e.profileName||`Default Profile`}),P(`td`,{class:q,children:vt(e.createdAt)}),P(`td`,{class:q,children:bt(e.createdAt)}),P(`td`,{class:J,children:P(`button`,{class:Y,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>F(`tr`,{class:wt,children:[F(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),P(`td`,{children:P(`span`,{class:Tt,children:`Open`})}),P(`td`,{class:Et,title:t.url,children:t.title||t.url||`about:blank`}),P(`td`,{class:q,children:vt(t.createdAt)}),P(`td`,{class:q,children:bt(t.createdAt)}),P(`td`,{class:J,children:P(`button`,{class:Y,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]}))]}))})]})})}function kt({stats:e}){return F(`div`,{class:`stats-container`,children:[F(`div`,{class:St,children:[P(`h3`,{children:`Active Browsers`}),P(`div`,{class:`value`,children:e.totalBrowsers})]}),F(`div`,{class:St,children:[P(`h3`,{children:`Active Pages`}),P(`div`,{class:`value`,children:e.totalPages})]})]})}function At({browsers:e,stats:t}){return F(`div`,{class:`dashboard-container`,children:[F(`div`,{class:`dashboard-header`,children:[F(`div`,{children:[P(`h1`,{children:`Playwright MCP Dashboard`}),P(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),F(`div`,{children:[P(`button`,{id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),P(`button`,{id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),P(kt,{stats:t}),P(Ot,{browsers:e}),P(`script`,{children:de(`
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('.${wt} .value');
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('.${Ct}');
55
+ const container = tbody.closest('.${xt}');
56
56
  if (container) {
57
57
  container.innerHTML = \`
58
- <div class="${Ot}">
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 = '${Tt}';
72
+ browserRow.className = '${Ct}';
73
73
  browserRow.innerHTML = \`
74
74
  <td>\${browser.id}</td>
75
- <td><span class="${G}">\${browser.pages.length} page\${browser.pages.length !== 1 ? 's' : ''}</span></td>
75
+ <td><span class="${Tt}">\${browser.pages.length} page\${browser.pages.length !== 1 ? 's' : ''}</span></td>
76
76
  <td>\${browser.profileName || 'Default Profile'}</td>
77
- <td class="${K}">\${this.formatTimestamp(browser.createdAt)}</td>
78
- <td class="${K}">\${this.formatAge(browser.createdAt)}</td>
79
- <td class="${q}"><button class="${J}" onclick="dashboard.killBrowser('\${browser.id}')">Kill</button></td>
77
+ <td class="${q}">\${this.formatTimestamp(browser.createdAt)}</td>
78
+ <td class="${q}">\${this.formatAge(browser.createdAt)}</td>
79
+ <td class="${J}"><button class="${Y}" 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 = '${Et}';
86
+ pageRow.className = '${wt}';
87
87
  const isActive = browser.currentPageId === page.id;
88
88
  pageRow.innerHTML = \`
89
89
  <td>\${page.id}\${isActive ? ' (active)' : ''}</td>
90
- <td><span class="${G}">Open</span></td>
91
- <td class="${Dt}" title="\${this.escapeHtml(page.url)}">\${this.escapeHtml(page.title || page.url || 'about:blank')}</td>
92
- <td class="${K}">\${this.formatTimestamp(page.createdAt)}</td>
93
- <td class="${K}">\${this.formatAge(page.createdAt)}</td>
94
- <td class="${q}"><button class="${J}" onclick="dashboard.killPage('\${page.id}')">Close</button></td>
90
+ <td><span class="${Tt}">Open</span></td>
91
+ <td class="${Et}" title="\${this.escapeHtml(page.url)}">\${this.escapeHtml(page.title || page.url || 'about:blank')}</td>
92
+ <td class="${q}">\${this.formatTimestamp(page.createdAt)}</td>
93
+ <td class="${q}">\${this.formatAge(page.createdAt)}</td>
94
+ <td class="${J}"><button class="${Y}" 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 Mt({title:e,children:t}){return M(`html`,{lang:`en`,children:[M(`head`,{children:[j(`meta`,{charset:`UTF-8`}),j(`meta`,{name:`viewport`,content:`width=device-width, initial-scale=1.0`}),j(`title`,{children:e}),j(`style`,{children:me(`
190
+ `)})]})}function jt({title:e,children:t}){return F(`html`,{lang:`en`,children:[F(`head`,{children:[P(`meta`,{charset:`UTF-8`}),P(`meta`,{name:`viewport`,content:`width=device-width, initial-scale=1.0`}),P(`title`,{children:e}),P(`style`,{children:de(`
191
191
  * {
192
192
  margin: 0;
193
193
  padding: 0;
@@ -428,8 +428,8 @@ window.addEventListener('beforeunload', () => {
428
428
  margin-bottom: 0.5rem;
429
429
  color: #999;
430
430
  }
431
- `)})]}),j(`body`,{children:t})]})}function Nt(t){let n=new E;return n.get(`/`,async n=>{try{let r=t.get(e.BrowserService),i=t.get(e.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(j(Mt,{title:`Playwright MCP Dashboard`,children:j(jt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html(j(Mt,{title:`Playwright MCP Dashboard - Error`,children:M(`div`,{style:`padding: 2rem; text-align: center;`,children:[j(`h1`,{children:`Failed to load dashboard`}),j(`p`,{children:e instanceof Error?e.message:String(e)}),j(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function Pt(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 Ft(t){try{return t.get(e.TelemetryService)}catch{return new b}}function It(t){let n=new E,r=t.get(e.PageRegistry),i=t.get(e.BrowserService),a=t.get(e.ExtensionTaskQueue),o=Ft(t),s=new vt(r,a,o,i);n.use(`*`,ie({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.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:k.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||{},u=typeof l.pageId==`string`?l.pageId:void 0;if(u){let n=t.get(e.BrowserService),i=r.get(u);i&&n.recordBrowserActivity(i.browserId,u)}let d=Pt(c);return d?(a?.setStatus({code:k.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}}),n.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}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/browsers`,n=>{try{let r=t.get(e.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.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 u=await s.executeTool(i,a.tool,a.arguments||{});if(c){let n=t.get(e.BrowserService),i=r.get(c);i&&n.recordBrowserActivity(i.browserId,c)}let d=Pt(u);return d?(l?.setStatus({code:k.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}}),n.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}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}});let c=ye(t);n.route(`/extension`,c);let l=yt(t);n.route(`/api`,l);let u=Nt(t);return n.route(`/`,u),n}function Lt(t,n,r){t.get(`/ws/extension/:browserId`,r(t=>{let r=t.req.param(`browserId`),i=null;return{onOpen(t,a){try{i=n.get(e.WebSocketHub).addConnection(a,r)}catch{a.close(1011,`Internal server error`)}},onMessage(t){if(i)try{let r=n.get(e.WebSocketHub),a=typeof t.data==`string`?t.data:t.data.toString();r.handleMessage(i,a)}catch{}},onClose(){if(i)try{n.get(e.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{n.get(e.WebSocketHub).removeConnection(i)}catch{}}}})),t.get(`/ws/stats`,t=>{try{let r=n.get(e.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 Rt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function zt(e=process.cwd()){let t=O.resolve(e);for(;;){for(let e of Rt)if(D(O.join(t,e)))return t;let e=O.dirname(t);if(e===t)return process.cwd();t=e}}const Bt=new C(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(d)).option(`--host <host>`,`Host to bind`,o()).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`).action(async function(t){let n=P(`httpServe`),i={port:F(this,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),headless:F(this,`headless`,t.headless,n.headless),idleTimeout:F(this,`idleTimeout`,t.idleTimeout,n.idleTimeout===void 0?void 0:String(n.idleTimeout),process.env[a]),host:F(this,`host`,t.host,n.host,process.env.PLAYWRIGHT_HOST),registryDir:F(this,`registryDir`,t.registryDir,n.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:F(this,`registryPath`,t.registryPath,n.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:F(this,`pidsDir`,t.pidsDir,n.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:F(this,`profilesDir`,t.profilesDir,n.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:F(this,`snippetsDir`,t.snippetsDir,n.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR)};try{let t=Number.parseInt(i.port,10),n=`browse-tool-http`,o=process.env.NODE_ENV||`development`,s=zt(process.cwd()),c=i.registryPath||i.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;c&&(process.env.PLAYWRIGHT_REGISTRY_DIR=c,process.env.PORT_REGISTRY_PATH=c),process.env.PLAYWRIGHT_HOST=i.host,process.env.PLAYWRIGHT_PORT=i.port,process.env[a]=i.idleTimeout,i.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=i.pidsDir),i.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=i.profilesDir),i.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=O.resolve(i.snippetsDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${t}`),console.log(` Host: ${i.host}`),console.log(` Headless: ${i.headless}`),console.log(` Idle Timeout: ${i.idleTimeout} minutes`),process.env.BROWSE_TOOL_SNIPPETS_DIR&&console.log(` Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`);let l=r(),u=It(l),{injectWebSocket:d,upgradeWebSocket:f}=pe({app:u});Lt(u,l,f);let p=l.get(e.WebSocketHub),m=l.get(e.ExtensionTaskQueue),h=l.get(e.BrowserService),g=l.get(e.ExtensionSessionRegistry),_=l.get(e.PageRegistry),y=l.get(e.IdleCleanupService);y.start(),p.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=h.getBrowser(n),i,a;if(r&&(r.mode===`extension`||r.mode===`vm`)){i=r.id;let t=_.findByBrowser(i);a=r.currentPageId||t[0]?.id||``,p.reassignConnection(e.id,i),a||(a=_.registerExtensionPage(i),r.pageIds.add(a),r.currentPageId=a),console.log(`[WebSocket] Extension connected to existing browser: ${i}, pageId: ${a}`)}else{i=n,h.registerExtensionBrowserWithId(i),a=_.registerExtensionPage(i);let e=h.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=_.get(a);o&&(o.extensionTabId=t.payload.tabId,o.url=t.payload.url??o.url);let s=g.register({browserId:i,tabId:t.payload.tabId,url:t.payload.url,metadata:{transport:`websocket`}});p.sendSessionAck(e,t.id??``,s.id,s.controlMode),p.broadcastPageCreated(i,a,_.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=m.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&h.recordBrowserActivity(e.browserId,e.pageId)}},onTabMapped:(e,t)=>{if(t.type===`tab:mapped`){let e=_.get(t.payload.pageId);if(!e)return;e.extensionTabId=t.payload.tabId,h.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&g.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let b=new le(process.env.PORT_REGISTRY_PATH);c?console.log(` Registry path: ${c}`):console.log(` Registry path: default (~/.port-registry/ports.json)`);let x=await b.reservePort({repositoryPath:s,serviceName:n,serviceType:`tool`,environment:o,preferredPort:t,pid:process.pid,host:i.host,force:!0,portRange:{min:t,max:t},metadata:{healthCheckUrl:`${v(i.host,t)}/health`,headless:i.headless,idleTimeout:i.idleTimeout}});if(!x.success||!x.record)throw Error(x.error||`Failed to reserve port ${t} in global registry`);let S=t,C;try{C=ee({fetch:u.fetch,port:S,hostname:i.host})}catch(e){throw await b.releasePort({repositoryPath:s,serviceName:n,serviceType:`tool`,environment:o,pid:process.pid}),e}d(C);let w=v(i.host,S);console.log(`HTTP server listening on ${w}`),console.log(` Health check: ${w}/health`),console.log(` Execute tool: POST ${w}/execute`),console.log(`
432
- Press Ctrl+C to stop the server`);let T=async t=>{console.log(`\n\n${t} received. Shutting down gracefully...`);try{y.stop(),C.close(),console.log(`Server closed`);try{await l.get(e.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{await b.releasePort({repositoryPath:s,serviceName:n,serviceType:`tool`,environment:o,pid:process.pid}),console.log(`Service deregistered`)}catch{}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>T(`SIGINT`)),process.on(`SIGTERM`,()=>T(`SIGTERM`))}catch(e){console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Vt={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 Ht(e){let t=new Set;for(let[n,r]of Object.entries(Vt))r.some(t=>e.includes(t))&&t.add(n);return t}const Ut=3e3,Wt=[`browser_launch`],Gt=[`browser_new_page`];function Kt(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 qt(e){await new Promise(t=>setTimeout(t,e))}async function Jt(e){let t=new AbortController,n=setTimeout(()=>t.abort(),Ut);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 Yt(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function Xt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),Ut);try{let r=await fetch(Yt(e,t),{signal:n.signal});if(!r.ok)throw Error(`HTTP error ${r.status}: ${r.statusText}`);let i=await r.json();if(i.error)throw Error(i.error);if(!Array.isArray(i.tools))throw Error(`Invalid /custom-tools response: missing tools array`);return i.tools}finally{clearTimeout(r)}}async function Zt(e){let t;for(let n=1;n<=4;n+=1)try{return await Jt(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await qt(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function Qt(e,t){let n;for(let r=1;r<=4;r+=1)try{return await Xt(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await qt(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function $t(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?Ht(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 en(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:r,toolFilter:i,customToolsDir:a,enforcedProfileName:o}=e,s=i?.tags?.length?$t(i):null,c=new Set(i?.exclude??[]),l=s!==null||c.size>0,u=new T({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),d={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}},f=[],p=[],m=new Set;function h(e){return l?e.filter(e=>c.has(e.name)?!1:s===null?!0:s.has(e.name)):e}function g(e){return e.filter(e=>!c.has(e.name))}function _(e){return o?{...e,profileName:o}:e}function v(e){if(!o)return e;let t=e?.content?.[0]?.text;if(!t)return e;try{let n=JSON.parse(t),r=(Array.isArray(n.profiles)?n.profiles:[]).filter(e=>e?.name===o);return{...e,content:[{...e.content[0],text:JSON.stringify({...n,profileCount:r.length,profiles:r},null,2)},...e.content.slice(1)]}}catch{return e}}function y(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function b(e){return c.has(e)?!1:s===null?!0:s.has(e)}return u.setRequestHandler(re,async()=>{try{let e=await Zt(t);f=e,m=new Set(e.map(e=>e.name));let r=a?await Qt(t,a):[];p=r;let i=[...h(e),...g(r).map(e=>y(e))];return n&&i.push(d),{tools:i}}catch(e){if(f.length>0||p.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[...h(f),...g(p).map(e=>y(e))];return n&&t.push(d),{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})}}),u.setRequestHandler(ne,async e=>{let{name:i,arguments:o}=e.params,s=new Set(p.map(e=>e.name));if(i!==`browser_list_session`&&!b(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=_(c));try{if(a&&!s.has(i)&&!m.has(i))try{p=await Qt(t,a),s=new Set(p.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?Yt(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(Wt.includes(i)){let e=Kt(o.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if(Gt.includes(i)){let e=Kt(o.result);e?.pageId&&e?.browserId&&n.trackPage(e.pageId,e.browserId,e.url)}}return i===`browser_list_profiles`&&o.result?v(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}}}),u}const Y=`stdio`,tn=`http`,X=`streamable-http`,nn=new Set([Y,tn,X]),rn=`chromium`,an=new Set([rn,`firefox`,`webkit`]),on=new Set([`playwright`,`extension`,`vm`,`stealth`]),Z=`SIGINT`,Q=`SIGTERM`;var sn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...nn].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},cn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...an].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},ln=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...on].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function un(e){let t=e.toLowerCase();if(!an.has(t))throw new cn(e);return t}function dn(e){let t=e.toLowerCase();if(!on.has(t))throw new ln(e);return t}function fn(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 pn(e){let t=e.toLowerCase();return t===tn?X:t}function mn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function hn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function gn(e){let t=e.tags?hn(e.tags):void 0,n=e.exclude?hn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var _n=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}},vn=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}},yn=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 xn(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 _n(r,t,{cause:i});e.clear()}async function Sn(e,t,n){await e.start();let r=!1,i=async i=>{if(r)return;r=!0,console.error(`\nReceived ${i}, shutting down gracefully...`);let a;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await xn(t,n))}catch(e){a=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,a)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),a??=t}if(a){let e=new vn(i,{cause:a});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(Z,()=>i(Z)),process.once(Q,()=>i(Q))}const Cn=new C(`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: ${[...an].join(`, `)}`,rn).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`,o()).option(`--port <port>`,`Port for streamable HTTP transport`,`3201`).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...on].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(`--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`).action(async function(t){let n=P(`mcpServe`),r={type:F(this,`type`,t.type,n.type),browser:F(this,`browser`,t.browser,n.browser),headless:F(this,`headless`,t.headless,n.headless),profile:F(this,`profile`,t.profile,n.profile),mode:F(this,`mode`,t.mode,n.mode),host:F(this,`host`,t.host,n.host,process.env.PLAYWRIGHT_HOST),port:F(this,`port`,t.port,n.port,process.env.PLAYWRIGHT_MCP_PORT),tags:F(this,`tags`,t.tags,n.tags),exclude:F(this,`exclude`,t.exclude,n.exclude),customTools:F(this,`customTools`,t.customTools,n.customTools),snippetsDir:F(this,`snippetsDir`,t.snippetsDir,n.snippetsDir),idleTimeout:F(this,`idleTimeout`,t.idleTimeout,n.idleTimeout===void 0?void 0:String(n.idleTimeout),process.env[a]),registryPath:F(this,`registryPath`,t.registryPath,n.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:F(this,`registryDir`,t.registryDir,n.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:F(this,`pidsDir`,t.pidsDir,n.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:F(this,`profilesDir`,t.profilesDir,n.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR)},i=pn(r.type);try{if(!nn.has(i))throw new sn(i);let t=un(r.browser),n=r.mode?dn(r.mode):void 0,c=fn(r.port??3201),l=gn(r),u=r.customTools?O.resolve(r.customTools):void 0,d=r.snippetsDir?O.resolve(r.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${i}`),i===X&&console.error(` MCP endpoint: http://${r.host??o()}:${c}/mcp`),console.error(` Default browser: ${t}`),console.error(` Headless: ${r.headless}`),n&&console.error(` Default mode: ${n}`),r.profile&&console.error(` Profile: ${r.profile}`),l?.tags?.length&&console.error(` Tags: ${l.tags.join(`, `)}`),l?.exclude?.length&&console.error(` Exclude: ${l.exclude.join(`, `)}`),u&&console.error(` Custom tools: ${u}`),d&&console.error(` Snippets dir: ${d}`),r.idleTimeout&&console.error(` Idle timeout: ${r.idleTimeout} minutes`);let f=r.registryPath||r.registryDir;f&&(process.env.PLAYWRIGHT_REGISTRY_DIR=f,process.env.PORT_REGISTRY_PATH=f,console.error(` Registry path: ${f}`)),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}`)),d&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=d),r.idleTimeout&&(process.env[a]=r.idleTimeout),r.host&&(process.env.PLAYWRIGHT_HOST=r.host),process.env.PLAYWRIGHT_PORT=s().toString();let h=await m().get(e.HttpServerManager).ensureRunning();if(!h.running)throw new yn;let y=v(o(),h.port),b=h.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${b} on port ${h.port}`),i===Y){let e=new g;await Sn(new p(en({httpBaseUrl:y,sessionTracker:e,defaultMode:n,toolFilter:l,customToolsDir:u,enforcedProfileName:r.profile})),e,y);return}let x=new _(({headers:e})=>{let t=mn(e,`x-profile`)??r.profile,i=new g;return{server:en({httpBaseUrl:y,sessionTracker:i,defaultMode:n,toolFilter:l,customToolsDir:u,enforcedProfileName:t}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await xn(i,y)}}},{host:r.host??o(),port:c});await x.start();let S=!1,C=async e=>{if(!S){S=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await x.stop(),process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(Z,()=>C(Z)),process.once(Q,()=>C(Q))}catch(e){(e instanceof sn||e instanceof cn||e instanceof ln||e instanceof yn)&&(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 C(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=P(`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 u().get(e.HttpServerManager).getStatus();if(r.running){let e=process.env.PLAYWRIGHT_HOST??t.host??o();console.log(`HTTP Server: Running`),console.log(` PID: ${r.pid}`),console.log(` Port: ${r.port}`),console.log(` Health: ${v(e,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)}}),Tn=new C(`stop`).description(`Stop HTTP server and clean up registry/PID files`).action(async()=>{try{let t=P(`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 u().get(e.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)}}),En=new Set([`--config`]);function Dn(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(En.has(n)){t+=1;continue}if(![...En].some(e=>n.startsWith(`${e}=`))&&!n.startsWith(`-`))return n}}function On(e,t=!1){if(t)return!1;let n=Dn(e);return n?n===`tools`?!0:n===`help`&&e.includes(`tools`):!1}function kn(e){return e.replace(/_/g,`-`)}function $(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function An(e){let t=new Set;e.type&&t.add(e.type);for(let n of e.oneOf??[])n.type&&t.add(n.type);return t}function jn(e,t){let n=An(t);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 Mn(e,t){let n=$(e);return t.type===`boolean`?`--${n}`:`--${n} <value>`}function Nn(e){let{definition:t,execute:n}=e,r=new C(kn(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=Mn(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-${$(e)}`,`Disable ${a}`):r.option(i,a,n.default):n.enum&&n.enum.length>0?r.addOption(new w(i,a).choices(n.enum)):r.option(i,a)}return r.action(async function(){let e=this.optsWithGlobals(),r=P(`tools`),i=F(this,`format`,e.format??`json`,r.format),s=F(this,`color`,e.color??!0,r.color),c=F(this,`port`,String(e.port??d),r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),l={format:i,color:s},u=Le(t.name);try{let r={};for(let[t,n]of Object.entries(a)){let i=n,a=$(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`)&&u[t]!==void 0&&(s=u[t]),s!==void 0&&(typeof s==`string`&&i.type!==`string`?r[t]=jn(s,i):r[t]=s)}for(let e of o)if(r[e]===void 0){let t=$(e);console.error(z(`Missing required option: --${t}`,l.color)),process.exit(1)}let i=Number.parseInt(c,10),s=I({port:i}),d=n?await n(r,{command:this,formatterOptions:l,port:i}):await s.execute(t.name,r),f=R(d,l);f&&console.log(f),d.isError&&process.exit(1)}catch(e){console.error(z(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),r}function Pn(e){return e.map(Nn)}const Fn={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 In(){return new C(`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(d))}function Ln(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 Rn(){return{definition:Fn,execute:async(e,t)=>{let n=await I({port:t.port}).listBrowsers();return{content:[{type:`text`,text:JSON.stringify(Ln(n),null,2)}]}}}}function zn(e){return[...e.map(e=>({definition:e})),Rn()]}async function Bn(e,t){let n=P(`tools`),r=Number(t?.port??process.env.PLAYWRIGHT_PORT??n.port??d);try{let t=Pn(zn(await I({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(`${z(`Failed to load tool commands: ${t}`,!0)}\n`),process.stderr.write(`Make sure the HTTP server is running: browse-tool http-serve
434
- `)}}function Vn(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 Hn(){let e=Ie(process.argv.slice(2)),t=process.env.PLAYWRIGHT_PORT??e.config.commands.tools?.port??e.config.commands.exec?.port??d,n=new C;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(ge),n.addCommand(Cn),n.addCommand(Bt),n.addCommand(Se),n.addCommand(Xe),n.addCommand(Tn),n.addCommand(wn),n.addCommand(Ze),n.addCommand(Je),n.addCommand(Ye);let r=In();On(process.argv.slice(2),process.env.PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS===`1`)&&await Bn(r,{port:Vn(process.argv.slice(2),t)}),n.addCommand(r),await n.parseAsync(process.argv)}Hn().catch(e=>{console.error(e instanceof Error?e.message:String(e)),process.exit(1)});export{};
431
+ `)})]}),P(`body`,{children:t})]})}function Mt(t){let n=new O;return n.get(`/`,async n=>{try{let r=t.get(e.BrowserService),i=t.get(e.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(P(jt,{title:`Playwright MCP Dashboard`,children:P(At,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html(P(jt,{title:`Playwright MCP Dashboard - Error`,children:F(`div`,{style:`padding: 2rem; text-align: center;`,children:[P(`h1`,{children:`Failed to load dashboard`}),P(`p`,{children:e instanceof Error?e.message:String(e)}),P(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function Nt(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 Pt(t){try{return t.get(e.TelemetryService)}catch{return new b}}function Ft(t){let n=new O,r=t.get(e.PageRegistry),i=t.get(e.BrowserService),a=t.get(e.ExtensionTaskQueue),o=Pt(t),s=new gt(r,a,o,i);n.use(`*`,k({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.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:M.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||{},u=typeof l.pageId==`string`?l.pageId:void 0;if(u){let n=t.get(e.BrowserService),i=r.get(u);i&&n.recordBrowserActivity(i.browserId,u)}let d=Nt(c);return d?(a?.setStatus({code:M.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}}),n.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}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/browsers`,n=>{try{let r=t.get(e.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.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 u=await s.executeTool(i,a.tool,a.arguments||{});if(c){let n=t.get(e.BrowserService),i=r.get(c);i&&n.recordBrowserActivity(i.browserId,c)}let d=Nt(u);return d?(l?.setStatus({code:M.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}}),n.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}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}});let c=ge(t);n.route(`/extension`,c);let l=_t(t);n.route(`/api`,l);let u=Mt(t);return n.route(`/`,u),n}function It(t,n,r){t.get(`/ws/extension/:browserId`,r(t=>{let r=t.req.param(`browserId`),i=null;return{onOpen(t,a){try{i=n.get(e.WebSocketHub).addConnection(a,r)}catch{a.close(1011,`Internal server error`)}},onMessage(t){if(i)try{let r=n.get(e.WebSocketHub),a=typeof t.data==`string`?t.data:t.data.toString();r.handleMessage(i,a)}catch{}},onClose(){if(i)try{n.get(e.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{n.get(e.WebSocketHub).removeConnection(i)}catch{}}}})),t.get(`/ws/stats`,t=>{try{let r=n.get(e.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 Lt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function Rt(e=process.cwd()){let t=j.resolve(e);for(;;){for(let e of Lt)if(A(j.join(t,e)))return t;let e=j.dirname(t);if(e===t)return process.cwd();t=e}}function zt(e,t){let n=j.resolve(e),r=j.extname(n)===`.json`?j.dirname(n):n;return j.join(r,t)}async function Bt(e,t){let n=await e.registerProcess({...t,force:!0});if(!n.success||!n.record)throw Error(n.error||`Failed to register process ${t.serviceName}`);let r=!1;return{release:async()=>{if(r)return;r=!0;let n=await e.releaseProcess({repositoryPath:t.repositoryPath,serviceName:t.serviceName,serviceType:t.serviceType,environment:t.environment,pid:t.pid,kill:!1,releasePort:!1});!n.success&&!n.error?.includes(`No matching process entry`)&&console.error(`Failed to release process registry entry for ${t.serviceName}: ${n.error}`)}}}const Vt=new C(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(d)).option(`--host <host>`,`Host to bind`,o()).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`).action(async function(t){let n=L(`httpServe`),i={port:R(this,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),headless:R(this,`headless`,t.headless,n.headless),idleTimeout:R(this,`idleTimeout`,t.idleTimeout,n.idleTimeout===void 0?void 0:String(n.idleTimeout),process.env[a]),host:R(this,`host`,t.host,n.host,process.env.PLAYWRIGHT_HOST),registryDir:R(this,`registryDir`,t.registryDir,n.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:R(this,`registryPath`,t.registryPath,n.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:R(this,`pidsDir`,t.pidsDir,n.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:R(this,`profilesDir`,t.profilesDir,n.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:R(this,`snippetsDir`,t.snippetsDir,n.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR)},o;try{let t=Number.parseInt(i.port,10),n=`browse-tool-http`,s=process.env.NODE_ENV||`development`,c=Rt(process.cwd()),l=i.registryPath||i.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;l&&(process.env.PLAYWRIGHT_REGISTRY_DIR=l,process.env.PORT_REGISTRY_PATH=l,process.env.PROCESS_REGISTRY_PATH=zt(l,`processes.json`)),process.env.PLAYWRIGHT_HOST=i.host,process.env.PLAYWRIGHT_PORT=i.port,process.env[a]=i.idleTimeout,i.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=i.pidsDir),i.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=i.profilesDir),i.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=j.resolve(i.snippetsDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${t}`),console.log(` Host: ${i.host}`),console.log(` Headless: ${i.headless}`),console.log(` Idle Timeout: ${i.idleTimeout} minutes`),process.env.BROWSE_TOOL_SNIPPETS_DIR&&console.log(` Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`);let u=r(),d=u.get(e.ProcessRegistryService),f=Ft(u),{injectWebSocket:p,upgradeWebSocket:m}=ue({app:f});It(f,u,m);let h=u.get(e.WebSocketHub),g=u.get(e.ExtensionTaskQueue),_=u.get(e.BrowserService),y=u.get(e.ExtensionSessionRegistry),b=u.get(e.PageRegistry),x=u.get(e.IdleCleanupService);x.start(),h.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=_.getBrowser(n),i,a;if(r&&(r.mode===`extension`||r.mode===`vm`)){i=r.id;let t=b.findByBrowser(i);a=r.currentPageId||t[0]?.id||``,h.reassignConnection(e.id,i),a||(a=b.registerExtensionPage(i),r.pageIds.add(a),r.currentPageId=a),console.log(`[WebSocket] Extension connected to existing browser: ${i}, pageId: ${a}`)}else{i=n,_.registerExtensionBrowserWithId(i),a=b.registerExtensionPage(i);let e=_.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=b.get(a);o&&(o.extensionTabId=t.payload.tabId,o.url=t.payload.url??o.url);let s=y.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,b.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=g.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&_.recordBrowserActivity(e.browserId,e.pageId)}},onTabMapped:(e,t)=>{if(t.type===`tab:mapped`){let e=b.get(t.payload.pageId);if(!e)return;e.extensionTabId=t.payload.tabId,_.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&y.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let S=new oe(process.env.PORT_REGISTRY_PATH);l?console.log(` Registry path: ${l}`):console.log(` Registry path: default (~/.port-registry/ports.json)`);let C=await S.reservePort({repositoryPath:c,serviceName:n,serviceType:`tool`,environment:s,preferredPort:t,pid:process.pid,host:i.host,force:!0,portRange:{min:t,max:t},metadata:{healthCheckUrl:`${v(i.host,t)}/health`,headless:i.headless,idleTimeout:i.idleTimeout}});if(!C.success||!C.record)throw Error(C.error||`Failed to reserve port ${t} in global registry`);let w=t,T;try{T=ee({fetch:f.fetch,port:w,hostname:i.host})}catch(e){throw await S.releasePort({repositoryPath:c,serviceName:n,serviceType:`tool`,environment:s,pid:process.pid}),e}p(T),o=await Bt(d,{repositoryPath:c,serviceName:n,serviceType:`service`,environment:s,pid:process.pid,port:w,host:i.host,metadata:{healthCheckUrl:`${v(i.host,w)}/health`,headless:i.headless,idleTimeout:i.idleTimeout,transport:`http`}});let E=v(i.host,w);console.log(`HTTP server listening on ${E}`),console.log(` Health check: ${E}/health`),console.log(` Execute tool: POST ${E}/execute`),console.log(`
432
+ Press Ctrl+C to stop the server`);let D=async t=>{console.log(`\n\n${t} received. Shutting down gracefully...`);try{x.stop(),T.close(),console.log(`Server closed`);try{await u.get(e.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{o&&await o.release()}catch(e){console.error(`Process registry cleanup error:`,e)}try{await S.releasePort({repositoryPath:c,serviceName:n,serviceType:`tool`,environment:s,pid:process.pid}),console.log(`Service deregistered`)}catch{}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>D(`SIGINT`)),process.on(`SIGTERM`,()=>D(`SIGTERM`))}catch(e){if(o)try{await o.release()}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Ht={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 Ut(e){let t=new Set;for(let[n,r]of Object.entries(Ht))r.some(t=>e.includes(t))&&t.add(n);return t}const Wt=3e3,Gt=[`browser_launch`],Kt=[`browser_new_page`];function qt(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 Jt(e){await new Promise(t=>setTimeout(t,e))}async function Yt(e){let t=new AbortController,n=setTimeout(()=>t.abort(),Wt);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 Xt(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function Zt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),Wt);try{let r=await fetch(Xt(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 Qt(e){let t;for(let n=1;n<=4;n+=1)try{return await Yt(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 $t(e,t){let n;for(let r=1;r<=4;r+=1)try{return await Zt(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await Jt(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function en(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?Ut(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 tn(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:r,toolFilter:i,customToolsDir:a,enforcedProfileName:o}=e,s=i?.tags?.length?en(i):null,c=new Set(i?.exclude??[]),l=s!==null||c.size>0,u=new T({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),d={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}},f=[],p=[],m=new Set;function h(e){return l?e.filter(e=>c.has(e.name)?!1:s===null?!0:s.has(e.name)):e}function g(e){return e.filter(e=>!c.has(e.name))}function _(e){return o?{...e,profileName:o}:e}function v(e){if(!o)return e;let t=e?.content?.[0]?.text;if(!t)return e;try{let n=JSON.parse(t),r=(Array.isArray(n.profiles)?n.profiles:[]).filter(e=>e?.name===o);return{...e,content:[{...e.content[0],text:JSON.stringify({...n,profileCount:r.length,profiles:r},null,2)},...e.content.slice(1)]}}catch{return e}}function y(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function b(e){return c.has(e)?!1:s===null?!0:s.has(e)}return u.setRequestHandler(te,async()=>{try{let e=await Qt(t);f=e,m=new Set(e.map(e=>e.name));let r=a?await $t(t,a):[];p=r;let i=[...h(e),...g(r).map(e=>y(e))];return n&&i.push(d),{tools:i}}catch(e){if(f.length>0||p.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[...h(f),...g(p).map(e=>y(e))];return n&&t.push(d),{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})}}),u.setRequestHandler(D,async e=>{let{name:i,arguments:o}=e.params,s=new Set(p.map(e=>e.name));if(i!==`browser_list_session`&&!b(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=_(c));try{if(a&&!s.has(i)&&!m.has(i))try{p=await $t(t,a),s=new Set(p.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?Xt(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(Gt.includes(i)){let e=qt(o.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if(Kt.includes(i)){let e=qt(o.result);e?.pageId&&e?.browserId&&n.trackPage(e.pageId,e.browserId,e.url)}}return i===`browser_list_profiles`&&o.result?v(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}}}),u}const X=`stdio`,nn=`http`,Z=`streamable-http`,rn=new Set([X,nn,Z]),an=`chromium`,on=new Set([an,`firefox`,`webkit`]),sn=new Set([`playwright`,`extension`,`vm`,`stealth`]),Q=`SIGINT`,$=`SIGTERM`,cn=[`pnpm-workspace.yaml`,`nx.json`,`.git`],ln=`browse-tool-mcp-http`,un=`tool`,dn=`service`;var fn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...rn].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},pn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...on].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},mn=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...sn].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function hn(e){let t=e.toLowerCase();if(!on.has(t))throw new pn(e);return t}function gn(e){let t=e.toLowerCase();if(!sn.has(t))throw new mn(e);return t}function _n(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 vn(e){let t=e.toLowerCase();return t===nn?Z:t}function yn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function bn(e=process.cwd()){let t=j.resolve(e);for(;;){for(let e of cn)if(A(j.join(t,e)))return t;let e=j.dirname(t);if(e===t)return process.cwd();t=e}}function xn(e,t){let n=j.resolve(e),r=j.extname(n)===`.json`?j.dirname(n):n;return j.join(r,t)}async function Sn(e,t){let n=await e.registerProcess({...t,force:!0});if(!n.success||!n.record)throw Error(n.error||`Failed to register process ${t.serviceName}`);let r=!1;return{release:async()=>{if(r)return;r=!0;let n=await e.releaseProcess({repositoryPath:t.repositoryPath,serviceName:t.serviceName,serviceType:t.serviceType,environment:t.environment,pid:t.pid,kill:!1,releasePort:!1});!n.success&&!n.error?.includes(`No matching process entry`)&&console.error(`Failed to release process registry entry for ${t.serviceName}: ${n.error}`)}}}function Cn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function wn(e){let t=e.tags?Cn(e.tags):void 0,n=e.exclude?Cn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var Tn=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}},En=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}},Dn=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`}},On=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 kn(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 Tn(r,t,{cause:i});e.clear()}async function An(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 kn(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 En(a,{cause:o});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(Q,()=>a(Q)),process.once($,()=>a($))}const jn=new C(`mcp-serve`).description(`Start Playwright MCP server for browser automation`).option(`-t, --type <type>`,`Transport type: ${[X,Z].join(`, `)}`,X).option(`-b, --browser <browser>`,`Default browser type: ${[...on].join(`, `)}`,an).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`,o()).option(`--port <port>`,`Port for streamable HTTP transport`,`3201`).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...sn].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(`--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`).action(async function(t){let n=L(`mcpServe`),r={type:R(this,`type`,t.type,n.type),browser:R(this,`browser`,t.browser,n.browser),headless:R(this,`headless`,t.headless,n.headless),profile:R(this,`profile`,t.profile,n.profile),mode:R(this,`mode`,t.mode,n.mode),host:R(this,`host`,t.host,n.host,process.env.PLAYWRIGHT_HOST),port:R(this,`port`,t.port,n.port,process.env.PLAYWRIGHT_MCP_PORT),tags:R(this,`tags`,t.tags,n.tags),exclude:R(this,`exclude`,t.exclude,n.exclude),customTools:R(this,`customTools`,t.customTools,n.customTools),snippetsDir:R(this,`snippetsDir`,t.snippetsDir,n.snippetsDir),idleTimeout:R(this,`idleTimeout`,t.idleTimeout,n.idleTimeout===void 0?void 0:String(n.idleTimeout),process.env[a]),registryPath:R(this,`registryPath`,t.registryPath,n.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:R(this,`registryDir`,t.registryDir,n.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:R(this,`pidsDir`,t.pidsDir,n.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:R(this,`profilesDir`,t.profilesDir,n.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR)},i=vn(r.type),c=bn(process.cwd()),l=process.env.NODE_ENV||`development`,u;try{if(!rn.has(i))throw new fn(i);let t=hn(r.browser),n=r.mode?gn(r.mode):void 0,d=_n(r.port??3201),f=wn(r),h=r.customTools?j.resolve(r.customTools):void 0,y=r.snippetsDir?j.resolve(r.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${i}`),i===Z&&console.error(` MCP endpoint: http://${r.host??o()}:${d}/mcp`),console.error(` Default browser: ${t}`),console.error(` Headless: ${r.headless}`),n&&console.error(` Default mode: ${n}`),r.profile&&console.error(` Profile: ${r.profile}`),f?.tags?.length&&console.error(` Tags: ${f.tags.join(`, `)}`),f?.exclude?.length&&console.error(` Exclude: ${f.exclude.join(`, `)}`),h&&console.error(` Custom tools: ${h}`),y&&console.error(` Snippets dir: ${y}`),r.idleTimeout&&console.error(` Idle timeout: ${r.idleTimeout} minutes`);let b=r.registryPath||r.registryDir;b&&(process.env.PLAYWRIGHT_REGISTRY_DIR=b,process.env.PORT_REGISTRY_PATH=b,process.env.PROCESS_REGISTRY_PATH=xn(b,`processes.json`),console.error(` Registry path: ${b}`)),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}`)),y&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=y),r.idleTimeout&&(process.env[a]=r.idleTimeout),r.host&&(process.env.PLAYWRIGHT_HOST=r.host),process.env.PLAYWRIGHT_PORT=s().toString();let x=m(),S=x.get(e.ProcessRegistryService),C=await x.get(e.HttpServerManager).ensureRunning();if(!C.running)throw new Dn;let w=v(o(),C.port),ee=C.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${ee} on port ${C.port}`),i===X){let e=new g,i=new p(tn({httpBaseUrl:w,sessionTracker:e,defaultMode:n,toolFilter:f,customToolsDir:h,enforcedProfileName:r.profile}));u=await Sn(S,{repositoryPath:c,serviceName:`browse-tool-mcp`,serviceType:dn,environment:l,pid:process.pid,metadata:{transport:X,browser:t,mode:n}}),await An(i,e,w,async()=>{await u?.release()});return}let T=new oe(process.env.PORT_REGISTRY_PATH),E=await T.reservePort({repositoryPath:c,serviceName:ln,serviceType:un,environment:l,preferredPort:d,pid:process.pid,host:r.host??o(),force:!0,portRange:{min:d,max:d},metadata:{healthCheckUrl:`${v(r.host??o(),d)}/health`,transport:Z}});if(!E.success||!E.record)throw Error(E.error||`Failed to reserve ${Z} port ${d}`);let D=E.record.port;u=await Sn(S,{repositoryPath:c,serviceName:ln,serviceType:dn,environment:l,pid:process.pid,port:D,host:r.host??o(),metadata:{healthCheckUrl:`${v(r.host??o(),D)}/health`,transport:Z}});let te=new _(({headers:e})=>{let t=yn(e,`x-profile`)??r.profile,i=new g;return{server:tn({httpBaseUrl:w,sessionTracker:i,defaultMode:n,toolFilter:f,customToolsDir:h,enforcedProfileName:t}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await kn(i,w)}}},{host:r.host??o(),port:D});try{await te.start()}catch(e){try{await u.release()}catch(e){console.error(`Process registry cleanup error:`,e)}throw await T.releasePort({repositoryPath:c,serviceName:ln,serviceType:un,environment:l,pid:process.pid}),e}let O=!1,k=async e=>{if(!O){O=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await te.stop();try{await u?.release()}catch(e){console.error(`Process registry cleanup error:`,e)}await T.releasePort({repositoryPath:c,serviceName:ln,serviceType:un,environment:l,pid:process.pid}),process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(Q,()=>k(Q)),process.once($,()=>k($))}catch(e){if(u)try{await u.release()}catch(e){console.error(`Process registry cleanup error:`,e)}(e instanceof fn||e instanceof pn||e instanceof mn||e instanceof Dn)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new On({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)}}),Mn=new C(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=L(`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 u().get(e.HttpServerManager).getStatus();if(r.running){let e=process.env.PLAYWRIGHT_HOST??t.host??o();console.log(`HTTP Server: Running`),console.log(` PID: ${r.pid}`),console.log(` Port: ${r.port}`),console.log(` Health: ${v(e,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)}}),Nn=new C(`stop`).description(`Stop HTTP server and clean up registry/PID files`).action(async()=>{try{let t=L(`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 u().get(e.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)}}),Pn=new Set([`--config`]);function Fn(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(Pn.has(n)){t+=1;continue}if(![...Pn].some(e=>n.startsWith(`${e}=`))&&!n.startsWith(`-`))return n}}function In(e,t=!1){if(t)return!1;let n=Fn(e);return n?n===`tools`?!0:n===`help`&&e.includes(`tools`):!1}function Ln(e){return e.replace(/_/g,`-`)}function Rn(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function zn(e){let t=new Set;e.type&&t.add(e.type);for(let n of e.oneOf??[])n.type&&t.add(n.type);return t}function Bn(e,t){let n=zn(t);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 Vn(e,t){let n=Rn(e);return t.type===`boolean`?`--${n}`:`--${n} <value>`}function Hn(e){let{definition:t,execute:n}=e,r=new C(Ln(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=Vn(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-${Rn(e)}`,`Disable ${a}`):r.option(i,a,n.default):n.enum&&n.enum.length>0?r.addOption(new w(i,a).choices(n.enum)):r.option(i,a)}return r.action(async function(){let e=this.optsWithGlobals(),r=L(`tools`),i=R(this,`format`,e.format??`json`,r.format),s=R(this,`color`,e.color??!0,r.color),c=R(this,`port`,String(e.port??d),r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),l={format:i,color:s},u=Pe(t.name);try{let r={};for(let[t,n]of Object.entries(a)){let i=n,a=Rn(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`)&&u[t]!==void 0&&(s=u[t]),s!==void 0&&(typeof s==`string`&&i.type!==`string`?r[t]=Bn(s,i):r[t]=s)}for(let e of o)if(r[e]===void 0){let t=Rn(e);console.error(V(`Missing required option: --${t}`,l.color)),process.exit(1)}let i=Number.parseInt(c,10),s=z({port:i}),d=n?await n(r,{command:this,formatterOptions:l,port:i}):await s.execute(t.name,r),f=Le(d,l);f&&console.log(f),d.isError&&process.exit(1)}catch(e){console.error(V(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),r}function Un(e){return e.map(Hn)}const Wn={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 Gn(){return new C(`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(d))}function Kn(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 qn(){return{definition:Wn,execute:async(e,t)=>{let n=await z({port:t.port}).listBrowsers();return{content:[{type:`text`,text:JSON.stringify(Kn(n),null,2)}]}}}}function Jn(e){return[...e.map(e=>({definition:e})),qn()]}async function Yn(e,t){let n=L(`tools`),r=Number(t?.port??process.env.PLAYWRIGHT_PORT??n.port??d);try{let t=Un(Jn(await z({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(`${V(`Failed to load tool commands: ${t}`,!0)}\n`),process.stderr.write(`Make sure the HTTP server is running: browse-tool http-serve
434
+ `)}}function Xn(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 Zn(){let e=Ne(process.argv.slice(2)),t=process.env.PLAYWRIGHT_PORT??e.config.commands.tools?.port??e.config.commands.exec?.port??d,n=new C;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(pe),n.addCommand(jn),n.addCommand(Vt),n.addCommand(ye),n.addCommand(Je),n.addCommand(Nn),n.addCommand(Mn),n.addCommand(Ye),n.addCommand(Ke),n.addCommand(qe);let r=Gn();In(process.argv.slice(2),process.env.PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS===`1`)&&await Yn(r,{port:Xn(process.argv.slice(2),t)}),n.addCommand(r),await n.parseAsync(process.argv)}Zn().catch(e=>{console.error(e instanceof Error?e.message:String(e)),process.exit(1)});export{};
435
435
  //# sourceMappingURL=cli.mjs.map