@agimon-ai/browse-tool 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +11 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +23 -23
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{playwright-test-CGAZOnPw.cjs → playwright-test-YmYZs8Rs.cjs} +2 -2
- package/dist/{playwright-test-CGAZOnPw.cjs.map → playwright-test-YmYZs8Rs.cjs.map} +1 -1
- package/dist/{streamable-http-DsuJYpv3.cjs → streamable-http-DtALQyGP.cjs} +12 -12
- package/dist/streamable-http-DtALQyGP.cjs.map +1 -0
- package/dist/streamable-http-a245cRfE.mjs +15 -0
- package/dist/streamable-http-a245cRfE.mjs.map +1 -0
- package/dist/stubs/playwright-test.cjs +1 -1
- package/package.json +4 -4
- package/dist/streamable-http-B2_gj_b3.mjs +0 -15
- package/dist/streamable-http-B2_gj_b3.mjs.map +0 -1
- package/dist/streamable-http-DsuJYpv3.cjs.map +0 -1
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-B2_gj_b3.mjs";import"./playwright-test-lZlw-zhY.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{createProcessLease as T,resolveSiblingRegistryPath as te}from"@agimon-ai/foundation-process-registry";import{DEFAULT_PORT_RANGE as E,PortRegistryService as ne}from"@agimon-ai/foundation-port-registry";import{Server as D}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as O}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as k,ListToolsRequestSchema as A}from"@modelcontextprotocol/sdk/types.js";import{existsSync as j,readFileSync as re,statSync as ie}from"node:fs";import M from"node:path";import{Hono as N}from"hono";import{cors as ae}from"hono/cors";import{Container as oe,ContainerModule as se}from"inversify";import{SpanStatusCode as P}from"@opentelemetry/api";import{homedir as ce}from"node:os";import{z as F}from"zod";import{readFile as le,stat as ue}from"node:fs/promises";import{createNodeWebSocket as de}from"@hono/node-ws";import{raw as fe}from"hono/html";import{Fragment as pe,jsx as I,jsxs as L}from"hono/jsx/jsx-runtime";var me=`0.2.21`;const he=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function ge(e,t){if(!he)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function _e(n){let r=new N,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 ge(`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 ge(`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:{}}}),ge(`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 ve(e){if(e==null||e===``)return;let t=typeof e==`number`?e:typeof e==`string`?Number.parseInt(e,10):NaN;if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}const ye=[`pnpm-workspace.yaml`,`nx.json`,`.git`],be=`browse-tool-chrome`,xe=`tool`,Se=process.env.NODE_ENV||`development`,Ce=`127.0.0.1`;function we(e=process.cwd()){let t=M.resolve(e);for(;;){for(let e of ye)if(j(M.join(t,e)))return t;let e=M.dirname(t);if(e===t)return process.cwd();t=e}}function Te(){return new ne(process.env.PORT_REGISTRY_PATH)}async function Ee(e,t,n){let r=Te(),i=await r.reservePort({repositoryPath:e,serviceName:be,serviceType:xe,environment:Se,preferredPort:t,portRange:n,pid:process.pid,host:Ce,force:!0,metadata:{transport:`stdio`,mode:`chrome-serve`}});if(!i.success||!i.record)throw Error(i.error||`Failed to reserve port ${t}`);let a=!1;return{port:i.record.port,release:async()=>{if(a)return;a=!0;let n=await r.releasePort({repositoryPath:e,serviceName:be,serviceType:xe,environment:Se,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function De(){return new se(t=>{t.bind(e.ExtensionTaskQueue).to(i).inSingletonScope(),t.bind(e.ExtensionToolDelegator).to(x).inSingletonScope()})}function Oe(e){let t=new D({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(A,async()=>({tools:n})),t.setRequestHandler(k,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const ke=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`).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(``);let n,r;try{let i=ve(t.port),a=we(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=te(o,`processes.json`)),n=await Ee(a,i??E.min,i?{min:i,max:i}:E);let s=n.port;r=await T({repositoryPath:a,serviceName:be,serviceType:xe,environment:Se,port:s,host:Ce,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:t.waitForExtension}}),t.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${s}`),console.error(` Wait for extension: ${t.waitForExtension}`));let c=new oe({defaultScope:`Singleton`});c.load(De());let l=c.get(e.ExtensionTaskQueue),u=c.get(e.ExtensionToolDelegator),d=new N;d.use(`*`,ae({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=_e(c);d.route(`/extension`,f),d.get(`/health`,e=>{let t=l.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let p=ee({fetch:d.fetch,port:s});p.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${s} is already in use.`),console.error(`Recovery: Try a different port with --port <port>`)):console.error(`Error [SERVER_ERROR]: HTTP server error: ${e.message}`),process.exit(1)}),console.error(`HTTP server started on port ${s}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${s}/extension/tasks`),t.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{l.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let m=Oe(u),h=new O;await m.connect(h),console.error(`Chrome extension MCP server started on stdio`);let g=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{l.clearAllTasks(`Server shutting down`),await h.close(),p.close(),await r.release({kill:!1}),await n.release(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>g(`SIGINT`)),process.on(`SIGTERM`,()=>g(`SIGTERM`))}catch(e){if(r||n)try{await r?.release(),await n?.release()}catch{}let t=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${t}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),Ae=`BROWSE_TOOL_CONFIG`,je=`.browse-tool`,Me=`config.json`,Ne=[`json`,`text`,`quiet`],Pe={commands:{},tools:{}},Fe=F.record(F.string(),F.unknown()),Ie=F.object({mcpServe:F.object({type:F.string().optional(),browser:F.string().optional(),headless:F.boolean().optional(),profile:F.string().optional(),mode:F.string().optional(),host:F.string().optional(),port:F.coerce.number().int().positive().optional(),tags:F.string().optional(),exclude:F.string().optional(),customTools:F.string().optional(),snippetsDir:F.string().optional(),registryPath:F.string().optional(),registryDir:F.string().optional(),pidsDir:F.string().optional(),profilesDir:F.string().optional()}).partial().optional(),httpServe:F.object({port:F.coerce.number().int().positive().optional(),headless:F.boolean().optional(),idleTimeout:F.coerce.number().positive().optional(),host:F.string().optional(),registryDir:F.string().optional(),registryPath:F.string().optional(),pidsDir:F.string().optional(),profilesDir:F.string().optional(),snippetsDir:F.string().optional()}).partial().optional(),exec:F.object({format:F.enum(Ne).optional(),color:F.boolean().optional(),port:F.coerce.number().int().positive().optional()}).partial().optional(),tools:F.object({format:F.enum(Ne).optional(),color:F.boolean().optional(),port:F.coerce.number().int().positive().optional()}).partial().optional(),status:F.record(F.string(),F.unknown()).optional(),stop:F.record(F.string(),F.unknown()).optional()}),Le=F.object({commands:Ie.default({}),tools:F.record(F.string(),Fe).default({})});let R={config:Pe};function Re(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 ze(e){let t=M.resolve(e);if(!j(t))throw Error(`Config path not found: ${t}`);return ie(t).isDirectory()?M.join(t,Me):t}function Be(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??ce(),a=Re(e);if(a)return ze(a);if(n[Ae])return ze(n[Ae]);let o=M.join(r,je,Me);if(j(o))return o;let s=M.join(i,je,Me);if(j(s))return s}function Ve(e){let t=re(e,`utf8`),n=JSON.parse(t);return Le.parse(n)}function He(e,t={}){let n=Be(e,t);return n?{configPath:n,config:Ve(n)}:{config:Pe}}function Ue(e,t={}){return R=He(e,t),R}function z(e){return(R.config.commands??{})[e]??{}}function We(e){return R.config.tools?.[e]??{}}function B(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}var Ge=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??d,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 V(e){return new Ge(e)}const Ke={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function H(e,t,n){return n?`${Ke[t]}${e}${Ke.reset}`:e}function qe(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Je(e):Ye(e,r)}function Je(e){return JSON.stringify(e,null,2)}function Ye(e,t){let n=[];e.isError&&n.push(H(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(Xe(r,t)):r.type===`image`?n.push($e(r,t)):n.push(H(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
3
|
-
`)}function
|
|
4
|
-
`)}function
|
|
5
|
-
`)}function tt(e,t){let n=z(`tools`),r=B(e,`format`,t.format,n.format),i=B(e,`color`,t.color,n.color);return{port:B(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const nt=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}=tt(this,t);try{let t=await V({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?et(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(U(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),rt=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}=tt(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(U(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await V({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=qe(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(U(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new C(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(nt).addCommand(rt);const it=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)}}),at=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=z(`exec`),i=B(this,`format`,n.format,r.format),a=B(this,`color`,n.color,r.color),o=B(this,`port`,n.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),s=this.getOptionValueSourceWithGlobals(`port`),c=s!==void 0&&s!==`default`&&s!==`implied`,l={format:i,color:a};try{let n;try{n=JSON.parse(t)}catch{console.error(U(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await V({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=qe(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(U(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),W=`pageId`,G=`browserId`,ot=F.record(F.string(),F.unknown()),st=F.object({type:F.literal(`object`),properties:F.record(F.string(),F.unknown()).optional(),required:F.array(F.string()).optional(),additionalProperties:F.boolean().optional()}).passthrough(),ct=F.object({name:F.string().min(1),description:F.string().min(1),script:F.string().min(1),suggestionActions:F.string().min(1).optional(),capabilities:ot,inputSchema:st}),lt=F.object({tools:F.array(ct)}),ut=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function dt(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function ft(e,t){if(ut){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function K(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function pt(e){return K(e)&&Array.isArray(e.content)}function q(e){let t=e.trim();return t===``?``:t.startsWith(`"`)&&t.endsWith(`"`)||t.startsWith(`[`)&&t.endsWith(`]`)||t.startsWith(`{`)&&t.endsWith(`}`)?JSON.parse(t):t.startsWith(`'`)&&t.endsWith(`'`)?t.slice(1,-1):t===`true`?!0:t===`false`?!1:t===`null`?null:/^-?\d+(\.\d+)?$/.test(t)?Number(t):t}function mt(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 ht(e){return e.replace(/^\uFEFF/,``).split(/\r?\n/).map(e=>{if(e.includes(` `))throw Error(`Tab indentation is not supported in tools.yaml`);let t=e.replace(/\s+#.*$/,``);return t.trim().length===0?null:{indent:t.match(/^ */)?.[0].length??0,text:t.trim()}}).filter(e=>e!==null)}function J(e,t,n){let r=e[t];if(!r||r.indent!==n)throw Error(`Invalid indentation in tools.yaml at line ${t+1}`);return r.text.startsWith(`-`)?gt(e,t,n):_t(e,t,n)}function gt(e,t,n){let r=[],i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||!t.text.startsWith(`-`))break;let a=t.text.slice(1).trim();if(a===``){let t=e[i+1];if(!t||t.indent<=n){r.push(null),i+=1;continue}let[a,o]=J(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=vt(a),s={};if(o===void 0){let r=e[i+1];if(!r||r.indent<=n)throw Error(`Expected nested value for "${t}" in tools.yaml`);let[a,o]=J(e,i+1,r.indent);s[t]=a,i=o}else s[t]=q(o),i+=1;for(;i<e.length&&e[i].indent>n;){let t=e[i];if(t.indent!==n+2||t.text.startsWith(`-`)){let[n,r]=J(e,i,t.indent);if(!K(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=vt(t.text);if(a===void 0){let n=e[i+1];if(!n||n.indent<=t.indent)throw Error(`Expected nested value for "${r}" in tools.yaml`);let[a,o]=J(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=q(a),i+=1}r.push(s);continue}r.push(q(a)),i+=1}return[r,i]}function _t(e,t,n){let r={},i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||t.text.startsWith(`-`))break;let[a,o]=vt(t.text);if(o===void 0){let t=e[i+1];if(!t||t.indent<=n){r[a]=null,i+=1;continue}let[o,s]=J(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=q(o),i+=1}return[r,i]}function vt(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 yt(e){let t=ht(e);if(t.length===0)return{};let[n]=J(t,0,t[0].indent);return n}function bt(e){let t=e.inputSchema.properties;if(!t||!K(t))throw Error(`Custom tool "${e.name}" must define inputSchema.properties`);let n=t[W],r=t[G],i=n!==void 0&&K(n)&&n.type===`string`,a=r!==void 0&&K(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 xt(e){let t=S(await le(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function St(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 Ct(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function wt(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(K(r)){let i=Ct(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 Tt(e,t,n){if(pt(t))return n?wt(t,n):t;if(typeof t==`string`)return n?{content:[{type:`text`,text:JSON.stringify({result:t,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:t}]};if(t===void 0)return n?{content:[{type:`text`,text:JSON.stringify({result:`Custom tool "${e}" completed successfully`,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:`Custom tool "${e}" completed successfully`}]};let r=K(t)&&n?Ct(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var Et=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[W]==`string`&&t[W].length>0?t[W]:void 0,r=typeof t[G]==`string`&&t[G].length>0?t[G]:void 0;if(!n&&!r)throw Error(`Custom tool "${e}" requires a string pageId or browserId`);if(n){let t=this.pageRegistry.get(n);if(!t)throw Error(`Page "${n}" not found`);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:mt({...r,...n?.attributes??{}}),exception:n?.exception})};return{getTraceContext:()=>this.telemetry.getActiveTraceContext(),trace:(e,t)=>i(`trace`,e,t),debug:(e,t)=>i(`debug`,e,t),info:(e,t)=>i(`info`,e,t),warn:(e,t)=>i(`warn`,e,t),error:(e,t)=>i(`error`,e,t),fatal:(e,t)=>i(`fatal`,e,t)}}async listTools(e){return(await this.loadTools(e)).map(({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i})=>({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i}))}async executeTool(e,t,n){let r=typeof n[W]==`string`?n[W]:void 0,i=typeof n[G]==`string`?n[G]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":M.resolve(e),"browse_tool.page.id":r,"browse_tool.browser.id":i}},async r=>{let i=(await this.loadTools(e)).find(e=>e.name===t);if(!i)throw r?.setStatus({code:P.ERROR,message:`Custom tool "${t}" not found`}),Error(`Custom tool "${t}" not found`);let{pageId:a,pageEntry:o,page:s,browser:c}=await this.resolveExecutionContext(t,n),l=this.createToolLogger(t,a,o);r?.setAttributes({"browse_tool.browser.id":o.browserId,"browse_tool.execution.mode":o.mode,"browse_tool.page.id":a}),ft(`Executing custom tool`,{toolName:t,directory:M.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:dt(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 ft(`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})}ft(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:dt(u)});let d=Tt(t,u,i.suggestionActions),f=d.isError?d.content[0]?.text:void 0;return f&&r?.setStatus({code:P.ERROR,message:f}),d})}async loadTools(e){let t=M.resolve(e),n=M.join(t,`tools.yaml`),r=yt(await le(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=lt.parse(r);return Promise.all(i.tools.map(async e=>{bt(e);let n=M.resolve(t,e.script);await ue(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=St(n,await xt(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function Dt(t){let n=new N;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 Ot(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 kt(e){let t=Math.floor(e/1e3),n=Math.floor(t/60),r=Math.floor(n/60);return r>0?`${r}h ${n%60}m`:n>0?`${n}m ${t%60}s`:`${t}s`}function At(e){return kt(new Date().getTime()-e.getTime())}const jt=`table-container`,Mt=`stat-card`,Nt=`browser-row`,Pt=`page-row`,Y=`status-active`,Ft=`url-cell`,X=`timestamp-cell`,It=`actions-cell`,Lt=`kill-btn`,Rt=`empty-state`;function zt({browsers:e}){return e.length===0?I(`div`,{class:jt,children:L(`div`,{class:Rt,children:[I(`h3`,{children:`No Active Browsers`}),I(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):I(`div`,{class:jt,children:L(`table`,{class:`browser-table`,children:[I(`thead`,{children:L(`tr`,{children:[I(`th`,{children:`ID`}),I(`th`,{children:`Status`}),I(`th`,{children:`URL / Title`}),I(`th`,{children:`Created`}),I(`th`,{children:`Age`}),I(`th`,{children:`Actions`})]})}),I(`tbody`,{id:`browser-table-body`,children:e.map(e=>L(pe,{children:[L(`tr`,{class:Nt,children:[I(`td`,{children:e.id}),I(`td`,{children:L(`span`,{class:Y,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),I(`td`,{children:e.profileName||`Default Profile`}),I(`td`,{class:X,children:Ot(e.createdAt)}),I(`td`,{class:X,children:At(e.createdAt)}),I(`td`,{class:It,children:I(`button`,{class:Lt,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>L(`tr`,{class:Pt,children:[L(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),I(`td`,{children:I(`span`,{class:Y,children:`Open`})}),I(`td`,{class:Ft,title:t.url,children:t.title||t.url||`about:blank`}),I(`td`,{class:X,children:Ot(t.createdAt)}),I(`td`,{class:X,children:At(t.createdAt)}),I(`td`,{class:It,children:I(`button`,{class:Lt,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]}))]}))})]})})}function Bt({stats:e}){return L(`div`,{class:`stats-container`,children:[L(`div`,{class:Mt,children:[I(`h3`,{children:`Active Browsers`}),I(`div`,{class:`value`,children:e.totalBrowsers})]}),L(`div`,{class:Mt,children:[I(`h3`,{children:`Active Pages`}),I(`div`,{class:`value`,children:e.totalPages})]})]})}function Vt({browsers:e,stats:t}){return L(`div`,{class:`dashboard-container`,children:[L(`div`,{class:`dashboard-header`,children:[L(`div`,{children:[I(`h1`,{children:`Playwright MCP Dashboard`}),I(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),L(`div`,{children:[I(`button`,{id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),I(`button`,{id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),I(Bt,{stats:t}),I(zt,{browsers:e}),I(`script`,{children:fe(`
|
|
2
|
+
import{S as e,_ as t,a as n,b as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,s as h,t as g,u as _,v,x as y,y as b}from"./streamable-http-a245cRfE.mjs";import"./playwright-test-lZlw-zhY.mjs";import{stripTypeScriptTypes as x}from"node:module";import"reflect-metadata/lite";import{Command as S,Option as C}from"commander";import{serve as w}from"@hono/node-server";import{createProcessLease as T,resolveSiblingRegistryPath as ee}from"@agimon-ai/foundation-process-registry";import{DEFAULT_PORT_RANGE as E,PortRegistryService as D}from"@agimon-ai/foundation-port-registry";import{Server as O}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as k}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as A,ListToolsRequestSchema as j}from"@modelcontextprotocol/sdk/types.js";import{existsSync as M,readFileSync as te,statSync as ne}from"node:fs";import N from"node:path";import{Hono as P}from"hono";import{cors as re}from"hono/cors";import{Container as ie,ContainerModule as ae}from"inversify";import{SpanStatusCode as F}from"@opentelemetry/api";import{homedir as oe}from"node:os";import{z as I}from"zod";import{readFile as se,stat as ce}from"node:fs/promises";import{createNodeWebSocket as le}from"@hono/node-ws";import{raw as ue}from"hono/html";import{Fragment as de,jsx as L,jsxs as R}from"hono/jsx/jsx-runtime";var fe=`0.3.1`;const pe=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function me(e,t){if(!pe)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function he(t){let n=new P,i;try{i=t.get(e.TelemetryService)}catch{i=new r}return n.get(`/telemetry-config`,e=>{try{let t=new URL(e.req.url).origin;return e.json(y(process.env,t))}catch(t){return e.json({enabled:!1,error:t instanceof Error?t.message:String(t)},500)}}),n.get(`/tasks`,n=>{try{let r=t.get(e.ExtensionTaskQueue).getNextTask();return r?n.json({task:{id:r.id,tool:r.tool,arguments:r.arguments,telemetry:r.telemetry}}):n.json({})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/result`,async n=>{try{let r=await n.req.json();if(!r.taskId)return n.json({success:!1,error:`Missing taskId in request body`},400);let i=t.get(e.ExtensionTaskQueue),a=t.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),n.json({success:!0})):n.json({success:!1,error:`Task ${r.taskId} not found or already completed`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/status`,n=>{try{let r=t.get(e.ExtensionTaskQueue),i=r.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:r.queueSize};return n.json(a)}catch(e){return n.json({connected:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/register`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.ExtensionSessionRegistry).register(r);return n.json({success:!0,session:{id:i.id,browserId:i.browserId,controlMode:i.controlMode,createdAt:i.createdAt.toISOString()}})}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/heartbeat`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.ExtensionSessionRegistry).heartbeat(r);return i?n.json({success:!0,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested,lastHeartbeatAt:i.lastHeartbeatAt.toISOString()}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/tab-mapped`,async n=>{try{let r=await n.req.json();if(!r.pageId||typeof r.tabId!=`number`)return n.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=t.get(e.PageRegistry),a=t.get(e.BrowserService),o=i.get(r.pageId);return o?(o.extensionTabId=r.tabId,a.recordBrowserActivity(o.browserId,r.pageId),n.json({success:!0})):n.json({success:!1,error:`Page ${r.pageId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return me(`artifact received`,{browserId:r.browserId,videoBase64Size:r.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(r.browserId),n.json({success:!0})):n.json({success:!1,error:`Browser "${r.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording/chunk`,async n=>{try{let r=await n.req.json();if(!r.browserId||!r.chunkBase64)return n.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=t.get(e.BrowserService),o=await a.persistExtensionRecordingChunk(r.browserId,r.chunkBase64);return me(`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),n.json({success:!0})):n.json({success:!1,error:`Browser "${r.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/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:{}}}),me(`browser log relayed`,{level:t.level??`info`,message:t.message,attributes:t.attributes}),e.json({success:!0}))}catch(t){return e.json({success:!1,error:t instanceof Error?t.message:String(t)},500)}}),n.post(`/handoff`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.ExtensionSessionRegistry).requestHandoff(r);return i?n.json({success:!0,message:`Handoff requested. AI will take control when ready.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/handoff/acknowledge`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.ExtensionSessionRegistry).acknowledgeHandoff(r.sessionId);return i?n.json({success:!0,message:`Handoff acknowledged. AI now has control.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found or no handoff pending`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/sessions`,n=>{try{let r=t.get(e.ExtensionSessionRegistry).listSessions();return n.json({sessions:r.map(e=>({id:e.id,browserId:e.browserId,tabId:e.tabId,currentUrl:e.currentUrl,controlMode:e.controlMode,activeSpecPath:e.activeSpecPath,handoffRequested:e.handoffRequested,createdAt:e.createdAt.toISOString(),lastHeartbeatAt:e.lastHeartbeatAt.toISOString()}))})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n}function ge(e){if(e==null||e===``)return;let t=typeof e==`number`?e:typeof e==`string`?Number.parseInt(e,10):NaN;if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}const _e=[`pnpm-workspace.yaml`,`nx.json`,`.git`],ve=`browse-tool-chrome`,ye=`tool`,be=process.env.NODE_ENV||`development`,xe=`127.0.0.1`;function Se(e=process.cwd()){let t=N.resolve(e);for(;;){for(let e of _e)if(M(N.join(t,e)))return t;let e=N.dirname(t);if(e===t)return process.cwd();t=e}}function Ce(){return new D(process.env.PORT_REGISTRY_PATH)}async function we(e,t,n){let r=Ce(),i=await r.reservePort({repositoryPath:e,serviceName:ve,serviceType:ye,environment:be,preferredPort:t,portRange:n,pid:process.pid,host:xe,force:!0,metadata:{transport:`stdio`,mode:`chrome-serve`}});if(!i.success||!i.record)throw Error(i.error||`Failed to reserve port ${t}`);let a=!1;return{port:i.record.port,release:async()=>{if(a)return;a=!0;let n=await r.releasePort({repositoryPath:e,serviceName:ve,serviceType:ye,environment:be,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function Te(){return new ae(t=>{t.bind(e.ExtensionTaskQueue).to(b).inSingletonScope(),t.bind(e.ExtensionToolDelegator).to(v).inSingletonScope()})}function Ee(e){let t=new O({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(j,async()=>({tools:n})),t.setRequestHandler(A,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const De=new S(`chrome-serve`).description(`[DEPRECATED] Start MCP server with Chrome extension HTTP polling for bot-detection-free browser automation`).option(`-p, --port <port>`,`HTTP server port for extension polling`).option(`-v, --verbose`,`Enable verbose output`,!1).option(`--wait-for-extension`,`Wait for extension to connect before accepting MCP requests`,!1).action(async 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(``);let n,r;try{let i=ge(t.port),a=Se(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=ee(o,`processes.json`)),n=await we(a,i??E.min,i?{min:i,max:i}:E);let s=n.port;r=await T({repositoryPath:a,serviceName:ve,serviceType:ye,environment:be,port:s,host:xe,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:t.waitForExtension}}),t.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${s}`),console.error(` Wait for extension: ${t.waitForExtension}`));let c=new ie({defaultScope:`Singleton`});c.load(Te());let l=c.get(e.ExtensionTaskQueue),u=c.get(e.ExtensionToolDelegator),d=new P;d.use(`*`,re({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=he(c);d.route(`/extension`,f),d.get(`/health`,e=>{let t=l.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let p=w({fetch:d.fetch,port:s});p.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${s} is already in use.`),console.error(`Recovery: Try a different port with --port <port>`)):console.error(`Error [SERVER_ERROR]: HTTP server error: ${e.message}`),process.exit(1)}),console.error(`HTTP server started on port ${s}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${s}/extension/tasks`),t.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{l.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let m=Ee(u),h=new k;await m.connect(h),console.error(`Chrome extension MCP server started on stdio`);let g=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{l.clearAllTasks(`Server shutting down`),await h.close(),p.close(),await r.release({kill:!1}),await n.release(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>g(`SIGINT`)),process.on(`SIGTERM`,()=>g(`SIGTERM`))}catch(e){if(r||n)try{await r?.release(),await n?.release()}catch{}let t=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${t}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),Oe=`BROWSE_TOOL_CONFIG`,ke=`.browse-tool`,Ae=`config.json`,je=[`json`,`text`,`quiet`],Me={commands:{},tools:{}},Ne=I.record(I.string(),I.unknown()),Pe=I.object({mcpServe:I.object({type:I.string().optional(),browser:I.string().optional(),headless:I.boolean().optional(),profile:I.string().optional(),mode:I.string().optional(),host:I.string().optional(),port:I.coerce.number().int().positive().optional(),httpPort:I.coerce.number().int().positive().optional(),tags:I.string().optional(),exclude:I.string().optional(),customTools:I.string().optional(),snippetsDir:I.string().optional(),registryPath:I.string().optional(),registryDir:I.string().optional(),pidsDir:I.string().optional(),profilesDir:I.string().optional()}).partial().optional(),httpServe:I.object({port:I.coerce.number().int().positive().optional(),headless:I.boolean().optional(),idleTimeout:I.coerce.number().positive().optional(),host:I.string().optional(),registryDir:I.string().optional(),registryPath:I.string().optional(),pidsDir:I.string().optional(),profilesDir:I.string().optional(),snippetsDir:I.string().optional()}).partial().optional(),exec:I.object({format:I.enum(je).optional(),color:I.boolean().optional(),port:I.coerce.number().int().positive().optional()}).partial().optional(),tools:I.object({format:I.enum(je).optional(),color:I.boolean().optional(),port:I.coerce.number().int().positive().optional()}).partial().optional(),status:I.record(I.string(),I.unknown()).optional(),stop:I.record(I.string(),I.unknown()).optional()}),Fe=I.object({commands:Pe.default({}),tools:I.record(I.string(),Ne).default({})});let z={config:Me};function Ie(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 Le(e){let t=N.resolve(e);if(!M(t))throw Error(`Config path not found: ${t}`);return ne(t).isDirectory()?N.join(t,Ae):t}function Re(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??oe(),a=Ie(e);if(a)return Le(a);if(n[Oe])return Le(n[Oe]);let o=N.join(r,ke,Ae);if(M(o))return o;let s=N.join(i,ke,Ae);if(M(s))return s}function ze(e){let t=te(e,`utf8`),n=JSON.parse(t);return Fe.parse(n)}function Be(e,t={}){let n=Re(e,t);return n?{configPath:n,config:ze(n)}:{config:Me}}function Ve(e,t={}){return z=Be(e,t),z}function B(e){return(z.config.commands??{})[e]??{}}function He(e){return z.config.tools?.[e]??{}}function V(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}var Ue=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??u,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let t=await p().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 H(e){return new Ue(e)}const We={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function U(e,t,n){return n?`${We[t]}${e}${We.reset}`:e}function Ge(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Ke(e):qe(e,r)}function Ke(e){return JSON.stringify(e,null,2)}function qe(e,t){let n=[];e.isError&&n.push(U(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(Je(r,t)):r.type===`image`?n.push(Ze(r,t)):n.push(U(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
3
|
+
`)}function Je(e,t){let n=e.text;try{return Ye(JSON.parse(n),t)}catch{return n}}function Ye(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=U(r,`cyan`,t),a=Xe(i,t);n.push(`${e}: ${a}`)}return n.join(`
|
|
4
|
+
`)}function Xe(e,t){return e===null?U(`null`,`gray`,t):e===void 0?U(`undefined`,`gray`,t):typeof e==`boolean`?U(String(e),e?`green`:`red`,t):typeof e==`number`?U(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?U(e,`blue`,t):e:Array.isArray(e)?e.length===0?U(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function Ze(e,t){let{mimeType:n,data:r}=e;return U(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function W(e,t){return U(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function Qe(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=U(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
|
|
5
|
+
`)}function $e(e,t){let n=B(`tools`),r=V(e,`format`,t.format,n.format),i=V(e,`color`,t.color,n.color);return{port:V(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const et=new S(`list-custom-tools`).description(`List custom tools from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(u)).action(async function(e,t){let{port:n,formatterOptions:r}=$e(this,t);try{let t=await H({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?Qe(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(W(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),tt=new S(`exec-custom-tool`).description(`Execute a custom tool from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).argument(`<tool>`,`Custom tool name to execute`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(u)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=$e(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(W(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await H({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=Ge(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(W(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new S(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(et).addCommand(tt);const nt=new S(`docker-build-cft`).description(`Build the Chrome for Testing Docker image used by vm mode`).option(`--cft-version <version>`,`Chrome for Testing version to build`,t).option(`--image <image>`,`Docker image tag to produce`,c).option(`--platform <platform>`,`Docker target platform`,s).action(async e=>{try{let t=await m({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: ${d(t.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),rt=new S(`exec`).description(`Execute a tool directly with JSON arguments`).argument(`<tool>`,`Tool name to execute (e.g., browser_launch)`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(u)).action(async function(e,t,n){let r=B(`exec`),i=V(this,`format`,n.format,r.format),a=V(this,`color`,n.color,r.color),o=V(this,`port`,n.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),s=this.getOptionValueSourceWithGlobals(`port`),c=s!==void 0&&s!==`default`&&s!==`implied`,l={format:i,color:a};try{let n;try{n=JSON.parse(t)}catch{console.error(W(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await H({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=Ge(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(W(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),G=`pageId`,K=`browserId`,it=I.record(I.string(),I.unknown()),at=I.object({type:I.literal(`object`),properties:I.record(I.string(),I.unknown()).optional(),required:I.array(I.string()).optional(),additionalProperties:I.boolean().optional()}).passthrough(),ot=I.object({name:I.string().min(1),description:I.string().min(1),script:I.string().min(1),suggestionActions:I.string().min(1).optional(),capabilities:it,inputSchema:at}),st=I.object({tools:I.array(ot)}),ct=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function lt(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function ut(e,t){if(ct){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function q(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function dt(e){return q(e)&&Array.isArray(e.content)}function J(e){let t=e.trim();return t===``?``:t.startsWith(`"`)&&t.endsWith(`"`)||t.startsWith(`[`)&&t.endsWith(`]`)||t.startsWith(`{`)&&t.endsWith(`}`)?JSON.parse(t):t.startsWith(`'`)&&t.endsWith(`'`)?t.slice(1,-1):t===`true`?!0:t===`false`?!1:t===`null`?null:/^-?\d+(\.\d+)?$/.test(t)?Number(t):t}function ft(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 pt(e){return e.replace(/^\uFEFF/,``).split(/\r?\n/).map(e=>{if(e.includes(` `))throw Error(`Tab indentation is not supported in tools.yaml`);let t=e.replace(/\s+#.*$/,``);return t.trim().length===0?null:{indent:t.match(/^ */)?.[0].length??0,text:t.trim()}}).filter(e=>e!==null)}function Y(e,t,n){let r=e[t];if(!r||r.indent!==n)throw Error(`Invalid indentation in tools.yaml at line ${t+1}`);return r.text.startsWith(`-`)?mt(e,t,n):ht(e,t,n)}function mt(e,t,n){let r=[],i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||!t.text.startsWith(`-`))break;let a=t.text.slice(1).trim();if(a===``){let t=e[i+1];if(!t||t.indent<=n){r.push(null),i+=1;continue}let[a,o]=Y(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=gt(a),s={};if(o===void 0){let r=e[i+1];if(!r||r.indent<=n)throw Error(`Expected nested value for "${t}" in tools.yaml`);let[a,o]=Y(e,i+1,r.indent);s[t]=a,i=o}else s[t]=J(o),i+=1;for(;i<e.length&&e[i].indent>n;){let t=e[i];if(t.indent!==n+2||t.text.startsWith(`-`)){let[n,r]=Y(e,i,t.indent);if(!q(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=gt(t.text);if(a===void 0){let n=e[i+1];if(!n||n.indent<=t.indent)throw Error(`Expected nested value for "${r}" in tools.yaml`);let[a,o]=Y(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=J(a),i+=1}r.push(s);continue}r.push(J(a)),i+=1}return[r,i]}function ht(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]=gt(t.text);if(o===void 0){let t=e[i+1];if(!t||t.indent<=n){r[a]=null,i+=1;continue}let[o,s]=Y(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=J(o),i+=1}return[r,i]}function gt(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 _t(e){let t=pt(e);if(t.length===0)return{};let[n]=Y(t,0,t[0].indent);return n}function vt(e){let t=e.inputSchema.properties;if(!t||!q(t))throw Error(`Custom tool "${e.name}" must define inputSchema.properties`);let n=t[G],r=t[K],i=n!==void 0&&q(n)&&n.type===`string`,a=r!==void 0&&q(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 yt(e){let t=x(await se(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function bt(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 xt(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function St(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(q(r)){let i=xt(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 Ct(e,t,n){if(dt(t))return n?St(t,n):t;if(typeof t==`string`)return n?{content:[{type:`text`,text:JSON.stringify({result:t,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:t}]};if(t===void 0)return n?{content:[{type:`text`,text:JSON.stringify({result:`Custom tool "${e}" completed successfully`,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:`Custom tool "${e}" completed successfully`}]};let r=q(t)&&n?xt(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var wt=class{constructor(e,t,n=new r,i){this.pageRegistry=e,this.extensionTaskQueue=t,this.telemetry=n,this.browserService=i}resolveToolPage(e,t,n){if(n.page)return n.page;if(n.mode===`extension`&&this.extensionTaskQueue){let e=new o(this.extensionTaskQueue);return e.setTarget(t,n.browserId),e}throw Error(`Custom tool "${e}" requires a supported page context`)}toPageSummary(e,t){return{pageId:t.id,url:t.url,title:t.title,active:e===t.id}}async createPageForBrowser(e,t,n){if(!this.browserService)throw Error(`Custom tool "${e}" requires browser service support to create a page`);let r=this.browserService.getBrowser(t);if(!r)throw Error(`Browser "${t}" not found`);let i=n?.setAsCurrent!==!1;if(r.mode===`extension`||r.mode===`vm`){if(!this.extensionTaskQueue)throw Error(`Custom tool "${e}" requires extension task support to create a page`);let a=this.pageRegistry.registerExtensionPage(t,void 0,n?.url,!1);try{let o=await this.extensionTaskQueue.queueTask(`browser_new_page`,{browserId:t,pageId:a,url:n?.url,setAsCurrent:i},1e4,t);if(!o.success)throw Error(o.error??`Custom tool "${e}" failed to create a page`);let s=o.result?.content[0]?.type===`text`?o.result.content[0].text:void 0,c={};if(typeof s==`string`&&s.length>0)try{c=JSON.parse(s)}catch{c={}}let l=this.pageRegistry.get(a);if(!l)throw Error(`Page "${a}" was not registered`);l.url=c.url??l.url,l.title=c.title??l.title,l.extensionTabId=c.tabId??l.extensionTabId;let u=await this.waitForResolvedPageMetadata(a);return r.pageIds.add(a),(i||!r.currentPageId)&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a),{...this.toPageSummary(r.currentPageId,u),page:this.resolveToolPage(e,a,u)}}catch(e){throw this.pageRegistry.remove(a),e}}let{pageId:a,page:o}=await this.browserService.newPage(t);n?.url&&(await o.goto(n.url),await this.pageRegistry.updateMetadata(a)),i&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a);let s=this.pageRegistry.get(a);if(!s)throw Error(`Page "${a}" was not registered`);return{...this.toPageSummary(r.currentPageId,s),page:this.resolveToolPage(e,a,s)}}async waitForResolvedPageMetadata(e){let t=Date.now(),n=this.pageRegistry.get(e);for(;n&&Date.now()-t<1500;){let t=typeof n.url==`string`&&n.url.length>0,r=typeof n.title==`string`&&n.title.length>0&&n.title!==`Extension Tab`;if(t&&r)return n;await new Promise(e=>setTimeout(e,50)),n=this.pageRegistry.get(e)}if(!n)throw Error(`Page "${e}" was not registered`);return n}async resolveExecutionContext(e,t){let n=typeof t[G]==`string`&&t[G].length>0?t[G]:void 0,r=typeof t[K]==`string`&&t[K].length>0?t[K]:void 0;if(!n&&!r)throw Error(`Custom tool "${e}" requires a string pageId or browserId`);if(n){let t=this.pageRegistry.get(n);if(!t)throw Error(`Page "${n}" not found`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let i=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:i}}let i=this.createBrowserHelper(e,r);try{let e=await i.getCurrentPage(),t=this.pageRegistry.get(e.pageId);if(!t)throw Error(`Page "${e.pageId}" not found`);return{pageId:e.pageId,pageEntry:t,page:e.page,browser:i}}catch(e){if(!(e instanceof Error?e.message:String(e)).includes(`has no pages`))throw e;let t=await i.newPage(),n=this.pageRegistry.get(t.pageId);if(!n)throw Error(`Page "${t.pageId}" not found`,{cause:e});return{pageId:t.pageId,pageEntry:n,page:t.page,browser:i}}}createBrowserHelper(e,t){return{browserId:t,mode:this.browserService?.getBrowser(t)?.mode??`extension`,listPages:async()=>{let e=this.browserService?.getBrowser(t);if(!e)throw Error(`Browser "${t}" not found`);return this.pageRegistry.findByBrowser(t).map(t=>this.toPageSummary(e.currentPageId,t))},getPage:async n=>{let r=this.pageRegistry.get(n);if(!r||r.browserId!==t)throw Error(`Page "${n}" not found in browser "${t}"`);let i=this.browserService?.getBrowser(t);return{...this.toPageSummary(i?.currentPageId??null,r),page:this.resolveToolPage(e,n,r)}},getCurrentPage:async()=>{let n=this.browserService?.getBrowser(t);if(!n)throw Error(`Browser "${t}" not found`);let r=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!r)throw Error(`Browser "${t}" has no pages`);let i=this.pageRegistry.get(r);if(!i)throw Error(`Page "${r}" not found`);return{...this.toPageSummary(n.currentPageId,i),page:this.resolveToolPage(e,r,i)}},newPage:async n=>this.createPageForBrowser(e,t,n)}}createToolLogger(e,t,n){let r={"browse_tool.tool.name":e,"browse_tool.page.id":t,"browse_tool.browser.id":n.browserId,"browse_tool.execution.mode":n.mode},i=(e,t,n)=>{this.telemetry.log(e,t,{attributes:ft({...r,...n?.attributes??{}}),exception:n?.exception})};return{getTraceContext:()=>this.telemetry.getActiveTraceContext(),trace:(e,t)=>i(`trace`,e,t),debug:(e,t)=>i(`debug`,e,t),info:(e,t)=>i(`info`,e,t),warn:(e,t)=>i(`warn`,e,t),error:(e,t)=>i(`error`,e,t),fatal:(e,t)=>i(`fatal`,e,t)}}async listTools(e){return(await this.loadTools(e)).map(({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i})=>({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i}))}async executeTool(e,t,n){let r=typeof n[G]==`string`?n[G]:void 0,i=typeof n[K]==`string`?n[K]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":N.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:F.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}),ut(`Executing custom tool`,{toolName:t,directory:N.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:lt(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 ut(`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})}ut(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:lt(u)});let d=Ct(t,u,i.suggestionActions),f=d.isError?d.content[0]?.text:void 0;return f&&r?.setStatus({code:F.ERROR,message:f}),d})}async loadTools(e){let t=N.resolve(e),n=N.join(t,`tools.yaml`),r=_t(await se(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=st.parse(r);return Promise.all(i.tools.map(async e=>{vt(e);let n=N.resolve(t,e.script);await ce(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=bt(n,await yt(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function Tt(t){let n=new P;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 Et(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 Dt(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 Ot(e){return Dt(new Date().getTime()-e.getTime())}const kt=`table-container`,At=`stat-card`,jt=`browser-row`,Mt=`page-row`,X=`status-active`,Nt=`url-cell`,Z=`timestamp-cell`,Pt=`actions-cell`,Ft=`kill-btn`,It=`empty-state`;function Lt({browsers:e}){return e.length===0?L(`div`,{class:kt,children:R(`div`,{class:It,children:[L(`h3`,{children:`No Active Browsers`}),L(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):L(`div`,{class:kt,children:R(`table`,{class:`browser-table`,children:[L(`thead`,{children:R(`tr`,{children:[L(`th`,{children:`ID`}),L(`th`,{children:`Status`}),L(`th`,{children:`URL / Title`}),L(`th`,{children:`Created`}),L(`th`,{children:`Age`}),L(`th`,{children:`Actions`})]})}),L(`tbody`,{id:`browser-table-body`,children:e.map(e=>R(de,{children:[R(`tr`,{class:jt,children:[L(`td`,{children:e.id}),L(`td`,{children:R(`span`,{class:X,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),L(`td`,{children:e.profileName||`Default Profile`}),L(`td`,{class:Z,children:Et(e.createdAt)}),L(`td`,{class:Z,children:Ot(e.createdAt)}),L(`td`,{class:Pt,children:L(`button`,{class:Ft,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>R(`tr`,{class:Mt,children:[R(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),L(`td`,{children:L(`span`,{class:X,children:`Open`})}),L(`td`,{class:Nt,title:t.url,children:t.title||t.url||`about:blank`}),L(`td`,{class:Z,children:Et(t.createdAt)}),L(`td`,{class:Z,children:Ot(t.createdAt)}),L(`td`,{class:Pt,children:L(`button`,{class:Ft,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]}))]}))})]})})}function Rt({stats:e}){return R(`div`,{class:`stats-container`,children:[R(`div`,{class:At,children:[L(`h3`,{children:`Active Browsers`}),L(`div`,{class:`value`,children:e.totalBrowsers})]}),R(`div`,{class:At,children:[L(`h3`,{children:`Active Pages`}),L(`div`,{class:`value`,children:e.totalPages})]})]})}function zt({browsers:e,stats:t}){return R(`div`,{class:`dashboard-container`,children:[R(`div`,{class:`dashboard-header`,children:[R(`div`,{children:[L(`h1`,{children:`Playwright MCP Dashboard`}),L(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),R(`div`,{children:[L(`button`,{id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),L(`button`,{id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),L(Rt,{stats:t}),L(Lt,{browsers:e}),L(`script`,{children:ue(`
|
|
6
6
|
class DashboardManager {
|
|
7
7
|
constructor() {
|
|
8
8
|
this.autoRefreshInterval = null;
|
|
@@ -40,7 +40,7 @@ class DashboardManager {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
updateStats(stats) {
|
|
43
|
-
const cards = document.querySelectorAll('.${
|
|
43
|
+
const cards = document.querySelectorAll('.${At} .value');
|
|
44
44
|
if (cards.length >= 2) {
|
|
45
45
|
cards[0].textContent = stats.totalBrowsers;
|
|
46
46
|
cards[1].textContent = stats.totalPages;
|
|
@@ -52,10 +52,10 @@ class DashboardManager {
|
|
|
52
52
|
if (!tbody) return;
|
|
53
53
|
|
|
54
54
|
if (browsers.length === 0) {
|
|
55
|
-
const container = tbody.closest('.${
|
|
55
|
+
const container = tbody.closest('.${kt}');
|
|
56
56
|
if (container) {
|
|
57
57
|
container.innerHTML = \`
|
|
58
|
-
<div class="${
|
|
58
|
+
<div class="${It}">
|
|
59
59
|
<h3>No Active Browsers</h3>
|
|
60
60
|
<p>Launch a browser using MCP tools to see it here.</p>
|
|
61
61
|
</div>
|
|
@@ -69,29 +69,29 @@ class DashboardManager {
|
|
|
69
69
|
browsers.forEach(browser => {
|
|
70
70
|
// Browser row
|
|
71
71
|
const browserRow = document.createElement('tr');
|
|
72
|
-
browserRow.className = '${
|
|
72
|
+
browserRow.className = '${jt}';
|
|
73
73
|
browserRow.innerHTML = \`
|
|
74
74
|
<td>\${browser.id}</td>
|
|
75
|
-
<td><span class="${
|
|
75
|
+
<td><span class="${X}">\${browser.pages.length} page\${browser.pages.length !== 1 ? 's' : ''}</span></td>
|
|
76
76
|
<td>\${browser.profileName || 'Default Profile'}</td>
|
|
77
|
-
<td class="${
|
|
78
|
-
<td class="${
|
|
79
|
-
<td class="${
|
|
77
|
+
<td class="${Z}">\${this.formatTimestamp(browser.createdAt)}</td>
|
|
78
|
+
<td class="${Z}">\${this.formatAge(browser.createdAt)}</td>
|
|
79
|
+
<td class="${Pt}"><button class="${Ft}" onclick="dashboard.killBrowser('\${browser.id}')">Kill</button></td>
|
|
80
80
|
\`;
|
|
81
81
|
tbody.appendChild(browserRow);
|
|
82
82
|
|
|
83
83
|
// Page rows
|
|
84
84
|
browser.pages.forEach(page => {
|
|
85
85
|
const pageRow = document.createElement('tr');
|
|
86
|
-
pageRow.className = '${
|
|
86
|
+
pageRow.className = '${Mt}';
|
|
87
87
|
const isActive = browser.currentPageId === page.id;
|
|
88
88
|
pageRow.innerHTML = \`
|
|
89
89
|
<td>\${page.id}\${isActive ? ' (active)' : ''}</td>
|
|
90
|
-
<td><span class="${
|
|
91
|
-
<td class="${
|
|
92
|
-
<td class="${
|
|
93
|
-
<td class="${
|
|
94
|
-
<td class="${
|
|
90
|
+
<td><span class="${X}">Open</span></td>
|
|
91
|
+
<td class="${Nt}" title="\${this.escapeHtml(page.url)}">\${this.escapeHtml(page.title || page.url || 'about:blank')}</td>
|
|
92
|
+
<td class="${Z}">\${this.formatTimestamp(page.createdAt)}</td>
|
|
93
|
+
<td class="${Z}">\${this.formatAge(page.createdAt)}</td>
|
|
94
|
+
<td class="${Pt}"><button class="${Ft}" onclick="dashboard.killPage('\${page.id}')">Close</button></td>
|
|
95
95
|
\`;
|
|
96
96
|
tbody.appendChild(pageRow);
|
|
97
97
|
});
|
|
@@ -187,7 +187,7 @@ const dashboard = new DashboardManager();
|
|
|
187
187
|
window.addEventListener('beforeunload', () => {
|
|
188
188
|
dashboard.stopAutoRefresh();
|
|
189
189
|
});
|
|
190
|
-
`)})]})}function
|
|
190
|
+
`)})]})}function Bt({title:e,children:t}){return R(`html`,{lang:`en`,children:[R(`head`,{children:[L(`meta`,{charset:`UTF-8`}),L(`meta`,{name:`viewport`,content:`width=device-width, initial-scale=1.0`}),L(`title`,{children:e}),L(`style`,{children:ue(`
|
|
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
|
-
`)})]}),I(`body`,{children:t})]})}function Ut(t){let n=new N;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(I(Ht,{title:`Playwright MCP Dashboard`,children:I(Vt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html(I(Ht,{title:`Playwright MCP Dashboard - Error`,children:L(`div`,{style:`padding: 2rem; text-align: center;`,children:[I(`h1`,{children:`Failed to load dashboard`}),I(`p`,{children:e instanceof Error?e.message:String(e)}),I(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function Wt(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 Gt(t){try{return t.get(e.TelemetryService)}catch{return new b}}function Kt(t){let n=new N,r=t.get(e.PageRegistry),i=t.get(e.BrowserService),a=t.get(e.ExtensionTaskQueue),o=Gt(t),s=new Et(r,a,o,i);n.use(`*`,ae({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:P.ERROR,message:`Tool "${i.tool}" not found`}),o.log(`warn`,`browse-tool HTTP execute rejected unknown tool`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool}}),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=Wt(c);return d?(a?.setStatus({code:P.ERROR,message:d}),o.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":u}})):o.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":u}}),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=Wt(u);return d?(l?.setStatus({code:P.ERROR,message:d}),o.log(`error`,`browse-tool custom tool execution failed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}})):o.log(`info`,`browse-tool custom tool execution succeeded`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}}),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=_e(t);n.route(`/extension`,c);let l=Dt(t);n.route(`/api`,l);let u=Ut(t);return n.route(`/`,u),n}function qt(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 Jt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function Yt(e=process.cwd()){let t=M.resolve(e);for(;;){for(let e of Jt)if(j(M.join(t,e)))return t;let e=M.dirname(t);if(e===t)return process.cwd();t=e}}const Xt=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=z(`httpServe`),i={port:B(this,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),headless:B(this,`headless`,t.headless,n.headless),idleTimeout:B(this,`idleTimeout`,t.idleTimeout,n.idleTimeout===void 0?void 0:String(n.idleTimeout),process.env[a]),host:B(this,`host`,t.host,n.host,process.env.PLAYWRIGHT_HOST),registryDir:B(this,`registryDir`,t.registryDir,n.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:B(this,`registryPath`,t.registryPath,n.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:B(this,`pidsDir`,t.pidsDir,n.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:B(this,`profilesDir`,t.profilesDir,n.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:B(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=Yt(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=te(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=M.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=Kt(u),{injectWebSocket:p,upgradeWebSocket:m}=de({app:f});qt(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 ne(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,E=ee({fetch:f.fetch,port:w,hostname:i.host});p(E),o=await T({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`}},d);let D=v(i.host,w);console.log(`HTTP server listening on ${D}`),console.log(` Health check: ${D}/health`),console.log(` Execute tool: POST ${D}/execute`),console.log(`
|
|
432
|
-
Press Ctrl+C to stop the server`);let O=async t=>{console.log(`\n\n${t} received. Shutting down gracefully...`);try{x.stop(),E.close(),console.log(`Server closed`);try{await u.get(e.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{o&&await o.release({kill:!1}),console.log(`Service deregistered`)}catch(e){console.error(`Process registry cleanup error:`,e)}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>O(`SIGINT`)),process.on(`SIGTERM`,()=>O(`SIGTERM`))}catch(e){if(o)try{await o.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Zt={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 Qt(e){let t=new Set;for(let[n,r]of Object.entries(Zt))r.some(t=>e.includes(t))&&t.add(n);return t}const $t=3e3,en=[`browser_launch`],tn=[`browser_new_page`];function nn(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 rn(e){await new Promise(t=>setTimeout(t,e))}async function an(e){let t=new AbortController,n=setTimeout(()=>t.abort(),$t);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 on(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function sn(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),$t);try{let r=await fetch(on(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 cn(e){let t;for(let n=1;n<=4;n+=1)try{return await an(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await rn(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function ln(e,t){let n;for(let r=1;r<=4;r+=1)try{return await sn(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await rn(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function un(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?Qt(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 dn(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:r,toolFilter:i,customToolsDir:a,enforcedProfileName:o}=e,s=i?.tags?.length?un(i):null,c=new Set(i?.exclude??[]),l=s!==null||c.size>0,u=new D({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(A,async()=>{try{let e=await cn(t);f=e,m=new Set(e.map(e=>e.name));let r=a?await ln(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(k,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 ln(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?on(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(en.includes(i)){let e=nn(o.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if(tn.includes(i)){let e=nn(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 Z=`stdio`,fn=`http`,Q=`streamable-http`,pn=new Set([Z,fn,Q]),mn=`chromium`,hn=new Set([mn,`firefox`,`webkit`]),gn=new Set([`playwright`,`extension`,`vm`,`stealth`]),_n=`SIGINT`,vn=`SIGTERM`,yn=[`pnpm-workspace.yaml`,`nx.json`,`.git`],bn=`browse-tool-mcp-http`,xn=`service`,Sn=E.min;var Cn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...pn].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},wn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...hn].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},Tn=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...gn].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function En(e){let t=e.toLowerCase();if(!hn.has(t))throw new wn(e);return t}function Dn(e){let t=e.toLowerCase();if(!gn.has(t))throw new Tn(e);return t}function On(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 kn(e){let t=e.toLowerCase();return t===fn?Q:t}function An(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function jn(e=process.cwd()){let t=M.resolve(e);for(;;){for(let e of yn)if(j(M.join(t,e)))return t;let e=M.dirname(t);if(e===t)return process.cwd();t=e}}function Mn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function Nn(e){let t=e.tags?Mn(e.tags):void 0,n=e.exclude?Mn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var Pn=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}},Fn=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}},In=class extends Error{code=`HTTP_SERVER_START_FAILED`;recovery=`Check if the HTTP server binary exists and the port is available`;constructor(e){super(`Failed to start HTTP server`,e),this.name=`HttpServerStartError`}},Ln=class extends Error{code=`MCP_SERVER_BOOTSTRAP_FAILED`;recovery=`Check if all dependencies are installed and try again`;constructor(e){super(`Failed to start MCP server`,e),this.name=`McpServerBootstrapError`}};async function Rn(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 Pn(r,t,{cause:i});e.clear()}async function zn(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 Rn(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 Fn(a,{cause:o});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(_n,()=>a(_n)),process.once(vn,()=>a(vn))}const Bn=new C(`mcp-serve`).description(`Start Playwright MCP server for browser automation`).option(`-t, --type <type>`,`Transport type: ${[Z,Q].join(`, `)}`,Z).option(`-b, --browser <browser>`,`Default browser type: ${[...hn].join(`, `)}`,mn).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`,String(Sn)).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...gn].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=z(`mcpServe`),r={type:B(this,`type`,t.type,n.type),browser:B(this,`browser`,t.browser,n.browser),headless:B(this,`headless`,t.headless,n.headless),profile:B(this,`profile`,t.profile,n.profile),mode:B(this,`mode`,t.mode,n.mode),host:B(this,`host`,t.host,n.host,process.env.PLAYWRIGHT_HOST),port:B(this,`port`,t.port,n.port,process.env.PLAYWRIGHT_MCP_PORT),tags:B(this,`tags`,t.tags,n.tags),exclude:B(this,`exclude`,t.exclude,n.exclude),customTools:B(this,`customTools`,t.customTools,n.customTools),snippetsDir:B(this,`snippetsDir`,t.snippetsDir,n.snippetsDir),idleTimeout:B(this,`idleTimeout`,t.idleTimeout,n.idleTimeout===void 0?void 0:String(n.idleTimeout),process.env[a]),registryPath:B(this,`registryPath`,t.registryPath,n.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:B(this,`registryDir`,t.registryDir,n.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:B(this,`pidsDir`,t.pidsDir,n.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:B(this,`profilesDir`,t.profilesDir,n.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR)},i=kn(r.type),c=jn(process.cwd()),l=process.env.NODE_ENV||`development`,u;try{if(!pn.has(i))throw new Cn(i);let t=En(r.browser),n=r.mode?Dn(r.mode):void 0,d=On(r.port??Sn),f=Nn(r),h=r.customTools?M.resolve(r.customTools):void 0,y=r.snippetsDir?M.resolve(r.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${i}`),i===Q&&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=te(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 In;let w=v(o(),C.port),ee=C.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${ee} on port ${C.port}`),i===Z){let e=new g,i=new p(dn({httpBaseUrl:w,sessionTracker:e,defaultMode:n,toolFilter:f,customToolsDir:h,enforcedProfileName:r.profile}));u=await T({repositoryPath:c,serviceName:`browse-tool-mcp`,serviceType:xn,environment:l,pid:process.pid,metadata:{transport:Z,browser:t,mode:n}},S),await zn(i,e,w,async()=>{await u?.release({kill:!1})});return}let E=await new ne(process.env.PORT_REGISTRY_PATH).reservePort({repositoryPath:c,serviceName:bn,serviceType:`tool`,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:Q}});if(!E.success||!E.record)throw Error(E.error||`Failed to reserve ${Q} port ${d}`);let D=E.record.port;u=await T({repositoryPath:c,serviceName:bn,serviceType:xn,environment:l,pid:process.pid,port:D,host:r.host??o(),metadata:{healthCheckUrl:`${v(r.host??o(),D)}/health`,transport:Q}},S);let O=new _(({headers:e})=>{let t=An(e,`x-profile`)??r.profile,i=new g;return{server:dn({httpBaseUrl:w,sessionTracker:i,defaultMode:n,toolFilter:f,customToolsDir:h,enforcedProfileName:t}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await Rn(i,w)}}},{host:r.host??o(),port:D});try{await O.start()}catch(e){try{await u.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}throw e}let k=!1,A=async e=>{if(!k){k=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await O.stop();try{await u?.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(_n,()=>A(_n)),process.once(vn,()=>A(vn))}catch(e){if(u)try{await u.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}(e instanceof Cn||e instanceof wn||e instanceof Tn||e instanceof In)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new Ln({cause:e instanceof Error?e:Error(String(e))});console.error(`Error [${t.code}]: ${t.message}`,t.cause),console.error(`Recovery: ${t.recovery}`),process.exit(1)}}),Vn=new C(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=z(`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
|
|
434
|
-
`)}}function
|
|
431
|
+
`)})]}),L(`body`,{children:t})]})}function Vt(t){let n=new P;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(L(Bt,{title:`Playwright MCP Dashboard`,children:L(zt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html(L(Bt,{title:`Playwright MCP Dashboard - Error`,children:R(`div`,{style:`padding: 2rem; text-align: center;`,children:[L(`h1`,{children:`Failed to load dashboard`}),L(`p`,{children:e instanceof Error?e.message:String(e)}),L(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function Ht(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 Ut(t){try{return t.get(e.TelemetryService)}catch{return new r}}function Wt(t){let n=new P,r=t.get(e.PageRegistry),i=t.get(e.BrowserService),a=t.get(e.ExtensionTaskQueue),o=Ut(t),s=new wt(r,a,o,i);n.use(`*`,re({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:F.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=Ht(c);return d?(a?.setStatus({code:F.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=Ht(u);return d?(l?.setStatus({code:F.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=he(t);n.route(`/extension`,c);let l=Tt(t);n.route(`/api`,l);let u=Vt(t);return n.route(`/`,u),n}function Gt(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 Kt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function qt(e=process.cwd()){let t=N.resolve(e);for(;;){for(let e of Kt)if(M(N.join(t,e)))return t;let e=N.dirname(t);if(e===t)return process.cwd();t=e}}const Jt=new S(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(u)).option(`--host <host>`,`Host to bind`,a()).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 r=B(`httpServe`),a={port:V(this,`port`,t.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),headless:V(this,`headless`,t.headless,r.headless),idleTimeout:V(this,`idleTimeout`,t.idleTimeout,r.idleTimeout===void 0?void 0:String(r.idleTimeout),process.env[i]),host:V(this,`host`,t.host,r.host,process.env.PLAYWRIGHT_HOST),registryDir:V(this,`registryDir`,t.registryDir,r.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:V(this,`registryPath`,t.registryPath,r.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:V(this,`pidsDir`,t.pidsDir,r.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:V(this,`profilesDir`,t.profilesDir,r.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:V(this,`snippetsDir`,t.snippetsDir,r.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR)},o;try{let t=Number.parseInt(a.port,10),r=`browse-tool-http`,s=process.env.NODE_ENV||`development`,c=qt(process.cwd()),l=a.registryPath||a.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;l&&(process.env.PLAYWRIGHT_REGISTRY_DIR=l,process.env.PORT_REGISTRY_PATH=l,process.env.PROCESS_REGISTRY_PATH=ee(l,`processes.json`)),process.env.PLAYWRIGHT_HOST=a.host,process.env.PLAYWRIGHT_PORT=a.port,process.env[i]=a.idleTimeout,a.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=a.pidsDir),a.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=a.profilesDir),a.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=N.resolve(a.snippetsDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${t}`),console.log(` Host: ${a.host}`),console.log(` Headless: ${a.headless}`),console.log(` Idle Timeout: ${a.idleTimeout} minutes`),process.env.BROWSE_TOOL_SNIPPETS_DIR&&console.log(` Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`);let u=n(),d=u.get(e.ProcessRegistryService),f=Wt(u),{injectWebSocket:p,upgradeWebSocket:m}=le({app:f});Gt(f,u,m);let h=u.get(e.WebSocketHub),g=u.get(e.ExtensionTaskQueue),v=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=v.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,v.registerExtensionBrowserWithId(i),a=b.registerExtensionPage(i);let e=v.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=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&&v.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,v.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&y.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let S=new D(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:r,serviceType:`tool`,environment:s,preferredPort:t,pid:process.pid,host:a.host,force:!0,portRange:{min:t,max:t},metadata:{healthCheckUrl:`${_(a.host,t)}/health`,headless:a.headless,idleTimeout:a.idleTimeout}});if(!C.success||!C.record)throw Error(C.error||`Failed to reserve port ${t} in global registry`);let E=t,O=w({fetch:f.fetch,port:E,hostname:a.host});p(O),o=await T({repositoryPath:c,serviceName:r,serviceType:`service`,environment:s,pid:process.pid,port:E,host:a.host,metadata:{healthCheckUrl:`${_(a.host,E)}/health`,headless:a.headless,idleTimeout:a.idleTimeout,transport:`http`}},d);let k=_(a.host,E);console.log(`HTTP server listening on ${k}`),console.log(` Health check: ${k}/health`),console.log(` Execute tool: POST ${k}/execute`),console.log(`
|
|
432
|
+
Press Ctrl+C to stop the server`);let A=async t=>{console.log(`\n\n${t} received. Shutting down gracefully...`);try{x.stop(),O.close(),console.log(`Server closed`);try{await u.get(e.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{o&&await o.release({kill:!1}),console.log(`Service deregistered`)}catch(e){console.error(`Process registry cleanup error:`,e)}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>A(`SIGINT`)),process.on(`SIGTERM`,()=>A(`SIGTERM`)),await new Promise(()=>{})}catch(e){if(o)try{await o.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Yt={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 Xt(e){let t=new Set;for(let[n,r]of Object.entries(Yt))r.some(t=>e.includes(t))&&t.add(n);return t}const Zt=3e3,Qt=[`browser_launch`],$t=[`browser_new_page`];function en(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 tn(e){await new Promise(t=>setTimeout(t,e))}async function nn(e){let t=new AbortController,n=setTimeout(()=>t.abort(),Zt);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 rn(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function an(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),Zt);try{let r=await fetch(rn(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 on(e){let t;for(let n=1;n<=4;n+=1)try{return await nn(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await tn(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function sn(e,t){let n;for(let r=1;r<=4;r+=1)try{return await an(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await tn(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function cn(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?Xt(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 ln(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:r,toolFilter:i,customToolsDir:a,enforcedProfileName:o}=e,s=i?.tags?.length?cn(i):null,c=new Set(i?.exclude??[]),l=s!==null||c.size>0,u=new O({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(j,async()=>{try{let e=await on(t);f=e,m=new Set(e.map(e=>e.name));let r=a?await sn(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(A,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 sn(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?rn(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(Qt.includes(i)){let e=en(o.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if($t.includes(i)){let e=en(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 Q=`stdio`,un=`http`,$=`streamable-http`,dn=new Set([Q,un,$]),fn=`chromium`,pn=new Set([fn,`firefox`,`webkit`]),mn=new Set([`playwright`,`extension`,`vm`,`stealth`]),hn=`SIGINT`,gn=`SIGTERM`,_n=`PLAYWRIGHT_PORT`,vn=[`pnpm-workspace.yaml`,`nx.json`,`.git`],yn=`browse-tool-mcp-http`,bn=`service`,xn=E.min;var Sn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...dn].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},Cn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...pn].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},wn=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...mn].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function Tn(e){let t=e.toLowerCase();if(!pn.has(t))throw new Cn(e);return t}function En(e){let t=e.toLowerCase();if(!mn.has(t))throw new wn(e);return t}function Dn(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 On(e){return e!=null&&String(e).trim()!==``}function kn(e){let t=e.toLowerCase();return t===un?$:t}function An(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function jn(e=process.cwd()){let t=N.resolve(e);for(;;){for(let e of vn)if(M(N.join(t,e)))return t;let e=N.dirname(t);if(e===t)return process.cwd();t=e}}function Mn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function Nn(e){let t=e.tags?Mn(e.tags):void 0,n=e.exclude?Mn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var Pn=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}},Fn=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}},In=class extends Error{code=`HTTP_SERVER_START_FAILED`;recovery=`Check if the HTTP server binary exists and the port is available`;constructor(e){super(`Failed to start HTTP server`,e),this.name=`HttpServerStartError`}},Ln=class extends Error{code=`MCP_SERVER_BOOTSTRAP_FAILED`;recovery=`Check if all dependencies are installed and try again`;constructor(e){super(`Failed to start MCP server`,e),this.name=`McpServerBootstrapError`}};async function Rn(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 Pn(r,t,{cause:i});e.clear()}async function zn(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 Rn(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 Fn(a,{cause:o});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(hn,()=>a(hn)),process.once(gn,()=>a(gn))}const Bn=new S(`mcp-serve`).description(`Start Playwright MCP server for browser automation`).option(`-t, --type <type>`,`Transport type: ${[Q,$].join(`, `)}`,Q).option(`-b, --browser <browser>`,`Default browser type: ${[...pn].join(`, `)}`,fn).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`,a()).option(`--port <port>`,`Port for streamable HTTP transport`,String(xn)).option(`--http-port <port>`,`Port for the inner browse-tool HTTP service`).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...mn].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=B(`mcpServe`),r={type:V(this,`type`,t.type,n.type),browser:V(this,`browser`,t.browser,n.browser),headless:V(this,`headless`,t.headless,n.headless),profile:V(this,`profile`,t.profile,n.profile),mode:V(this,`mode`,t.mode,n.mode),host:V(this,`host`,t.host,n.host,process.env.PLAYWRIGHT_HOST),port:V(this,`port`,t.port,n.port,process.env.PLAYWRIGHT_MCP_PORT),httpPort:V(this,`httpPort`,t.httpPort,n.httpPort,process.env[_n]),tags:V(this,`tags`,t.tags,n.tags),exclude:V(this,`exclude`,t.exclude,n.exclude),customTools:V(this,`customTools`,t.customTools,n.customTools),snippetsDir:V(this,`snippetsDir`,t.snippetsDir,n.snippetsDir),idleTimeout:V(this,`idleTimeout`,t.idleTimeout,n.idleTimeout===void 0?void 0:String(n.idleTimeout),process.env[i]),registryPath:V(this,`registryPath`,t.registryPath,n.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:V(this,`registryDir`,t.registryDir,n.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:V(this,`pidsDir`,t.pidsDir,n.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:V(this,`profilesDir`,t.profilesDir,n.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR)},o=kn(r.type),s=jn(process.cwd()),c=process.env.NODE_ENV||`development`,l,u;try{if(!dn.has(o))throw new Sn(o);let t=Tn(r.browser),n=r.mode?En(r.mode):void 0,d=Dn(r.port??xn),m=On(r.httpPort),v=m?Dn(r.httpPort):void 0,y=Nn(r),b=r.customTools?N.resolve(r.customTools):void 0,x=r.snippetsDir?N.resolve(r.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${o}`),o===$&&console.error(` MCP endpoint: http://${r.host??a()}:${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}`),y?.tags?.length&&console.error(` Tags: ${y.tags.join(`, `)}`),y?.exclude?.length&&console.error(` Exclude: ${y.exclude.join(`, `)}`),b&&console.error(` Custom tools: ${b}`),x&&console.error(` Snippets dir: ${x}`),r.idleTimeout&&console.error(` Idle timeout: ${r.idleTimeout} minutes`);let S=r.registryPath||r.registryDir;S&&(process.env.PLAYWRIGHT_REGISTRY_DIR=S,process.env.PORT_REGISTRY_PATH=S,process.env.PROCESS_REGISTRY_PATH=ee(S,`processes.json`),console.error(` Registry path: ${S}`)),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}`)),x&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=x),r.idleTimeout&&(process.env[i]=r.idleTimeout),r.host&&(process.env.PLAYWRIGHT_HOST=r.host);let C=p(),w=C.get(e.ProcessRegistryService);u=await C.get(e.McpPortAllocationService).acquirePorts({repositoryPath:s,environment:c,host:r.host??a(),pid:process.pid,preferredMcpPort:o===$?d:void 0,preferredHttpPort:v,exactHttpPort:m,reserveMcpPort:o===$,mcpServiceName:yn,mcpServiceType:`tool`,mcpMetadata:o===$?{healthCheckUrl:`${_(r.host??a(),d)}/health`,transport:$}:void 0});let E=u.httpPort;process.env[_n]=E.toString();let D=await C.get(e.HttpServerManager).ensureRunning(E,{exactPort:!0});if(!D.running)throw new In;let O=_(a(),D.port),k=D.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${k} on port ${D.port}`),o===Q){let e=new h,i=new f(ln({httpBaseUrl:O,sessionTracker:e,defaultMode:n,toolFilter:y,customToolsDir:b,enforcedProfileName:r.profile}));l=await T({repositoryPath:s,serviceName:`browse-tool-mcp`,serviceType:bn,environment:c,pid:process.pid,metadata:{transport:Q,browser:t,mode:n}},w),await zn(i,e,O,async()=>{await l?.release({kill:!1}),await u?.release()});return}let A=u.mcpPort;if(!A)throw Error(`Failed to reserve ${$} port ${d}`);l=await T({repositoryPath:s,serviceName:yn,serviceType:bn,environment:c,pid:process.pid,port:A,host:r.host??a(),metadata:{healthCheckUrl:`${_(r.host??a(),A)}/health`,transport:$}},w);let j=new g(({headers:e})=>{let t=An(e,`x-profile`)??r.profile,i=new h;return{server:ln({httpBaseUrl:O,sessionTracker:i,defaultMode:n,toolFilter:y,customToolsDir:b,enforcedProfileName:t}),onClose:async()=>{i.getSessionState().totalBrowsers>0&&await Rn(i,O)}}},{host:r.host??a(),port:A});try{await j.start()}catch(e){try{await l.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}throw await u.release(),u=void 0,e}let M=!1,te=async e=>{if(!M){M=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await j.stop();try{await l?.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}await u?.release(),u=void 0,process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(hn,()=>te(hn)),process.once(gn,()=>te(gn))}catch(e){if(l)try{await l.release({kill:!1})}catch(e){console.error(`Process registry cleanup error:`,e)}if(u)try{await u.release()}catch(e){console.error(`Port allocation cleanup error:`,e)}(e instanceof Sn||e instanceof Cn||e instanceof wn||e instanceof In)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new Ln({cause:e instanceof Error?e:Error(String(e))});console.error(`Error [${t.code}]: ${t.message}`,t.cause),console.error(`Recovery: ${t.recovery}`),process.exit(1)}}),Vn=new S(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=B(`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 l().get(e.HttpServerManager).getStatus();if(r.running){let e=process.env.PLAYWRIGHT_HOST??t.host??a();console.log(`HTTP Server: Running`),console.log(` PID: ${r.pid}`),console.log(` Port: ${r.port}`),console.log(` Health: ${_(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)}}),Hn=new S(`stop`).description(`Stop HTTP server and clean up registry/PID files`).action(async()=>{try{let t=B(`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 l().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)}}),Un=new Set([`--config`]);function Wn(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(Un.has(n)){t+=1;continue}if(![...Un].some(e=>n.startsWith(`${e}=`))&&!n.startsWith(`-`))return n}}function Gn(e,t=!1){if(t)return!1;let n=Wn(e);return n?n===`tools`?!0:n===`help`&&e.includes(`tools`):!1}function Kn(e){return e.replace(/_/g,`-`)}function qn(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function Jn(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 Yn(e,t){let n=Jn(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 Xn(e,t){let n=qn(e);return t.type===`boolean`?`--${n}`:`--${n} <value>`}function Zn(e){let{definition:t,execute:n}=e,r=new S(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=Xn(e,n),a=n.description||``;n.enum&&n.enum.length>0&&(a+=` (choices: ${n.enum.join(`, `)})`),n.default!==void 0&&(a+=` (default: ${JSON.stringify(n.default)})`),o.has(e)&&(a+=` [required]`),n.type===`boolean`?n.default===!0?r.option(`--no-${qn(e)}`,`Disable ${a}`):r.option(i,a,n.default):n.enum&&n.enum.length>0?r.addOption(new C(i,a).choices(n.enum)):r.option(i,a)}return r.action(async function(){let e=this.optsWithGlobals(),r=B(`tools`),i=V(this,`format`,e.format??`json`,r.format),s=V(this,`color`,e.color??!0,r.color),c=V(this,`port`,String(e.port??u),r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),l={format:i,color:s},d=He(t.name);try{let r={};for(let[t,n]of Object.entries(a)){let i=n,a=qn(t).replace(/-([a-z])/g,(e,t)=>t.toUpperCase()),o=this.getOptionValueSourceWithGlobals(a),s=e[a];i.type===`boolean`&&i.default===!0&&(s=e[a]),(o===void 0||o===`default`||o===`implied`)&&d[t]!==void 0&&(s=d[t]),s!==void 0&&(typeof s==`string`&&i.type!==`string`?r[t]=Yn(s,i):r[t]=s)}for(let e of o)if(r[e]===void 0){let t=qn(e);console.error(W(`Missing required option: --${t}`,l.color)),process.exit(1)}let i=Number.parseInt(c,10),s=H({port:i}),u=n?await n(r,{command:this,formatterOptions:l,port:i}):await s.execute(t.name,r),f=Ge(u,l);f&&console.log(f),u.isError&&process.exit(1)}catch(e){console.error(W(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),r}function Qn(e){return e.map(Zn)}const $n={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 er(){return new S(`tools`).description(`Execute browser automation tools`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(u))}function tr(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 nr(){return{definition:$n,execute:async(e,t)=>{let n=await H({port:t.port}).listBrowsers();return{content:[{type:`text`,text:JSON.stringify(tr(n),null,2)}]}}}}function rr(e){return[...e.map(e=>({definition:e})),nr()]}async function ir(e,t){let n=B(`tools`),r=Number(t?.port??process.env.PLAYWRIGHT_PORT??n.port??u);try{let t=Qn(rr(await H({port:r}).listTools()));for(let n of t)e.addCommand(n)}catch(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(`${W(`Failed to load tool commands: ${t}`,!0)}\n`),process.stderr.write(`Make sure the HTTP server is running: browse-tool http-serve
|
|
434
|
+
`)}}function ar(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 or(){let e=Ve(process.argv.slice(2)),t=process.env.PLAYWRIGHT_PORT??e.config.commands.tools?.port??e.config.commands.exec?.port??u,n=new S;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(fe),n.addCommand(Bn),n.addCommand(Jt),n.addCommand(De),n.addCommand(nt),n.addCommand(Hn),n.addCommand(Vn),n.addCommand(rt),n.addCommand(et),n.addCommand(tt);let r=er();Gn(process.argv.slice(2),process.env.PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS===`1`)&&await ir(r,{port:ar(process.argv.slice(2),t)}),n.addCommand(r),await n.parseAsync(process.argv)}or().catch(e=>{console.error(e instanceof Error?e.message:String(e)),process.exit(1)});export{};
|
|
435
435
|
//# sourceMappingURL=cli.mjs.map
|