browser-devtools-mcp 0.4.5 → 0.5.1

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/SECURITY.md CHANGED
@@ -2,10 +2,15 @@
2
2
 
3
3
  ## Supported Versions
4
4
 
5
- | Version | Supported |
6
- | ------- | ------------------ |
7
- | 0.2.x | :white_check_mark: |
8
- | 0.1.x | :white_check_mark: |
5
+ Security patches are published for supported release lines. We recommend always running the **latest** `0.5.x` release.
6
+
7
+ | Version | Supported |
8
+ | --------- | --------- |
9
+ | **0.5.x** | :white_check_mark: Active (features + security fixes) |
10
+ | **0.4.x** | :warning: Security fixes only (best effort); upgrade to 0.5.x recommended |
11
+ | **< 0.4** | :x: Unsupported |
12
+
13
+ Pre-1.0 versions may include breaking changes between minors; treat upgrades as requiring release-note review.
9
14
 
10
15
  ## Reporting a Vulnerability
11
16
 
@@ -40,26 +45,34 @@ Browser DevTools MCP provides powerful browser automation capabilities. Users sh
40
45
 
41
46
  2. **Network Access**: The browser can make requests to any URL
42
47
  - HTTP requests from the browser inherit the page's cookies and session
43
- - Stubbing/mocking tools can intercept and modify requests
48
+ - Stubbing/mocking tools can intercept and modify requests or responses—use only in trusted environments and avoid pointing at production systems unless intentional
49
+ - Observability tools can log URLs, headers, and (when enabled) bodies; treat captured traffic as potentially sensitive
44
50
  - Be cautious when automating authenticated sessions
45
51
 
46
52
  3. **Persistent Context**: When `BROWSER_PERSISTENT_ENABLE=true`
47
53
  - Browser state (cookies, localStorage) persists across sessions
48
54
  - User data is stored in `BROWSER_PERSISTENT_USER_DATA_DIR`
49
55
  - Sensitive data may be stored locally
56
+ - Restrict filesystem permissions on the user data directory
57
+
58
+ 4. **Chrome DevTools Protocol (CDP) attach**: When `BROWSER_CDP_ENABLE=true` or `BROWSER_CDP_ENDPOINT_URL` is set (Chromium only)
59
+ - The server may connect to an existing browser over CDP, which is equivalent to **full remote control** of that browser (navigation, JS, network, etc.)
60
+ - **Never** expose a CDP endpoint to untrusted networks or the public internet without strict access controls (firewall, VPN, auth proxy)
61
+ - Prefer **loopback** (`127.0.0.1`) for local debugging; treat any remote `BROWSER_CDP_ENDPOINT_URL` as highly sensitive configuration
62
+ - Enabling remote debugging on Chrome (e.g. `--remote-debugging-port=`) increases attack surface—disable when not needed
50
63
 
51
- 4. **Screenshot/PDF Capture**: Content tools can capture sensitive information
64
+ 5. **Screenshot/PDF Capture**: Content tools can capture sensitive information
52
65
  - Screenshots may contain PII or credentials visible on screen
53
66
  - Optional annotated screenshots overlay ref labels and return an annotations array (ref, role, name, box); when refs are empty, an ARIA snapshot is taken automatically to populate refs
54
67
  - PDFs preserve full page content
55
68
  - Be mindful of what pages are captured
56
69
 
57
- 5. **ARIA Snapshots and Ref Map**: Session context stores a ref map (e1, e2, ...) from the last ARIA snapshot
70
+ 6. **ARIA Snapshots and Ref Map**: Session context stores a ref map (e1, e2, ...) from the last ARIA snapshot
58
71
  - Refs map to element roles, names, and selectors; used by interaction tools (click, fill, hover, select, drag, scroll, press-key) and annotated screenshots
59
72
  - Ref map is session-scoped and not persisted; it is cleared on navigation or when a new snapshot is taken
60
73
  - Cursor-interactive refs (when cursorInteractive is enabled) may include generated CSS selectors for elements without ARIA roles
61
74
 
62
- 6. **Debug Tools**: Non-blocking debugging capabilities capture runtime state
75
+ 7. **Debug Tools**: Non-blocking debugging capabilities capture runtime state
63
76
  - Tracepoints capture call stack and local variables (may include sensitive data)
64
77
  - Logpoints evaluate expressions in page context
65
78
  - Exception snapshots capture error state and stack traces
@@ -95,10 +108,15 @@ When using HTTP transport (`--transport=streamable-http`):
95
108
  ### Session Security
96
109
 
97
110
  - Sessions are isolated per MCP client connection
111
+ - **Trust model**: Any client that can invoke the MCP server can use browser automation tools; lock down who can start the process and who can connect (stdio parent process, HTTP bind address, reverse proxy auth)
98
112
  - Session data is stored in memory (not persisted by default)
99
113
  - Idle sessions are automatically cleaned up (`SESSION_IDLE_SECONDS`)
100
114
  - Setting `SESSION_CLOSE_ON_SOCKET_CLOSE=true` closes sessions immediately on disconnect
101
115
 
116
+ ### Node Platform (`node-devtools-mcp`)
117
+
118
+ The Node entrypoint exposes the same **`execute`** tool pattern in a VM without an injected Playwright `page`. It remains **not** a security boundary: arbitrary JS in the sandbox can still be dangerous if the VM or allowed builtins change over time. Only run with trusted inputs and callers.
119
+
102
120
  ## Security Features
103
121
 
104
122
  ### Sandbox Isolation
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{isToolEnabled,platformInfo}from"../core-RE2PLB4I.js";import{AMAZON_BEDROCK_ENABLE,AMAZON_BEDROCK_IMAGE_EMBED_MODEL_ID,AMAZON_BEDROCK_TEXT_EMBED_MODEL_ID,AMAZON_BEDROCK_VISION_MODEL_ID,AWS_PROFILE,AWS_REGION,DAEMON_PORT,DAEMON_SESSION_IDLE_CHECK_SECONDS,DAEMON_SESSION_IDLE_SECONDS,FIGMA_ACCESS_TOKEN,FIGMA_API_BASE_URL,OTEL_ENABLE,OTEL_EXPORTER_HTTP_URL,OTEL_EXPORTER_TYPE,OTEL_SERVICE_NAME,OTEL_SERVICE_VERSION}from"../core-3YBKJFSF.js";import{Command,Option}from"commander";import{ZodFirstPartyTypeKind}from"zod";function _unwrapZodType(zodType){let current=zodType,isOptional=!1,defaultValue;for(;;){let typeName=current._def.typeName;if(typeName===ZodFirstPartyTypeKind.ZodOptional)isOptional=!0,current=current._def.innerType;else if(typeName===ZodFirstPartyTypeKind.ZodDefault)isOptional=!0,defaultValue=current._def.defaultValue(),current=current._def.innerType;else if(typeName===ZodFirstPartyTypeKind.ZodNullable)isOptional=!0,current=current._def.innerType;else break}return{innerType:current,isOptional,defaultValue}}function _getDescription(zodType){return zodType._def.description}function _toCamelCase(str){return str.replace(/[-_]([a-z])/g,(_,letter)=>letter.toUpperCase())}function _toKebabCase(str){return str.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function _createOption(name,zodType){let{innerType,isOptional,defaultValue}=_unwrapZodType(zodType),description=_getDescription(zodType)||`The ${name} value`,flagName=_toKebabCase(name),typeName=innerType._def.typeName,option;switch(typeName){case ZodFirstPartyTypeKind.ZodString:option=new Option(`--${flagName} <string>`,description);break;case ZodFirstPartyTypeKind.ZodNumber:option=new Option(`--${flagName} <number>`,description),option.argParser(value=>{let n=Number(value);if(!Number.isFinite(n))throw new Error(`Invalid number: ${value}`);return n});break;case ZodFirstPartyTypeKind.ZodBoolean:option=new Option(`--${flagName}`,description);break;case ZodFirstPartyTypeKind.ZodEnum:let enumValues=innerType._def.values;option=new Option(`--${flagName} <choice>`,description).choices(enumValues);break;case ZodFirstPartyTypeKind.ZodArray:option=new Option(`--${flagName} <value...>`,description);break;case ZodFirstPartyTypeKind.ZodObject:case ZodFirstPartyTypeKind.ZodRecord:option=new Option(`--${flagName} <json>`,description),option.argParser(value=>{try{return JSON.parse(value)}catch{throw new Error(`Invalid JSON: ${value}`)}});break;case ZodFirstPartyTypeKind.ZodAny:case ZodFirstPartyTypeKind.ZodUnknown:option=new Option(`--${flagName} <value>`,description),option.argParser(value=>{try{return JSON.parse(value)}catch{return value}});break;case ZodFirstPartyTypeKind.ZodLiteral:let literalValue=innerType._def.value;typeof literalValue=="boolean"?option=new Option(`--${flagName}`,description):(option=new Option(`--${flagName} <value>`,description),option.default(literalValue));break;case ZodFirstPartyTypeKind.ZodUnion:let unionOptions=innerType._def.options;if(unionOptions.every(opt=>opt._def.typeName===ZodFirstPartyTypeKind.ZodLiteral)){let choices=unionOptions.map(opt=>String(opt._def.value));option=new Option(`--${flagName} <choice>`,description).choices(choices)}else option=new Option(`--${flagName} <value>`,description),option.argParser(value=>{try{return JSON.parse(value)}catch{return value}});break;default:option=new Option(`--${flagName} <value>`,description);break}return defaultValue!==void 0&&option.default(defaultValue),!isOptional&&defaultValue===void 0&&option.makeOptionMandatory(!0),option}function _generateOptionsFromSchema(schema,toolName){let options=[];for(let[name,zodType]of Object.entries(schema)){let option=_createOption(name,zodType);option&&options.push(option)}return options}function _parseOptionsToToolInput(options){let result={};for(let[key,value]of Object.entries(options)){let camelKey=_toCamelCase(key);value!==void 0&&(result[camelKey]=value)}return result}function _parseToolName(toolName){let underscoreIndex=toolName.indexOf("_");return underscoreIndex===-1?{domain:"default",commandName:toolName}:{domain:toolName.substring(0,underscoreIndex),commandName:toolName.substring(underscoreIndex+1)}}function registerToolCommands(program,tools2,handler){let domainCommands=new Map;for(let tool of tools2){let{domain,commandName}=_parseToolName(tool.name()),domainCommand=domainCommands.get(domain);domainCommand||(domainCommand=new Command(domain).description(`${domain.charAt(0).toUpperCase()+domain.slice(1)} commands`),domainCommands.set(domain,domainCommand),program.addCommand(domainCommand));let toolCommand=new Command(commandName).description(tool.description().trim()),options=_generateOptionsFromSchema(tool.inputSchema(),tool.name());for(let option of options)toolCommand.addOption(option);toolCommand.action(async opts=>{let toolInput=_parseOptionsToToolInput(opts),globalOptions=program.opts();await handler(tool.name(),toolInput,globalOptions)}),domainCommand.addCommand(toolCommand)}}import{spawn,execSync}from"node:child_process";import{createRequire}from"node:module";import*as path from"node:path";import*as readline from"node:readline";import{fileURLToPath}from"node:url";import{Command as Command2,Option as Option2}from"commander";var require2=createRequire(import.meta.url),__filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),cliProvider=platformInfo.cliInfo.cliProvider,tools=cliProvider.tools.filter(isToolEnabled),DEFAULT_TIMEOUT=3e4,verboseEnabled=!1,quietEnabled=!1;function _debug(message,data){if(verboseEnabled){let timestamp=new Date().toISOString();data!==void 0?console.error(`[${timestamp}] [DEBUG] ${message}`,data):console.error(`[${timestamp}] [DEBUG] ${message}`)}}function _output(message){quietEnabled||console.log(message)}function _error(message){console.error(message)}async function _isDaemonRunning(port){_debug(`Checking if daemon is running on port ${port}`);try{let response=await fetch(`http://localhost:${port}/health`,{method:"GET",signal:AbortSignal.timeout(3e3)});if(response.ok){let isRunning=(await response.json()).status==="ok";return _debug(`Daemon health check result: ${isRunning?"running":"not running"}`),isRunning}return _debug(`Daemon health check failed: HTTP ${response.status}`),!1}catch(err){return _debug(`Daemon health check error: ${err.message}`),!1}}function _buildDaemonEnv(opts){return cliProvider.buildEnv(opts)}function _startDaemonDetached(opts){let daemonServerPath=path.join(__dirname,"..","daemon-server.js"),env=_buildDaemonEnv(opts);_debug(`Starting daemon server from: ${daemonServerPath}`),_debug(`Daemon port: ${opts.port}`);let child=spawn(process.execPath,[daemonServerPath,"--port",String(opts.port)],{detached:!0,stdio:"ignore",env});child.unref(),_debug(`Daemon process spawned with PID: ${child.pid}`),_output(`Started daemon server as detached process (PID: ${child.pid})`)}async function _ensureDaemonRunning(opts){if(await _isDaemonRunning(opts.port))_debug("Daemon is already running");else{_output(`Daemon server is not running on port ${opts.port}, starting...`),_startDaemonDetached(opts);let maxRetries=10,retryDelay=500;_debug(`Waiting for daemon to be ready (max ${maxRetries} retries, ${retryDelay}ms delay)`);for(let i=0;i<maxRetries;i++)if(await new Promise(resolve=>setTimeout(resolve,retryDelay)),_debug(`Retry ${i+1}/${maxRetries}: checking daemon status...`),await _isDaemonRunning(opts.port)){_debug("Daemon is now ready"),_output("Daemon server is ready");return}throw new Error(`Daemon server failed to start within ${maxRetries*retryDelay/1e3} seconds`)}}async function _stopDaemon(port,timeout){try{return(await fetch(`http://localhost:${port}/shutdown`,{method:"POST",signal:AbortSignal.timeout(timeout)})).ok}catch{return!1}}async function _callTool(port,toolName,toolInput,sessionId,timeout){let headers={"Content-Type":"application/json"};sessionId&&(headers["session-id"]=sessionId);let request={toolName,toolInput};_debug(`Calling tool: ${toolName}`),_debug("Tool input:",toolInput),_debug(`Session ID: ${sessionId||"(default)"}`),_debug(`Timeout: ${timeout||"none"}`);let startTime=Date.now(),response=await fetch(`http://localhost:${port}/call`,{method:"POST",headers,body:JSON.stringify(request),signal:timeout?AbortSignal.timeout(timeout):void 0}),duration=Date.now()-startTime;if(_debug(`Tool call completed in ${duration}ms, status: ${response.status}`),!response.ok){let errorBody=await response.json().catch(()=>({}));_debug("Tool call error:",errorBody);let message=errorBody?.toolError?.message||errorBody?.error?.message||`HTTP ${response.status}: ${response.statusText}`;throw new Error(message)}let result=await response.json();return _debug("Tool call result:",result.toolError?{error:result.toolError}:{success:!0}),result}async function _deleteSession(port,sessionId,timeout){try{return(await fetch(`http://localhost:${port}/session`,{method:"DELETE",headers:{"session-id":sessionId},signal:AbortSignal.timeout(timeout)})).ok}catch{return!1}}async function _getDaemonInfo(port,timeout){try{let response=await fetch(`http://localhost:${port}/info`,{method:"GET",signal:AbortSignal.timeout(timeout)});return response.ok?await response.json():null}catch{return null}}async function _listSessions(port,timeout){try{let response=await fetch(`http://localhost:${port}/sessions`,{method:"GET",signal:AbortSignal.timeout(timeout)});return response.ok?await response.json():null}catch{return null}}async function _getSessionInfo(port,sessionId,timeout){try{let response=await fetch(`http://localhost:${port}/session`,{method:"GET",headers:{"session-id":sessionId},signal:AbortSignal.timeout(timeout)});return response.ok?await response.json():null}catch{return null}}function _formatUptime(seconds){let days=Math.floor(seconds/86400),hours=Math.floor(seconds%86400/3600),minutes=Math.floor(seconds%3600/60),secs=seconds%60,parts=[];return days>0&&parts.push(`${days}d`),hours>0&&parts.push(`${hours}h`),minutes>0&&parts.push(`${minutes}m`),parts.push(`${secs}s`),parts.join(" ")}function _formatTimestamp(timestamp){return new Date(timestamp).toISOString()}function _getZodTypeName(schema){let typeName=schema._def.typeName;return typeName==="ZodOptional"||typeName==="ZodNullable"||typeName==="ZodDefault"?_getZodTypeName(schema._def.innerType):typeName==="ZodArray"?`${_getZodTypeName(schema._def.type)}[]`:typeName==="ZodEnum"?schema._def.values.join(" | "):typeName==="ZodLiteral"?JSON.stringify(schema._def.value):typeName==="ZodUnion"?schema._def.options.map(opt=>_getZodTypeName(opt)).join(" | "):{ZodString:"string",ZodNumber:"number",ZodBoolean:"boolean",ZodObject:"object",ZodRecord:"Record<string, any>",ZodAny:"any"}[typeName]||typeName.replace("Zod","").toLowerCase()}function _getZodDescription(schema){if(schema._def.description)return schema._def.description;if(schema._def.typeName==="ZodOptional"||schema._def.typeName==="ZodNullable"||schema._def.typeName==="ZodDefault")return _getZodDescription(schema._def.innerType)}function _isZodOptional(schema){let typeName=schema._def.typeName;return typeName==="ZodOptional"||typeName==="ZodNullable"}function _hasZodDefault(schema){return schema._def.typeName==="ZodDefault"?!0:schema._def.typeName==="ZodOptional"||schema._def.typeName==="ZodNullable"?_hasZodDefault(schema._def.innerType):!1}function _getZodDefault(schema){if(schema._def.typeName==="ZodDefault")return schema._def.defaultValue();if(schema._def.typeName==="ZodOptional"||schema._def.typeName==="ZodNullable")return _getZodDefault(schema._def.innerType)}function _formatOutput(output,indent=0){let prefix=" ".repeat(indent);if(output==null)return`${prefix}(empty)`;if(typeof output=="string")return output.split(`
2
+ import{isToolEnabled,platformInfo}from"../core-PKYX4YOY.js";import{AMAZON_BEDROCK_ENABLE,AMAZON_BEDROCK_IMAGE_EMBED_MODEL_ID,AMAZON_BEDROCK_TEXT_EMBED_MODEL_ID,AMAZON_BEDROCK_VISION_MODEL_ID,AWS_PROFILE,AWS_REGION,DAEMON_PORT,DAEMON_SESSION_IDLE_CHECK_SECONDS,DAEMON_SESSION_IDLE_SECONDS,FIGMA_ACCESS_TOKEN,FIGMA_API_BASE_URL,OTEL_ENABLE,OTEL_EXPORTER_HTTP_URL,OTEL_EXPORTER_TYPE,OTEL_SERVICE_NAME,OTEL_SERVICE_VERSION}from"../core-3YBKJFSF.js";import{Command,Option}from"commander";import{ZodFirstPartyTypeKind}from"zod";function _unwrapZodType(zodType){let current=zodType,isOptional=!1,defaultValue;for(;;){let typeName=current._def.typeName;if(typeName===ZodFirstPartyTypeKind.ZodOptional)isOptional=!0,current=current._def.innerType;else if(typeName===ZodFirstPartyTypeKind.ZodDefault)isOptional=!0,defaultValue=current._def.defaultValue(),current=current._def.innerType;else if(typeName===ZodFirstPartyTypeKind.ZodNullable)isOptional=!0,current=current._def.innerType;else break}return{innerType:current,isOptional,defaultValue}}function _getDescription(zodType){return zodType._def.description}function _toCamelCase(str){return str.replace(/[-_]([a-z])/g,(_,letter)=>letter.toUpperCase())}function _toKebabCase(str){return str.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function _createOption(name,zodType){let{innerType,isOptional,defaultValue}=_unwrapZodType(zodType),description=_getDescription(zodType)||`The ${name} value`,flagName=_toKebabCase(name),typeName=innerType._def.typeName,option;switch(typeName){case ZodFirstPartyTypeKind.ZodString:option=new Option(`--${flagName} <string>`,description);break;case ZodFirstPartyTypeKind.ZodNumber:option=new Option(`--${flagName} <number>`,description),option.argParser(value=>{let n=Number(value);if(!Number.isFinite(n))throw new Error(`Invalid number: ${value}`);return n});break;case ZodFirstPartyTypeKind.ZodBoolean:option=new Option(`--${flagName}`,description);break;case ZodFirstPartyTypeKind.ZodEnum:let enumValues=innerType._def.values;option=new Option(`--${flagName} <choice>`,description).choices(enumValues);break;case ZodFirstPartyTypeKind.ZodArray:option=new Option(`--${flagName} <value...>`,description);break;case ZodFirstPartyTypeKind.ZodObject:case ZodFirstPartyTypeKind.ZodRecord:option=new Option(`--${flagName} <json>`,description),option.argParser(value=>{try{return JSON.parse(value)}catch{throw new Error(`Invalid JSON: ${value}`)}});break;case ZodFirstPartyTypeKind.ZodAny:case ZodFirstPartyTypeKind.ZodUnknown:option=new Option(`--${flagName} <value>`,description),option.argParser(value=>{try{return JSON.parse(value)}catch{return value}});break;case ZodFirstPartyTypeKind.ZodLiteral:let literalValue=innerType._def.value;typeof literalValue=="boolean"?option=new Option(`--${flagName}`,description):(option=new Option(`--${flagName} <value>`,description),option.default(literalValue));break;case ZodFirstPartyTypeKind.ZodUnion:let unionOptions=innerType._def.options;if(unionOptions.every(opt=>opt._def.typeName===ZodFirstPartyTypeKind.ZodLiteral)){let choices=unionOptions.map(opt=>String(opt._def.value));option=new Option(`--${flagName} <choice>`,description).choices(choices)}else option=new Option(`--${flagName} <value>`,description),option.argParser(value=>{try{return JSON.parse(value)}catch{return value}});break;default:option=new Option(`--${flagName} <value>`,description);break}return defaultValue!==void 0&&option.default(defaultValue),!isOptional&&defaultValue===void 0&&option.makeOptionMandatory(!0),option}function _generateOptionsFromSchema(schema,toolName){let options=[];for(let[name,zodType]of Object.entries(schema)){let option=_createOption(name,zodType);option&&options.push(option)}return options}function _parseOptionsToToolInput(options){let result={};for(let[key,value]of Object.entries(options)){let camelKey=_toCamelCase(key);value!==void 0&&(result[camelKey]=value)}return result}function _parseToolName(toolName){let underscoreIndex=toolName.indexOf("_");return underscoreIndex===-1?{domain:"default",commandName:toolName}:{domain:toolName.substring(0,underscoreIndex),commandName:toolName.substring(underscoreIndex+1)}}function registerToolCommands(program,tools2,handler){let domainCommands=new Map;for(let tool of tools2){let{domain,commandName}=_parseToolName(tool.name()),domainCommand=domainCommands.get(domain);domainCommand||(domainCommand=new Command(domain).description(`${domain.charAt(0).toUpperCase()+domain.slice(1)} commands`),domainCommands.set(domain,domainCommand),program.addCommand(domainCommand));let toolCommand=new Command(commandName).description(tool.description().trim()),options=_generateOptionsFromSchema(tool.inputSchema(),tool.name());for(let option of options)toolCommand.addOption(option);toolCommand.action(async opts=>{let toolInput=_parseOptionsToToolInput(opts),globalOptions=program.opts();await handler(tool.name(),toolInput,globalOptions)}),domainCommand.addCommand(toolCommand)}}import{spawn,execSync}from"node:child_process";import{createRequire}from"node:module";import*as path from"node:path";import*as readline from"node:readline";import{fileURLToPath}from"node:url";import{Command as Command2,Option as Option2}from"commander";var require2=createRequire(import.meta.url),__filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),cliProvider=platformInfo.cliInfo.cliProvider,tools=cliProvider.tools.filter(isToolEnabled),DEFAULT_TIMEOUT=3e4,verboseEnabled=!1,quietEnabled=!1;function _debug(message,data){if(verboseEnabled){let timestamp=new Date().toISOString();data!==void 0?console.error(`[${timestamp}] [DEBUG] ${message}`,data):console.error(`[${timestamp}] [DEBUG] ${message}`)}}function _output(message){quietEnabled||console.log(message)}function _error(message){console.error(message)}async function _isDaemonRunning(port){_debug(`Checking if daemon is running on port ${port}`);try{let response=await fetch(`http://localhost:${port}/health`,{method:"GET",signal:AbortSignal.timeout(3e3)});if(response.ok){let isRunning=(await response.json()).status==="ok";return _debug(`Daemon health check result: ${isRunning?"running":"not running"}`),isRunning}return _debug(`Daemon health check failed: HTTP ${response.status}`),!1}catch(err){return _debug(`Daemon health check error: ${err.message}`),!1}}function _buildDaemonEnv(opts){return cliProvider.buildEnv(opts)}function _startDaemonDetached(opts){let daemonServerPath=path.join(__dirname,"..","daemon-server.js"),env=_buildDaemonEnv(opts);_debug(`Starting daemon server from: ${daemonServerPath}`),_debug(`Daemon port: ${opts.port}`);let child=spawn(process.execPath,[daemonServerPath,"--port",String(opts.port)],{detached:!0,stdio:"ignore",env});child.unref(),_debug(`Daemon process spawned with PID: ${child.pid}`),_output(`Started daemon server as detached process (PID: ${child.pid})`)}async function _ensureDaemonRunning(opts){if(await _isDaemonRunning(opts.port))_debug("Daemon is already running");else{_output(`Daemon server is not running on port ${opts.port}, starting...`),_startDaemonDetached(opts);let maxRetries=10,retryDelay=500;_debug(`Waiting for daemon to be ready (max ${maxRetries} retries, ${retryDelay}ms delay)`);for(let i=0;i<maxRetries;i++)if(await new Promise(resolve=>setTimeout(resolve,retryDelay)),_debug(`Retry ${i+1}/${maxRetries}: checking daemon status...`),await _isDaemonRunning(opts.port)){_debug("Daemon is now ready"),_output("Daemon server is ready");return}throw new Error(`Daemon server failed to start within ${maxRetries*retryDelay/1e3} seconds`)}}async function _stopDaemon(port,timeout){try{return(await fetch(`http://localhost:${port}/shutdown`,{method:"POST",signal:AbortSignal.timeout(timeout)})).ok}catch{return!1}}async function _callTool(port,toolName,toolInput,sessionId,timeout){let headers={"Content-Type":"application/json"};sessionId&&(headers["session-id"]=sessionId);let request={toolName,toolInput};_debug(`Calling tool: ${toolName}`),_debug("Tool input:",toolInput),_debug(`Session ID: ${sessionId||"(default)"}`),_debug(`Timeout: ${timeout||"none"}`);let startTime=Date.now(),response=await fetch(`http://localhost:${port}/call`,{method:"POST",headers,body:JSON.stringify(request),signal:timeout?AbortSignal.timeout(timeout):void 0}),duration=Date.now()-startTime;if(_debug(`Tool call completed in ${duration}ms, status: ${response.status}`),!response.ok){let errorBody=await response.json().catch(()=>({}));_debug("Tool call error:",errorBody);let message=errorBody?.toolError?.message||errorBody?.error?.message||`HTTP ${response.status}: ${response.statusText}`;throw new Error(message)}let result=await response.json();return _debug("Tool call result:",result.toolError?{error:result.toolError}:{success:!0}),result}async function _deleteSession(port,sessionId,timeout){try{return(await fetch(`http://localhost:${port}/session`,{method:"DELETE",headers:{"session-id":sessionId},signal:AbortSignal.timeout(timeout)})).ok}catch{return!1}}async function _getDaemonInfo(port,timeout){try{let response=await fetch(`http://localhost:${port}/info`,{method:"GET",signal:AbortSignal.timeout(timeout)});return response.ok?await response.json():null}catch{return null}}async function _listSessions(port,timeout){try{let response=await fetch(`http://localhost:${port}/sessions`,{method:"GET",signal:AbortSignal.timeout(timeout)});return response.ok?await response.json():null}catch{return null}}async function _getSessionInfo(port,sessionId,timeout){try{let response=await fetch(`http://localhost:${port}/session`,{method:"GET",headers:{"session-id":sessionId},signal:AbortSignal.timeout(timeout)});return response.ok?await response.json():null}catch{return null}}function _formatUptime(seconds){let days=Math.floor(seconds/86400),hours=Math.floor(seconds%86400/3600),minutes=Math.floor(seconds%3600/60),secs=seconds%60,parts=[];return days>0&&parts.push(`${days}d`),hours>0&&parts.push(`${hours}h`),minutes>0&&parts.push(`${minutes}m`),parts.push(`${secs}s`),parts.join(" ")}function _formatTimestamp(timestamp){return new Date(timestamp).toISOString()}function _getZodTypeName(schema){let typeName=schema._def.typeName;return typeName==="ZodOptional"||typeName==="ZodNullable"||typeName==="ZodDefault"?_getZodTypeName(schema._def.innerType):typeName==="ZodArray"?`${_getZodTypeName(schema._def.type)}[]`:typeName==="ZodEnum"?schema._def.values.join(" | "):typeName==="ZodLiteral"?JSON.stringify(schema._def.value):typeName==="ZodUnion"?schema._def.options.map(opt=>_getZodTypeName(opt)).join(" | "):{ZodString:"string",ZodNumber:"number",ZodBoolean:"boolean",ZodObject:"object",ZodRecord:"Record<string, any>",ZodAny:"any"}[typeName]||typeName.replace("Zod","").toLowerCase()}function _getZodDescription(schema){if(schema._def.description)return schema._def.description;if(schema._def.typeName==="ZodOptional"||schema._def.typeName==="ZodNullable"||schema._def.typeName==="ZodDefault")return _getZodDescription(schema._def.innerType)}function _isZodOptional(schema){let typeName=schema._def.typeName;return typeName==="ZodOptional"||typeName==="ZodNullable"}function _hasZodDefault(schema){return schema._def.typeName==="ZodDefault"?!0:schema._def.typeName==="ZodOptional"||schema._def.typeName==="ZodNullable"?_hasZodDefault(schema._def.innerType):!1}function _getZodDefault(schema){if(schema._def.typeName==="ZodDefault")return schema._def.defaultValue();if(schema._def.typeName==="ZodOptional"||schema._def.typeName==="ZodNullable")return _getZodDefault(schema._def.innerType)}function _formatOutput(output,indent=0){let prefix=" ".repeat(indent);if(output==null)return`${prefix}(empty)`;if(typeof output=="string")return output.split(`
3
3
  `).map(line=>`${prefix}${line}`).join(`
4
4
  `);if(typeof output=="number"||typeof output=="boolean")return`${prefix}${output}`;if(Array.isArray(output))return output.length===0?`${prefix}[]`:output.map(item=>_formatOutput(item,indent)).join(`
5
5
  `);if(typeof output=="object"){let lines=[];for(let[key,value]of Object.entries(output))value!==void 0&&(typeof value=="object"&&value!==null&&!Array.isArray(value)?(lines.push(`${prefix}${key}:`),lines.push(_formatOutput(value,indent+1))):Array.isArray(value)?(lines.push(`${prefix}${key}:`),lines.push(_formatOutput(value,indent+1))):lines.push(`${prefix}${key}: ${value}`));return lines.join(`
@@ -330,7 +330,7 @@ Examples:
330
330
  - "JSON.stringify(config)"
331
331
 
332
332
  Watch expressions are evaluated in the context of the paused frame.
333
- `}inputSchema(){return{expression:z18.string().describe("Expression evaluated at every tracepoint hit (e.g. user.name).")}}outputSchema(){return{id:z18.string().describe("Watch expression ID"),expression:z18.string().describe("The watch expression"),message:z18.string().describe("Status message")}}async handle(context,args){isDebuggingEnabled2(context.browserContext)||await enableDebugging2(context.browserContext,context.page);let watch=addWatchExpression2(context.browserContext,args.expression);return{id:watch.id,expression:watch.expression,message:`Watch expression added: ${args.expression}`}}};var tools3=[new Status,new ResolveSourceLocation,new PutTracepoint,new RemoveProbe,new ListProbes,new ClearProbes,new PutLogpoint,new PutExceptionpoint,new GetProbeSnapshots,new ClearProbeSnapshots,new AddWatch];import sharp from"sharp";import ssim from"ssim.js";var DEFAULT_GRAYSCALE=!1,DEFAULT_BLUR=10;function _clamp01(v){return Number.isFinite(v)?Math.max(0,Math.min(1,v)):0}async function _loadNormalized(input,canvasWidth,canvasHeight,mode){let img;if(mode==="semantic"){img=sharp(input).resize(canvasWidth,canvasHeight,{fit:"cover",position:"centre"});let w=Math.max(1,Math.floor(canvasWidth/2)),h=Math.max(1,Math.floor(canvasHeight/2));img=img.resize(w,h,{fit:"cover",position:"centre"}).grayscale(DEFAULT_GRAYSCALE).blur(DEFAULT_BLUR)}else img=sharp(input).resize(canvasWidth,canvasHeight,{fit:"cover",position:"centre"});let out=await img.ensureAlpha().raw().toBuffer({resolveWithObject:!0});return{data:new Uint8ClampedArray(out.data.buffer,out.data.byteOffset,out.data.byteLength),width:out.info.width,height:out.info.height}}function _computeSsim(a,b){let details=ssim({data:a.data,width:a.width,height:a.height},{data:b.data,width:b.width,height:b.height}),rawScore=Number(details.mssim??details.ssim??0);return _clamp01(rawScore)}async function compare(page,figma,options){let mode=options?.mode??"semantic",canvasWidth=options?.canvasWidth,canvasHeight=options?.canvasHeight;if(typeof canvasWidth!="number"||!Number.isFinite(canvasWidth)||canvasWidth<=0||typeof canvasHeight!="number"||!Number.isFinite(canvasHeight)||canvasHeight<=0){let figmaMeta=await sharp(figma.image).metadata();if(canvasWidth=figmaMeta.width??0,canvasHeight=figmaMeta.height??0,canvasWidth<=0||canvasHeight<=0)throw new Error("Failed to read Figma image dimensions.")}let figmaImg=await _loadNormalized(figma.image,canvasWidth,canvasHeight,mode),pageImg=await _loadNormalized(page.image,canvasWidth,canvasHeight,mode);return{score:_computeSsim(figmaImg,pageImg)}}function dot(a,b){let n=Math.min(a.length,b.length),s=0;for(let i=0;i<n;i++)s+=a[i]*b[i];return s}function norm(a){let s=0;for(let i=0;i<a.length;i++){let x=a[i];s+=x*x}return Math.sqrt(s)}function l2Normalize(v){let n=norm(v);if(n===0)return v.slice();let out=new Array(v.length);for(let i=0;i<v.length;i++)out[i]=v[i]/n;return out}function cosineSimilarity(a,b,normalize){if(normalize){let na=l2Normalize(a),nb=l2Normalize(b);return dot(na,nb)}let denom=norm(a)*norm(b);return denom===0?0:dot(a,b)/denom}import{BedrockRuntimeClient,InvokeModelCommand}from"@aws-sdk/client-bedrock-runtime";import{fromIni}from"@aws-sdk/credential-providers";import sharp2 from"sharp";var DEFAULT_MAX_DIMENSION=1024,DEFAULT_JPEG_QUALITY=90,DEFAULT_AMAZON_BEDROCK_TITAN_OUTPUT_EMBEDDING_LENGTH=1024;async function _prepareImage(buf,imageType,opt){let img=sharp2(buf),maxDim=opt?.maxDim||DEFAULT_MAX_DIMENSION;if(img=img.resize({width:maxDim,height:maxDim,fit:"inside",withoutEnlargement:!0}),imageType==="png")return await img.png().toBuffer();let jpegQuality=opt?.jpegQuality||DEFAULT_JPEG_QUALITY;return await img.jpeg({quality:jpegQuality}).toBuffer()}var SUPPORTED_AMAZON_BEDROCK_IMAGE_EMBED_MODEL_IDS=new Set(["amazon.titan-embed-image-v1"]),DEFAULT_AMAZON_BEDROCK_IMAGE_EMBED_MODEL_ID="amazon.titan-embed-image-v1",_bedrockClient;function _isAwsBedrockActive(){return AMAZON_BEDROCK_ENABLE&&!!AWS_REGION}function _getOrCreateBedrockClient(){if(_bedrockClient)return _bedrockClient;let region=AWS_REGION;if(!region)return;let profile=AWS_PROFILE;return profile?(_bedrockClient=new BedrockRuntimeClient({region,credentials:fromIni({profile})}),_bedrockClient):(_bedrockClient=new BedrockRuntimeClient({region}),_bedrockClient)}async function _embedImageWithAmazonBedrockTitan(ss,client,opt,modelId){let bodyObj={inputImage:(await _prepareImage(ss.image,ss.type,opt)).toString("base64"),embeddingConfig:{outputEmbeddingLength:DEFAULT_AMAZON_BEDROCK_TITAN_OUTPUT_EMBEDDING_LENGTH}},cmd=new InvokeModelCommand({modelId,contentType:"application/json",accept:"application/json",body:Buffer.from(JSON.stringify(bodyObj),"utf-8")}),resp=await client.send(cmd),raw=resp?.body instanceof Uint8Array?resp.body:new Uint8Array(resp?.body??[]),text=Buffer.from(raw).toString("utf-8"),parsed;try{parsed=text?JSON.parse(text):{}}catch{throw new Error(`Amazon Bedrock Titan returned non-JSON response for embeddings: ${text.slice(0,300)}`)}let emb=parsed?.embedding??parsed?.embeddings?.[0]??parsed?.outputEmbedding??parsed?.vector;if(!Array.isArray(emb)||emb.length===0||typeof emb[0]!="number")throw new Error(`Unexpected Amazon Bedrock Titan image embedding response format: ${text.slice(0,500)}`);return emb}async function _embedImageWithAmazonBedrock(ss,opt,client){let modelId=opt?.modelId??AMAZON_BEDROCK_IMAGE_EMBED_MODEL_ID??DEFAULT_AMAZON_BEDROCK_IMAGE_EMBED_MODEL_ID;if(!SUPPORTED_AMAZON_BEDROCK_IMAGE_EMBED_MODEL_IDS.has(modelId))throw new Error(`Unsupported Amazon Bedrock image embedding model id: ${modelId}`);return await _embedImageWithAmazonBedrockTitan(ss,client,opt,modelId)}async function _embedImage(ss,opt){if(_isAwsBedrockActive()){let client=_getOrCreateBedrockClient();return client?_embedImageWithAmazonBedrock(ss,opt,client):void 0}}async function compare2(page,figma,options){let normalize=typeof options?.normalize=="boolean"?options.normalize:!0,figmaVec=await _embedImage(figma,options);if(!figmaVec)return;let pageVec=await _embedImage(page,options);return pageVec?{score:cosineSimilarity(figmaVec,pageVec,normalize)}:void 0}import{BedrockRuntimeClient as BedrockRuntimeClient2,InvokeModelCommand as InvokeModelCommand2}from"@aws-sdk/client-bedrock-runtime";import{fromIni as fromIni2}from"@aws-sdk/credential-providers";import sharp3 from"sharp";var UI_DESCRIBE_PROMPT=`
333
+ `}inputSchema(){return{expression:z18.string().describe("Expression evaluated at every tracepoint hit (e.g. user.name).")}}outputSchema(){return{id:z18.string().describe("Watch expression ID"),expression:z18.string().describe("The watch expression"),message:z18.string().describe("Status message")}}async handle(context,args){isDebuggingEnabled2(context.browserContext)||await enableDebugging2(context.browserContext,context.page);let watch=addWatchExpression2(context.browserContext,args.expression);return{id:watch.id,expression:watch.expression,message:`Watch expression added: ${args.expression}`}}};var tools3=[new Status,new ResolveSourceLocation,new PutTracepoint,new RemoveProbe,new ListProbes,new ClearProbes,new PutLogpoint,new PutExceptionpoint,new GetProbeSnapshots,new ClearProbeSnapshots,new AddWatch];import ssim from"ssim.js";var _sharpPromise=null;async function getSharp(){return _sharpPromise||(_sharpPromise=import("sharp").then(m=>m.default??m)),_sharpPromise}var DEFAULT_GRAYSCALE=!1,DEFAULT_BLUR=10;function _clamp01(v){return Number.isFinite(v)?Math.max(0,Math.min(1,v)):0}async function _loadNormalized(input,canvasWidth,canvasHeight,mode){let sharp=await getSharp(),img;if(mode==="semantic"){img=sharp(input).resize(canvasWidth,canvasHeight,{fit:"cover",position:"centre"});let w=Math.max(1,Math.floor(canvasWidth/2)),h=Math.max(1,Math.floor(canvasHeight/2));img=img.resize(w,h,{fit:"cover",position:"centre"}).grayscale(DEFAULT_GRAYSCALE).blur(DEFAULT_BLUR)}else img=sharp(input).resize(canvasWidth,canvasHeight,{fit:"cover",position:"centre"});let out=await img.ensureAlpha().raw().toBuffer({resolveWithObject:!0});return{data:new Uint8ClampedArray(out.data.buffer,out.data.byteOffset,out.data.byteLength),width:out.info.width,height:out.info.height}}function _computeSsim(a,b){let details=ssim({data:a.data,width:a.width,height:a.height},{data:b.data,width:b.width,height:b.height}),rawScore=Number(details.mssim??details.ssim??0);return _clamp01(rawScore)}async function compare(page,figma,options){let mode=options?.mode??"semantic",canvasWidth=options?.canvasWidth,canvasHeight=options?.canvasHeight;if(typeof canvasWidth!="number"||!Number.isFinite(canvasWidth)||canvasWidth<=0||typeof canvasHeight!="number"||!Number.isFinite(canvasHeight)||canvasHeight<=0){let figmaMeta=await(await getSharp())(figma.image).metadata();if(canvasWidth=figmaMeta.width??0,canvasHeight=figmaMeta.height??0,canvasWidth<=0||canvasHeight<=0)throw new Error("Failed to read Figma image dimensions.")}let figmaImg=await _loadNormalized(figma.image,canvasWidth,canvasHeight,mode),pageImg=await _loadNormalized(page.image,canvasWidth,canvasHeight,mode);return{score:_computeSsim(figmaImg,pageImg)}}function dot(a,b){let n=Math.min(a.length,b.length),s=0;for(let i=0;i<n;i++)s+=a[i]*b[i];return s}function norm(a){let s=0;for(let i=0;i<a.length;i++){let x=a[i];s+=x*x}return Math.sqrt(s)}function l2Normalize(v){let n=norm(v);if(n===0)return v.slice();let out=new Array(v.length);for(let i=0;i<v.length;i++)out[i]=v[i]/n;return out}function cosineSimilarity(a,b,normalize){if(normalize){let na=l2Normalize(a),nb=l2Normalize(b);return dot(na,nb)}let denom=norm(a)*norm(b);return denom===0?0:dot(a,b)/denom}import{BedrockRuntimeClient,InvokeModelCommand}from"@aws-sdk/client-bedrock-runtime";import{fromIni}from"@aws-sdk/credential-providers";var _sharpPromise2=null;async function getSharp2(){return _sharpPromise2||(_sharpPromise2=import("sharp").then(m=>m.default??m)),_sharpPromise2}var DEFAULT_MAX_DIMENSION=1024,DEFAULT_JPEG_QUALITY=90,DEFAULT_AMAZON_BEDROCK_TITAN_OUTPUT_EMBEDDING_LENGTH=1024;async function _prepareImage(buf,imageType,opt){let img=(await getSharp2())(buf),maxDim=opt?.maxDim||DEFAULT_MAX_DIMENSION;if(img=img.resize({width:maxDim,height:maxDim,fit:"inside",withoutEnlargement:!0}),imageType==="png")return await img.png().toBuffer();let jpegQuality=opt?.jpegQuality||DEFAULT_JPEG_QUALITY;return await img.jpeg({quality:jpegQuality}).toBuffer()}var SUPPORTED_AMAZON_BEDROCK_IMAGE_EMBED_MODEL_IDS=new Set(["amazon.titan-embed-image-v1"]),DEFAULT_AMAZON_BEDROCK_IMAGE_EMBED_MODEL_ID="amazon.titan-embed-image-v1",_bedrockClient;function _isAwsBedrockActive(){return AMAZON_BEDROCK_ENABLE&&!!AWS_REGION}function _getOrCreateBedrockClient(){if(_bedrockClient)return _bedrockClient;let region=AWS_REGION;if(!region)return;let profile=AWS_PROFILE;return profile?(_bedrockClient=new BedrockRuntimeClient({region,credentials:fromIni({profile})}),_bedrockClient):(_bedrockClient=new BedrockRuntimeClient({region}),_bedrockClient)}async function _embedImageWithAmazonBedrockTitan(ss,client,opt,modelId){let bodyObj={inputImage:(await _prepareImage(ss.image,ss.type,opt)).toString("base64"),embeddingConfig:{outputEmbeddingLength:DEFAULT_AMAZON_BEDROCK_TITAN_OUTPUT_EMBEDDING_LENGTH}},cmd=new InvokeModelCommand({modelId,contentType:"application/json",accept:"application/json",body:Buffer.from(JSON.stringify(bodyObj),"utf-8")}),resp=await client.send(cmd),raw=resp?.body instanceof Uint8Array?resp.body:new Uint8Array(resp?.body??[]),text=Buffer.from(raw).toString("utf-8"),parsed;try{parsed=text?JSON.parse(text):{}}catch{throw new Error(`Amazon Bedrock Titan returned non-JSON response for embeddings: ${text.slice(0,300)}`)}let emb=parsed?.embedding??parsed?.embeddings?.[0]??parsed?.outputEmbedding??parsed?.vector;if(!Array.isArray(emb)||emb.length===0||typeof emb[0]!="number")throw new Error(`Unexpected Amazon Bedrock Titan image embedding response format: ${text.slice(0,500)}`);return emb}async function _embedImageWithAmazonBedrock(ss,opt,client){let modelId=opt?.modelId??AMAZON_BEDROCK_IMAGE_EMBED_MODEL_ID??DEFAULT_AMAZON_BEDROCK_IMAGE_EMBED_MODEL_ID;if(!SUPPORTED_AMAZON_BEDROCK_IMAGE_EMBED_MODEL_IDS.has(modelId))throw new Error(`Unsupported Amazon Bedrock image embedding model id: ${modelId}`);return await _embedImageWithAmazonBedrockTitan(ss,client,opt,modelId)}async function _embedImage(ss,opt){if(_isAwsBedrockActive()){let client=_getOrCreateBedrockClient();return client?_embedImageWithAmazonBedrock(ss,opt,client):void 0}}async function compare2(page,figma,options){let normalize=typeof options?.normalize=="boolean"?options.normalize:!0,figmaVec=await _embedImage(figma,options);if(!figmaVec)return;let pageVec=await _embedImage(page,options);return pageVec?{score:cosineSimilarity(figmaVec,pageVec,normalize)}:void 0}import{BedrockRuntimeClient as BedrockRuntimeClient2,InvokeModelCommand as InvokeModelCommand2}from"@aws-sdk/client-bedrock-runtime";import{fromIni as fromIni2}from"@aws-sdk/credential-providers";var _sharpPromise3=null;async function getSharp3(){return _sharpPromise3||(_sharpPromise3=import("sharp").then(m=>m.default??m)),_sharpPromise3}var UI_DESCRIBE_PROMPT=`
334
334
  You are analyzing a UI screenshot to compare it against another UI.
335
335
 
336
336
  Your goal is to produce a STRUCTURAL LAYOUT FINGERPRINT that remains stable
@@ -392,7 +392,7 @@ FORMAT
392
392
  - Keep the output under ~30 lines.
393
393
 
394
394
  Return plain text only. No markdown.
395
- `;function _resolvePrompt(opt){return opt?.prompt??UI_DESCRIBE_PROMPT.trim()}function _resolveMaxDim(opt){return typeof opt?.maxDim=="number"&&opt.maxDim>0?Math.floor(opt.maxDim):1024}function _resolveImageFormat(opt){return opt?.imageFormat==="jpeg"?"jpeg":"png"}function _resolveJpegQuality(opt){let q=opt?.jpegQuality;return typeof q=="number"&&q>=50&&q<=100?Math.floor(q):90}async function _preprocessImage(buf,opt){let maxDim=_resolveMaxDim(opt),format=_resolveImageFormat(opt),jpegQuality=_resolveJpegQuality(opt),img=sharp3(buf).resize({width:maxDim,height:maxDim,fit:"inside",withoutEnlargement:!0}),out,mimeType;return format==="png"?(out=await img.png().toBuffer(),mimeType="image/png"):(out=await img.jpeg({quality:jpegQuality}).toBuffer(),mimeType="image/jpeg"),{bytes:out,mimeType}}var SUPPORTED_AMAZON_BEDROCK_TEXT_EMBED_MODEL_IDS=new Set(["amazon.titan-embed-text-v2:0"]),DEFAULT_AMAZON_BEDROCK_TEXT_EMBED_MODEL_ID="amazon.titan-embed-text-v2:0",SUPPORTED_AMAZON_BEDROCK_VISION_MODEL_IDS=new Set(["anthropic.claude-3-haiku-20240307-v1","anthropic.claude-3-sonnet-20240229-v1:0","anthropic.claude-3-5-sonnet-20241022-v2:0","anthropic.claude-3-7-sonnet-20250219-v1:0","anthropic.claude-3-opus-20240229-v1:0","anthropic.claude-haiku-4-5-20251001-v1:0","anthropic.claude-opus-4-1-20250805-v1:0","anthropic.claude-opus-4-5-20251101-v1:0"]),DEFAULT_AMAZON_BEDROCK_VISION_MODEL_ID="anthropic.claude-3-sonnet-20240229-v1:0",_bedrockClient2;function _isAwsBedrockActive2(){return AMAZON_BEDROCK_ENABLE&&!!AWS_REGION}function _getOrCreateBedrockClient2(){if(_bedrockClient2)return _bedrockClient2;let region=AWS_REGION;if(!region)return;let profile=AWS_PROFILE;return profile?(_bedrockClient2=new BedrockRuntimeClient2({region,credentials:fromIni2({profile})}),_bedrockClient2):(_bedrockClient2=new BedrockRuntimeClient2({region}),_bedrockClient2)}async function _invokeBedrock(client,modelId,payload){let cmd=new InvokeModelCommand2({modelId,contentType:"application/json",accept:"application/json",body:Buffer.from(JSON.stringify(payload))}),resp=await client.send(cmd),raw=Buffer.from(resp.body).toString("utf-8");return JSON.parse(raw)}async function _describeUIWithAmazonBedrockClaude(ss,opt,client,modelId){let{bytes,mimeType}=await _preprocessImage(ss.image,opt),payload={anthropic_version:"bedrock-2023-05-31",max_tokens:1e4,temperature:0,messages:[{role:"user",content:[{type:"text",text:_resolvePrompt(opt)},{type:"image",source:{type:"base64",media_type:mimeType,data:bytes.toString("base64")}}]}]},parsed=await _invokeBedrock(client,modelId,payload),text=parsed?.content?.[0]?.text??parsed?.output_text??parsed?.completion;if(!text||!text.trim())throw new Error("Amazon Bedrock Claude returned empty description.");return text.trim()}async function _embedTextWithAmazonBedrockTitan(text,client,modelId){let emb=(await _invokeBedrock(client,modelId,{inputText:text}))?.embedding;if(!Array.isArray(emb)||typeof emb[0]!="number")throw new Error("Unexpected embedding response for Amazon Bedrock Titan text embedding.");return emb}async function _describeUIWithAmazonBedrock(ss,opt,client){let modelId=opt?.visionModelId??AMAZON_BEDROCK_VISION_MODEL_ID??DEFAULT_AMAZON_BEDROCK_VISION_MODEL_ID;if(!SUPPORTED_AMAZON_BEDROCK_VISION_MODEL_IDS.has(modelId))throw new Error(`Unsupported Amazon Bedrock vision model id: ${modelId}`);return await _describeUIWithAmazonBedrockClaude(ss,opt,client,modelId)}async function _embedTextWithAmazonBedrock(text,opt,client){let modelId=opt?.textEmbedModelId??AMAZON_BEDROCK_TEXT_EMBED_MODEL_ID??DEFAULT_AMAZON_BEDROCK_TEXT_EMBED_MODEL_ID;if(!SUPPORTED_AMAZON_BEDROCK_TEXT_EMBED_MODEL_IDS.has(modelId))throw new Error(`Unsupported Amazon Bedrock text embedding model id: ${modelId}`);return await _embedTextWithAmazonBedrockTitan(text,client,modelId)}async function _describeUI(ss,opt){if(_isAwsBedrockActive2()){let client=_getOrCreateBedrockClient2();return client?_describeUIWithAmazonBedrock(ss,opt,client):void 0}}async function _embedTextVector(text,opt){if(_isAwsBedrockActive2()){let client=_getOrCreateBedrockClient2();return client?_embedTextWithAmazonBedrock(text,opt,client):void 0}}async function compare3(page,figma,options){let normalize=typeof options?.normalize=="boolean"?options.normalize:!0,figmaDesc=await _describeUI(figma,options);if(!figmaDesc)return;let pageDesc=await _describeUI(page,options);if(!pageDesc)return;let figmaVec=await _embedTextVector(figmaDesc,options);if(!figmaVec)return;let pageVec=await _embedTextVector(pageDesc,options);return pageVec?{score:cosineSimilarity(figmaVec,pageVec,normalize)}:void 0}var DEFAULT_MSSIM_WEIGHT=.25,DEFAULT_VECTOR_EMBEDDING_WEIGHT=.5,DEFAULT_TEXT_EMBEDDING_WEIGHT=.25;function _clamp012(v){return Number.isFinite(v)?Math.max(0,Math.min(1,v)):0}function _weightOrDefault(v,def){return typeof v=="number"&&Number.isFinite(v)&&v>0?v:def}async function compareWithNotes(page,figma,options){let notes=[],wMssim=_weightOrDefault(options?.weights?.mssim,DEFAULT_MSSIM_WEIGHT),wVector=_weightOrDefault(options?.weights?.vectorEmbedding,DEFAULT_VECTOR_EMBEDDING_WEIGHT),wText=_weightOrDefault(options?.weights?.textEmbedding,DEFAULT_TEXT_EMBEDDING_WEIGHT),mssimRes=await compare(page,figma,options?.mssim),mssimScore=_clamp012(mssimRes.score);notes.push(`mssim=${mssimScore.toFixed(5)}`);let imageScore;try{let res=await compare2(page,figma,options?.imageEmbedding);res&&typeof res.score=="number"?(imageScore=_clamp012(res.score),notes.push(`image-embedding=${imageScore.toFixed(5)}`)):notes.push("image-embedding=skipped (inactive)")}catch(err){notes.push(`image-embedding=skipped (${err instanceof Error?err.message:String(err)})`)}let textScore;try{let res=await compare3(page,figma,options?.textEmbedding);res&&typeof res.score=="number"?(textScore=_clamp012(res.score),notes.push(`text-embedding=${textScore.toFixed(5)}`)):notes.push("text-embedding=skipped (inactive)")}catch(err){notes.push(`text-embedding=skipped (${err instanceof Error?err.message:String(err)})`)}let parts=[{name:"mssim",score:mssimScore,weight:wMssim}];typeof imageScore=="number"&&parts.push({name:"image-embedding",score:imageScore,weight:wVector}),typeof textScore=="number"&&parts.push({name:"text-embedding",score:textScore,weight:wText});let totalWeight=parts.reduce((s,p)=>s+p.weight,0),combined=totalWeight>0?parts.reduce((s,p)=>s+p.score*(p.weight/totalWeight),0):0,score=_clamp012(combined);return notes.push(`combined=${score.toFixed(5)} (signals=${parts.map(p=>p.name).join(", ")})`),{score,notes}}import crypto2 from"node:crypto";function _requireFigmaToken(){let token=FIGMA_ACCESS_TOKEN;if(!token)throw new Error("No Figma access token configured");return token}function _mimeTypeFor(format){return format==="jpg"?{mimeType:"image/jpeg",type:"jpeg"}:{mimeType:"image/png",type:"png"}}function _buildCacheKey(req){let h=crypto2.createHash("sha256");return h.update(req.fileKey),h.update("|"),h.update(req.nodeId),h.update("|"),h.update(req.format??"png"),h.update("|"),h.update(String(req.scale??2)),h.digest("hex").slice(0,24)}async function _fetchJson(url,token){let res=await fetch(url,{method:"GET",headers:{"X-Figma-Token":token,Accept:"application/json"}}),text=await res.text(),json;try{json=text?JSON.parse(text):{}}catch{throw new Error(`Figma API returned non-JSON response (status=${res.status}). Body: ${text.slice(0,500)}`)}if(!res.ok){let msg=typeof json?.err=="string"?json.err:`Figma API error (status=${res.status})`;throw new Error(msg)}return json}async function _fetchBinary(url){let res=await fetch(url,{method:"GET"});if(!res.ok){let t=await res.text().catch(()=>"");throw new Error(`Failed to download Figma rendered image (status=${res.status}): ${t.slice(0,300)}`)}let ab=await res.arrayBuffer();return Buffer.from(ab)}async function getFigmaDesignScreenshot(req){let token=_requireFigmaToken(),format=req.format??"png",scale=typeof req.scale=="number"&&req.scale>0?req.scale:2,{mimeType,type}=_mimeTypeFor(format),base=FIGMA_API_BASE_URL,fileKey=req.fileKey,nodeId=req.nodeId,url=`${base}/images/${encodeURIComponent(fileKey)}?ids=${encodeURIComponent(nodeId)}&format=${encodeURIComponent(format)}&scale=${encodeURIComponent(String(scale))}`,imgResp=await _fetchJson(url,token),imageUrl=imgResp.images?.[nodeId];if(!imageUrl){let err=typeof imgResp.err=="string"&&imgResp.err.trim()?imgResp.err:"Figma did not return an image URL for the given nodeId.";throw new Error(err)}let image=await _fetchBinary(imageUrl),cacheKey=_buildCacheKey(req),out={image,mimeType,type,cacheKey};return req.includeId===!0&&(out.nodeId=nodeId,out.fileKey=fileKey),out}import{z as z19}from"zod";var DEFAULT_SCREENSHOT_TYPE2="png",DEFAULT_FULL_PAGE=!0,DEFAULT_MSSIM_MODE="semantic",ComparePageWithDesign=class{name(){return"figma_compare-page-with-design"}description(){return`
395
+ `;function _resolvePrompt(opt){return opt?.prompt??UI_DESCRIBE_PROMPT.trim()}function _resolveMaxDim(opt){return typeof opt?.maxDim=="number"&&opt.maxDim>0?Math.floor(opt.maxDim):1024}function _resolveImageFormat(opt){return opt?.imageFormat==="jpeg"?"jpeg":"png"}function _resolveJpegQuality(opt){let q=opt?.jpegQuality;return typeof q=="number"&&q>=50&&q<=100?Math.floor(q):90}async function _preprocessImage(buf,opt){let maxDim=_resolveMaxDim(opt),format=_resolveImageFormat(opt),jpegQuality=_resolveJpegQuality(opt),img=(await getSharp3())(buf).resize({width:maxDim,height:maxDim,fit:"inside",withoutEnlargement:!0}),out,mimeType;return format==="png"?(out=await img.png().toBuffer(),mimeType="image/png"):(out=await img.jpeg({quality:jpegQuality}).toBuffer(),mimeType="image/jpeg"),{bytes:out,mimeType}}var SUPPORTED_AMAZON_BEDROCK_TEXT_EMBED_MODEL_IDS=new Set(["amazon.titan-embed-text-v2:0"]),DEFAULT_AMAZON_BEDROCK_TEXT_EMBED_MODEL_ID="amazon.titan-embed-text-v2:0",SUPPORTED_AMAZON_BEDROCK_VISION_MODEL_IDS=new Set(["anthropic.claude-3-haiku-20240307-v1","anthropic.claude-3-sonnet-20240229-v1:0","anthropic.claude-3-5-sonnet-20241022-v2:0","anthropic.claude-3-7-sonnet-20250219-v1:0","anthropic.claude-3-opus-20240229-v1:0","anthropic.claude-haiku-4-5-20251001-v1:0","anthropic.claude-opus-4-1-20250805-v1:0","anthropic.claude-opus-4-5-20251101-v1:0"]),DEFAULT_AMAZON_BEDROCK_VISION_MODEL_ID="anthropic.claude-3-sonnet-20240229-v1:0",_bedrockClient2;function _isAwsBedrockActive2(){return AMAZON_BEDROCK_ENABLE&&!!AWS_REGION}function _getOrCreateBedrockClient2(){if(_bedrockClient2)return _bedrockClient2;let region=AWS_REGION;if(!region)return;let profile=AWS_PROFILE;return profile?(_bedrockClient2=new BedrockRuntimeClient2({region,credentials:fromIni2({profile})}),_bedrockClient2):(_bedrockClient2=new BedrockRuntimeClient2({region}),_bedrockClient2)}async function _invokeBedrock(client,modelId,payload){let cmd=new InvokeModelCommand2({modelId,contentType:"application/json",accept:"application/json",body:Buffer.from(JSON.stringify(payload))}),resp=await client.send(cmd),raw=Buffer.from(resp.body).toString("utf-8");return JSON.parse(raw)}async function _describeUIWithAmazonBedrockClaude(ss,opt,client,modelId){let{bytes,mimeType}=await _preprocessImage(ss.image,opt),payload={anthropic_version:"bedrock-2023-05-31",max_tokens:1e4,temperature:0,messages:[{role:"user",content:[{type:"text",text:_resolvePrompt(opt)},{type:"image",source:{type:"base64",media_type:mimeType,data:bytes.toString("base64")}}]}]},parsed=await _invokeBedrock(client,modelId,payload),text=parsed?.content?.[0]?.text??parsed?.output_text??parsed?.completion;if(!text||!text.trim())throw new Error("Amazon Bedrock Claude returned empty description.");return text.trim()}async function _embedTextWithAmazonBedrockTitan(text,client,modelId){let emb=(await _invokeBedrock(client,modelId,{inputText:text}))?.embedding;if(!Array.isArray(emb)||typeof emb[0]!="number")throw new Error("Unexpected embedding response for Amazon Bedrock Titan text embedding.");return emb}async function _describeUIWithAmazonBedrock(ss,opt,client){let modelId=opt?.visionModelId??AMAZON_BEDROCK_VISION_MODEL_ID??DEFAULT_AMAZON_BEDROCK_VISION_MODEL_ID;if(!SUPPORTED_AMAZON_BEDROCK_VISION_MODEL_IDS.has(modelId))throw new Error(`Unsupported Amazon Bedrock vision model id: ${modelId}`);return await _describeUIWithAmazonBedrockClaude(ss,opt,client,modelId)}async function _embedTextWithAmazonBedrock(text,opt,client){let modelId=opt?.textEmbedModelId??AMAZON_BEDROCK_TEXT_EMBED_MODEL_ID??DEFAULT_AMAZON_BEDROCK_TEXT_EMBED_MODEL_ID;if(!SUPPORTED_AMAZON_BEDROCK_TEXT_EMBED_MODEL_IDS.has(modelId))throw new Error(`Unsupported Amazon Bedrock text embedding model id: ${modelId}`);return await _embedTextWithAmazonBedrockTitan(text,client,modelId)}async function _describeUI(ss,opt){if(_isAwsBedrockActive2()){let client=_getOrCreateBedrockClient2();return client?_describeUIWithAmazonBedrock(ss,opt,client):void 0}}async function _embedTextVector(text,opt){if(_isAwsBedrockActive2()){let client=_getOrCreateBedrockClient2();return client?_embedTextWithAmazonBedrock(text,opt,client):void 0}}async function compare3(page,figma,options){let normalize=typeof options?.normalize=="boolean"?options.normalize:!0,figmaDesc=await _describeUI(figma,options);if(!figmaDesc)return;let pageDesc=await _describeUI(page,options);if(!pageDesc)return;let figmaVec=await _embedTextVector(figmaDesc,options);if(!figmaVec)return;let pageVec=await _embedTextVector(pageDesc,options);return pageVec?{score:cosineSimilarity(figmaVec,pageVec,normalize)}:void 0}var DEFAULT_MSSIM_WEIGHT=.25,DEFAULT_VECTOR_EMBEDDING_WEIGHT=.5,DEFAULT_TEXT_EMBEDDING_WEIGHT=.25;function _clamp012(v){return Number.isFinite(v)?Math.max(0,Math.min(1,v)):0}function _weightOrDefault(v,def){return typeof v=="number"&&Number.isFinite(v)&&v>0?v:def}async function compareWithNotes(page,figma,options){let notes=[],wMssim=_weightOrDefault(options?.weights?.mssim,DEFAULT_MSSIM_WEIGHT),wVector=_weightOrDefault(options?.weights?.vectorEmbedding,DEFAULT_VECTOR_EMBEDDING_WEIGHT),wText=_weightOrDefault(options?.weights?.textEmbedding,DEFAULT_TEXT_EMBEDDING_WEIGHT),mssimRes=await compare(page,figma,options?.mssim),mssimScore=_clamp012(mssimRes.score);notes.push(`mssim=${mssimScore.toFixed(5)}`);let imageScore;try{let res=await compare2(page,figma,options?.imageEmbedding);res&&typeof res.score=="number"?(imageScore=_clamp012(res.score),notes.push(`image-embedding=${imageScore.toFixed(5)}`)):notes.push("image-embedding=skipped (inactive)")}catch(err){notes.push(`image-embedding=skipped (${err instanceof Error?err.message:String(err)})`)}let textScore;try{let res=await compare3(page,figma,options?.textEmbedding);res&&typeof res.score=="number"?(textScore=_clamp012(res.score),notes.push(`text-embedding=${textScore.toFixed(5)}`)):notes.push("text-embedding=skipped (inactive)")}catch(err){notes.push(`text-embedding=skipped (${err instanceof Error?err.message:String(err)})`)}let parts=[{name:"mssim",score:mssimScore,weight:wMssim}];typeof imageScore=="number"&&parts.push({name:"image-embedding",score:imageScore,weight:wVector}),typeof textScore=="number"&&parts.push({name:"text-embedding",score:textScore,weight:wText});let totalWeight=parts.reduce((s,p)=>s+p.weight,0),combined=totalWeight>0?parts.reduce((s,p)=>s+p.score*(p.weight/totalWeight),0):0,score=_clamp012(combined);return notes.push(`combined=${score.toFixed(5)} (signals=${parts.map(p=>p.name).join(", ")})`),{score,notes}}import crypto2 from"node:crypto";function _requireFigmaToken(){let token=FIGMA_ACCESS_TOKEN;if(!token)throw new Error("No Figma access token configured");return token}function _mimeTypeFor(format){return format==="jpg"?{mimeType:"image/jpeg",type:"jpeg"}:{mimeType:"image/png",type:"png"}}function _buildCacheKey(req){let h=crypto2.createHash("sha256");return h.update(req.fileKey),h.update("|"),h.update(req.nodeId),h.update("|"),h.update(req.format??"png"),h.update("|"),h.update(String(req.scale??2)),h.digest("hex").slice(0,24)}async function _fetchJson(url,token){let res=await fetch(url,{method:"GET",headers:{"X-Figma-Token":token,Accept:"application/json"}}),text=await res.text(),json;try{json=text?JSON.parse(text):{}}catch{throw new Error(`Figma API returned non-JSON response (status=${res.status}). Body: ${text.slice(0,500)}`)}if(!res.ok){let msg=typeof json?.err=="string"?json.err:`Figma API error (status=${res.status})`;throw new Error(msg)}return json}async function _fetchBinary(url){let res=await fetch(url,{method:"GET"});if(!res.ok){let t=await res.text().catch(()=>"");throw new Error(`Failed to download Figma rendered image (status=${res.status}): ${t.slice(0,300)}`)}let ab=await res.arrayBuffer();return Buffer.from(ab)}async function getFigmaDesignScreenshot(req){let token=_requireFigmaToken(),format=req.format??"png",scale=typeof req.scale=="number"&&req.scale>0?req.scale:2,{mimeType,type}=_mimeTypeFor(format),base=FIGMA_API_BASE_URL,fileKey=req.fileKey,nodeId=req.nodeId,url=`${base}/images/${encodeURIComponent(fileKey)}?ids=${encodeURIComponent(nodeId)}&format=${encodeURIComponent(format)}&scale=${encodeURIComponent(String(scale))}`,imgResp=await _fetchJson(url,token),imageUrl=imgResp.images?.[nodeId];if(!imageUrl){let err=typeof imgResp.err=="string"&&imgResp.err.trim()?imgResp.err:"Figma did not return an image URL for the given nodeId.";throw new Error(err)}let image=await _fetchBinary(imageUrl),cacheKey=_buildCacheKey(req),out={image,mimeType,type,cacheKey};return req.includeId===!0&&(out.nodeId=nodeId,out.fileKey=fileKey),out}import{z as z19}from"zod";var DEFAULT_SCREENSHOT_TYPE2="png",DEFAULT_FULL_PAGE=!0,DEFAULT_MSSIM_MODE="semantic",ComparePageWithDesign=class{name(){return"figma_compare-page-with-design"}description(){return`
396
396
  Compares the CURRENT PAGE UI against a Figma design snapshot and returns a combined similarity score.
397
397
 
398
398
  What this tool does:
@@ -1 +1 @@
1
- import{Execute,init,shutdown,trackToolCalled}from"./core-UNMLWWDL.js";import{ToolRegistry,isToolEnabled,platformInfo}from"./core-RE2PLB4I.js";import{AVAILABLE_TOOL_DOMAINS,DAEMON_PORT,DAEMON_SESSION_IDLE_CHECK_SECONDS,DAEMON_SESSION_IDLE_SECONDS,debug,enable,error,info,isDebugEnabled}from"./core-3YBKJFSF.js";import{createRequire}from"node:module";import{Command,Option,InvalidOptionArgumentError}from"commander";import{serve}from"@hono/node-server";import{Hono}from"hono";import{cors}from"hono/cors";import{z}from"zod";var require2=createRequire(import.meta.url),daemonStartTime=0,daemonPort=0,app=new Hono,sessions=new Map,DEFAULT_SESSION_ID="#default",ERRORS={get sessionNotFound(){return _buildErrorResponse(404,"Session Not Found")},get toolNotFound(){return _buildErrorResponse(404,"Tool Not Found")},get internalServerError(){return _buildErrorResponse(500,"Internal Server Error")}};function _buildErrorResponse(code,message){return{error:{code,message}}}async function _closeSession(session){session.closed=!0;try{await session.context.close(),debug("Closed MCP session context")}catch(err){error("Error occurred while closing MCP session context",err)}sessions.delete(session.id)}async function _createSession(ctx,sessionId){let now=Date.now(),session={id:sessionId,context:await platformInfo.toolsInfo.createToolSessionContext(()=>sessionId),toolExecutor:platformInfo.toolsInfo.createToolExecutor(),closed:!1,createdAt:now,lastActiveAt:now};return debug(`Created session with id ${sessionId}`),session}function _getSessionInfo(session){let now=Date.now();return{id:session.id,createdAt:session.createdAt,lastActiveAt:session.lastActiveAt,idleSeconds:Math.floor((now-session.lastActiveAt)/1e3)}}async function _getSession(ctx){let sessionId=ctx.req.header("session-id")||DEFAULT_SESSION_ID;return sessions.get(sessionId)}async function _getOrCreateSession(ctx){let sessionId=ctx.req.header("session-id")||DEFAULT_SESSION_ID,session=sessions.get(sessionId);return session?debug(`Reusing session with id ${sessionId}`):(debug(`No session could be found with id ${sessionId}`),session=await _createSession(ctx,sessionId),sessions.set(sessionId,session)),session}function _scheduleIdleSessionCheck(){let noActiveSession=!1;setInterval(()=>{let currentTime=Date.now();noActiveSession&&sessions.size===0&&(info("No active session found, so terminating daemon server"),process.exit(0));for(let[sessionId,session]of sessions)session.closed||(debug(`Checking whether session with id ${sessionId} is idle or not ...`),currentTime-session.lastActiveAt>DAEMON_SESSION_IDLE_SECONDS*1e3&&(debug(`Session with id ${sessionId} is idle, so it will be closing ...`),_closeSession(session).then(()=>{debug(`Session with id ${sessionId} was idle, so it has been closed`)}).catch(err=>{error(`Unable to delete idle session with id ${sessionId}`,err)})));noActiveSession=sessions.size===0},DAEMON_SESSION_IDLE_CHECK_SECONDS*1e3)}async function _logRequest(ctx){let reqClone=ctx.req.raw.clone();debug(`Got request: ${await reqClone.json()}`)}async function startDaemonHTTPServer(port){init({source:"cli"});let allowedDomains=AVAILABLE_TOOL_DOMAINS,allTools=platformInfo.toolsInfo.tools.filter(tool=>isToolEnabled(tool)),toolsToExpose=allowedDomains===void 0?allTools:allTools.filter(tool=>{let domain=tool.name().split("_")[0]?.toLowerCase()??"";return allowedDomains.has(domain)}),toolRegistry=new ToolRegistry;for(let tool of toolsToExpose)toolRegistry.addTool(tool);let executeTool=new Execute(toolRegistry,platformInfo.toolsInfo.executeImportantDescription,platformInfo.toolsInfo.executeDescription),toolMap=Object.fromEntries(toolsToExpose.map(tool=>[tool.name(),tool]));toolMap[executeTool.name()]=executeTool,app.use("*",cors({origin:"*",allowMethods:["GET","POST","DELETE","OPTIONS"],allowHeaders:["Content-Type","Authorization","session-id"]})),daemonPort=port,daemonStartTime=Date.now();let gracefulShutdown=async signal=>{info(`Received ${signal}, initiating graceful shutdown...`);let closePromises=[];for(let session of sessions.values())closePromises.push(_closeSession(session));await Promise.allSettled(closePromises),await shutdown(),info("All sessions closed, exiting..."),process.exit(0)};process.on("SIGTERM",()=>gracefulShutdown("SIGTERM")),process.on("SIGINT",()=>gracefulShutdown("SIGINT")),process.on("uncaughtException",err=>{error("Uncaught exception",err)}),process.on("unhandledRejection",reason=>{error("Unhandled rejection",reason)}),app.get("/health",ctx=>ctx.json({status:"ok"})),app.get("/info",ctx=>{let info2={version:require2("../package.json").version,uptime:Math.floor((Date.now()-daemonStartTime)/1e3),sessionCount:sessions.size,port:daemonPort};return ctx.json(info2)}),app.get("/sessions",ctx=>{let sessionList=[];for(let session of sessions.values())sessionList.push(_getSessionInfo(session));return ctx.json({sessions:sessionList})}),app.get("/session",async ctx=>{let session=await _getSession(ctx);return session?ctx.json(_getSessionInfo(session)):ctx.json(ERRORS.sessionNotFound,404)}),app.post("/shutdown",async ctx=>{info("Shutdown request received, closing all sessions...");let closePromises=[];for(let session of sessions.values())closePromises.push(_closeSession(session));return await Promise.allSettled(closePromises),await shutdown(),info("All sessions closed, shutting down daemon server..."),setTimeout(()=>{process.exit(0)},500),ctx.json({status:"shutting_down"},200)}),app.post("/call",async ctx=>{try{isDebugEnabled()&&await _logRequest(ctx);let session=await _getOrCreateSession(ctx);session.lastActiveAt=Date.now();let toolCallRequest=await ctx.req.json(),tool=toolMap[toolCallRequest.toolName];if(!tool)return ctx.json(ERRORS.toolNotFound,404);let toolInput;try{toolInput=z.object(tool.inputSchema()).parse(toolCallRequest.toolInput)}catch(err){let errorMessage=err.errors&&Array.isArray(err.errors)?err.errors.map(e=>`${e.path?.join(".")||"input"}: ${e.message}`).join("; "):"Invalid tool input";return ctx.json(_buildErrorResponse(400,`Invalid Tool Request: ${errorMessage}`),400)}let toolStartTime=Date.now();try{let toolOutput=await session.toolExecutor.executeTool(session.context,tool,toolInput);trackToolCalled({toolName:tool.name(),durationMs:Date.now()-toolStartTime,success:!0,sessionId:session.id});let toolCallResponse={toolOutput};return ctx.json(toolCallResponse,200)}catch(err){trackToolCalled({toolName:toolCallRequest.toolName,durationMs:Date.now()-toolStartTime,success:!1,sessionId:session.id,error:err});let toolCallResponse={toolError:{code:err.code,message:err.message}};return ctx.json(toolCallResponse,500)}}catch(err){return error("Error occurred while handling tool call request",err),ctx.json(ERRORS.internalServerError,500)}}),app.delete("/session",async ctx=>{try{let session=await _getSession(ctx);return session?(await _closeSession(session),ctx.json({ok:!0},200)):ctx.json(ERRORS.sessionNotFound,404)}catch(err){return error("Error occurred while deleting session",err),ctx.json(ERRORS.internalServerError,500)}}),app.onError((err,ctx)=>(error("Unhandled error in request handler",err),ctx.json({error:{code:500,message:"Internal Server Error"}},500))),app.notFound(ctx=>ctx.json({error:"Not Found",status:404},404)),serve({fetch:app.fetch,port},()=>info(`Listening on port ${port}`)),_scheduleIdleSessionCheck()}var isMainModule=import.meta.url===`file://${process.argv[1]}`||import.meta.url===`file://${process.argv[1]}.mjs`||process.argv[1]?.endsWith("daemon-server.js")||process.argv[1]?.endsWith("daemon-server.mjs");if(isMainModule){let parsePort=function(value){let n=Number(value);if(!Number.isInteger(n)||n<1||n>65535)throw new InvalidOptionArgumentError("port must be an integer between 1 and 65535");return n};parsePort2=parsePort;let options=new Command().addOption(new Option("--port <number>","port for daemon HTTP server").argParser(parsePort).default(DAEMON_PORT)).allowUnknownOption().parse(process.argv).opts();enable(),info("Starting daemon HTTP server..."),startDaemonHTTPServer(options.port).then(()=>{info("Daemon HTTP server started")}).catch(err=>{error("Failed to start daemon HTTP server",err),process.exit(1)})}var parsePort2;export{startDaemonHTTPServer};
1
+ import{Execute,init,shutdown,trackToolCalled}from"./core-UNMLWWDL.js";import{ToolRegistry,isToolEnabled,platformInfo}from"./core-PKYX4YOY.js";import{AVAILABLE_TOOL_DOMAINS,DAEMON_PORT,DAEMON_SESSION_IDLE_CHECK_SECONDS,DAEMON_SESSION_IDLE_SECONDS,debug,enable,error,info,isDebugEnabled}from"./core-3YBKJFSF.js";import{createRequire}from"node:module";import{Command,Option,InvalidOptionArgumentError}from"commander";import{serve}from"@hono/node-server";import{Hono}from"hono";import{cors}from"hono/cors";import{z}from"zod";var require2=createRequire(import.meta.url),daemonStartTime=0,daemonPort=0,app=new Hono,sessions=new Map,DEFAULT_SESSION_ID="#default",ERRORS={get sessionNotFound(){return _buildErrorResponse(404,"Session Not Found")},get toolNotFound(){return _buildErrorResponse(404,"Tool Not Found")},get internalServerError(){return _buildErrorResponse(500,"Internal Server Error")}};function _buildErrorResponse(code,message){return{error:{code,message}}}async function _closeSession(session){session.closed=!0;try{await session.context.close(),debug("Closed MCP session context")}catch(err){error("Error occurred while closing MCP session context",err)}sessions.delete(session.id)}async function _createSession(ctx,sessionId){let now=Date.now(),session={id:sessionId,context:await platformInfo.toolsInfo.createToolSessionContext(()=>sessionId),toolExecutor:platformInfo.toolsInfo.createToolExecutor(),closed:!1,createdAt:now,lastActiveAt:now};return debug(`Created session with id ${sessionId}`),session}function _getSessionInfo(session){let now=Date.now();return{id:session.id,createdAt:session.createdAt,lastActiveAt:session.lastActiveAt,idleSeconds:Math.floor((now-session.lastActiveAt)/1e3)}}async function _getSession(ctx){let sessionId=ctx.req.header("session-id")||DEFAULT_SESSION_ID;return sessions.get(sessionId)}async function _getOrCreateSession(ctx){let sessionId=ctx.req.header("session-id")||DEFAULT_SESSION_ID,session=sessions.get(sessionId);return session?debug(`Reusing session with id ${sessionId}`):(debug(`No session could be found with id ${sessionId}`),session=await _createSession(ctx,sessionId),sessions.set(sessionId,session)),session}function _scheduleIdleSessionCheck(){let noActiveSession=!1;setInterval(()=>{let currentTime=Date.now();noActiveSession&&sessions.size===0&&(info("No active session found, so terminating daemon server"),process.exit(0));for(let[sessionId,session]of sessions)session.closed||(debug(`Checking whether session with id ${sessionId} is idle or not ...`),currentTime-session.lastActiveAt>DAEMON_SESSION_IDLE_SECONDS*1e3&&(debug(`Session with id ${sessionId} is idle, so it will be closing ...`),_closeSession(session).then(()=>{debug(`Session with id ${sessionId} was idle, so it has been closed`)}).catch(err=>{error(`Unable to delete idle session with id ${sessionId}`,err)})));noActiveSession=sessions.size===0},DAEMON_SESSION_IDLE_CHECK_SECONDS*1e3)}async function _logRequest(ctx){let reqClone=ctx.req.raw.clone();debug(`Got request: ${await reqClone.json()}`)}async function startDaemonHTTPServer(port){init({source:"cli"});let allowedDomains=AVAILABLE_TOOL_DOMAINS,allTools=platformInfo.toolsInfo.tools.filter(tool=>isToolEnabled(tool)),toolsToExpose=allowedDomains===void 0?allTools:allTools.filter(tool=>{let domain=tool.name().split("_")[0]?.toLowerCase()??"";return allowedDomains.has(domain)}),toolRegistry=new ToolRegistry;for(let tool of toolsToExpose)toolRegistry.addTool(tool);let executeTool=new Execute(toolRegistry,platformInfo.toolsInfo.executeImportantDescription,platformInfo.toolsInfo.executeDescription),toolMap=Object.fromEntries(toolsToExpose.map(tool=>[tool.name(),tool]));toolMap[executeTool.name()]=executeTool,app.use("*",cors({origin:"*",allowMethods:["GET","POST","DELETE","OPTIONS"],allowHeaders:["Content-Type","Authorization","session-id"]})),daemonPort=port,daemonStartTime=Date.now();let gracefulShutdown=async signal=>{info(`Received ${signal}, initiating graceful shutdown...`);let closePromises=[];for(let session of sessions.values())closePromises.push(_closeSession(session));await Promise.allSettled(closePromises),await shutdown(),info("All sessions closed, exiting..."),process.exit(0)};process.on("SIGTERM",()=>gracefulShutdown("SIGTERM")),process.on("SIGINT",()=>gracefulShutdown("SIGINT")),process.on("uncaughtException",err=>{error("Uncaught exception",err)}),process.on("unhandledRejection",reason=>{error("Unhandled rejection",reason)}),app.get("/health",ctx=>ctx.json({status:"ok"})),app.get("/info",ctx=>{let info2={version:require2("../package.json").version,uptime:Math.floor((Date.now()-daemonStartTime)/1e3),sessionCount:sessions.size,port:daemonPort};return ctx.json(info2)}),app.get("/sessions",ctx=>{let sessionList=[];for(let session of sessions.values())sessionList.push(_getSessionInfo(session));return ctx.json({sessions:sessionList})}),app.get("/session",async ctx=>{let session=await _getSession(ctx);return session?ctx.json(_getSessionInfo(session)):ctx.json(ERRORS.sessionNotFound,404)}),app.post("/shutdown",async ctx=>{info("Shutdown request received, closing all sessions...");let closePromises=[];for(let session of sessions.values())closePromises.push(_closeSession(session));return await Promise.allSettled(closePromises),await shutdown(),info("All sessions closed, shutting down daemon server..."),setTimeout(()=>{process.exit(0)},500),ctx.json({status:"shutting_down"},200)}),app.post("/call",async ctx=>{try{isDebugEnabled()&&await _logRequest(ctx);let session=await _getOrCreateSession(ctx);session.lastActiveAt=Date.now();let toolCallRequest=await ctx.req.json(),tool=toolMap[toolCallRequest.toolName];if(!tool)return ctx.json(ERRORS.toolNotFound,404);let toolInput;try{toolInput=z.object(tool.inputSchema()).parse(toolCallRequest.toolInput)}catch(err){let errorMessage=err.errors&&Array.isArray(err.errors)?err.errors.map(e=>`${e.path?.join(".")||"input"}: ${e.message}`).join("; "):"Invalid tool input";return ctx.json(_buildErrorResponse(400,`Invalid Tool Request: ${errorMessage}`),400)}let toolStartTime=Date.now();try{let toolOutput=await session.toolExecutor.executeTool(session.context,tool,toolInput);trackToolCalled({toolName:tool.name(),durationMs:Date.now()-toolStartTime,success:!0,sessionId:session.id});let toolCallResponse={toolOutput};return ctx.json(toolCallResponse,200)}catch(err){trackToolCalled({toolName:toolCallRequest.toolName,durationMs:Date.now()-toolStartTime,success:!1,sessionId:session.id,error:err});let toolCallResponse={toolError:{code:err.code,message:err.message}};return ctx.json(toolCallResponse,500)}}catch(err){return error("Error occurred while handling tool call request",err),ctx.json(ERRORS.internalServerError,500)}}),app.delete("/session",async ctx=>{try{let session=await _getSession(ctx);return session?(await _closeSession(session),ctx.json({ok:!0},200)):ctx.json(ERRORS.sessionNotFound,404)}catch(err){return error("Error occurred while deleting session",err),ctx.json(ERRORS.internalServerError,500)}}),app.onError((err,ctx)=>(error("Unhandled error in request handler",err),ctx.json({error:{code:500,message:"Internal Server Error"}},500))),app.notFound(ctx=>ctx.json({error:"Not Found",status:404},404)),serve({fetch:app.fetch,port},()=>info(`Listening on port ${port}`)),_scheduleIdleSessionCheck()}var isMainModule=import.meta.url===`file://${process.argv[1]}`||import.meta.url===`file://${process.argv[1]}.mjs`||process.argv[1]?.endsWith("daemon-server.js")||process.argv[1]?.endsWith("daemon-server.mjs");if(isMainModule){let parsePort=function(value){let n=Number(value);if(!Number.isInteger(n)||n<1||n>65535)throw new InvalidOptionArgumentError("port must be an integer between 1 and 65535");return n};parsePort2=parsePort;let options=new Command().addOption(new Option("--port <number>","port for daemon HTTP server").argParser(parsePort).default(DAEMON_PORT)).allowUnknownOption().parse(process.argv).opts();enable(),info("Starting daemon HTTP server..."),startDaemonHTTPServer(options.port).then(()=>{info("Daemon HTTP server started")}).catch(err=>{error("Failed to start daemon HTTP server",err),process.exit(1)})}var parsePort2;export{startDaemonHTTPServer};
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import{Execute,init,shutdown,trackToolCalled}from"./core-UNMLWWDL.js";import{ToolRegistry,isToolEnabled,platformInfo}from"./core-RE2PLB4I.js";import{AVAILABLE_TOOL_DOMAINS,PORT,SESSION_CLOSE_ON_SOCKET_CLOSE,SESSION_IDLE_CHECK_SECONDS,SESSION_IDLE_SECONDS,TOOL_OUTPUT_SCHEMA_DISABLE,debug,disable,enable,error,info,isDebugEnabled}from"./core-3YBKJFSF.js";import{createRequire}from"node:module";var require2=createRequire(import.meta.url),SERVER_NAME="browser-devtools-mcp",SERVER_VERSION=require2("../package.json").version;function getServerInstructions(){if(!platformInfo.serverInfo.instructions)return;let parts=[];return parts.push(platformInfo.serverInfo.instructions),parts.join(`
2
+ import{Execute,init,shutdown,trackToolCalled}from"./core-UNMLWWDL.js";import{ToolRegistry,isToolEnabled,platformInfo}from"./core-PKYX4YOY.js";import{AVAILABLE_TOOL_DOMAINS,PORT,SESSION_CLOSE_ON_SOCKET_CLOSE,SESSION_IDLE_CHECK_SECONDS,SESSION_IDLE_SECONDS,TOOL_OUTPUT_SCHEMA_DISABLE,debug,disable,enable,error,info,isDebugEnabled}from"./core-3YBKJFSF.js";import{createRequire}from"node:module";var require2=createRequire(import.meta.url),SERVER_NAME="browser-devtools-mcp",SERVER_VERSION=require2("../package.json").version;function getServerInstructions(){if(!platformInfo.serverInfo.instructions)return;let parts=[];return parts.push(platformInfo.serverInfo.instructions),parts.join(`
3
3
 
4
4
  `).trim()}function getServerPolicies(){return platformInfo.serverInfo.policies}import crypto from"node:crypto";import{StreamableHTTPTransport}from"@hono/mcp";import{serve}from"@hono/node-server";import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{Hono}from"hono";import{cors}from"hono/cors";var MCP_TEMPLATE={jsonrpc:"2.0",error:{code:0,message:"N/A"},id:null},MCP_ERRORS={get sessionNotFound(){return _buildMCPErrorResponse(-32001,"Session Not Found")},get unauthorized(){return _buildMCPErrorResponse(-32001,"Unauthorized")},get internalServerError(){return _buildMCPErrorResponse(-32603,"Internal Server Error")}},sessions=new Map;function _buildMCPErrorResponse(code,message){let result={...MCP_TEMPLATE};return result.error.code=code,result.error.message=message,result}function _getImage(response){if("image"in response&&response.image!==null&&typeof response.image=="object"&&"data"in response.image&&"mimeType"in response.image&&Buffer.isBuffer(response.image.data)&&typeof response.image.mimeType=="string"){let image=response.image;return delete response.image,image}}function _toResponse(response){let image=_getImage(response),contents=[];return contents.push({type:"text",text:JSON.stringify(response,null,2)}),image&&(image.mimeType==="image/svg+xml"?contents.push({type:"text",text:image.data.toString(),mimeType:image.mimeType}):contents.push({type:"image",data:image.data.toString("base64"),mimeType:image.mimeType})),{content:contents,structuredContent:response,isError:!1}}function _createServer(opts){let server=new McpServer({name:SERVER_NAME,version:SERVER_VERSION},{capabilities:{resources:{},tools:{}},instructions:getServerInstructions()}),messages=[],policies=getServerPolicies();if(policies)for(let policy of policies)messages.push({role:"user",content:{type:"text",text:policy}});server.registerPrompt("default_system",{title:"Default System Prompt",description:"General behavior for the AI assistant"},async()=>({description:"Defines the assistant's general reasoning and tool usage rules.",messages}));let toolExecutor=platformInfo.toolsInfo.createToolExecutor(),fallbackSessionId=crypto.randomUUID(),toolSessionContext,createToolCallback=tool=>async args=>{let startTime=Date.now(),session=opts.sessionProvider?opts.sessionProvider():void 0,sessionId=session?.id||fallbackSessionId,clientName=server.server.getClientVersion()?.name;try{toolSessionContext||(session&&(toolSessionContext=session.context),toolSessionContext||(toolSessionContext=await platformInfo.toolsInfo.createToolSessionContext(()=>sessionId),session&&(session.context=toolSessionContext)));let response=await toolExecutor.executeTool(toolSessionContext,tool,args);return trackToolCalled({toolName:tool.name(),durationMs:Date.now()-startTime,success:!0,sessionId,clientName}),_toResponse(response)}catch(error2){return trackToolCalled({toolName:tool.name(),durationMs:Date.now()-startTime,success:!1,error:error2,sessionId,clientName}),{content:[{type:"text",text:`Error: ${error2.message}`}],isError:!0}}},includeOutputSchema=!TOOL_OUTPUT_SCHEMA_DISABLE,allowedDomains=AVAILABLE_TOOL_DOMAINS,toolRegistry=new ToolRegistry;platformInfo.toolsInfo.tools.forEach(t=>{if(!isToolEnabled(t)){debug(`Skipping tool ${t.name()} (isEnabled returned false)`);return}let domain=t.name().split("_")[0]?.toLowerCase()??"";if(allowedDomains&&!allowedDomains.has(domain)){debug(`Skipping tool ${t.name()} (domain ${domain} not in AVAILABLE_TOOL_DOMAINS)`);return}debug(`Registering tool ${t.name()} ...`);let toolOptions=includeOutputSchema?{description:t.description(),inputSchema:t.inputSchema(),outputSchema:t.outputSchema()}:{description:t.description(),inputSchema:t.inputSchema()};server.registerTool(t.name(),toolOptions,createToolCallback(t)),toolRegistry.addTool(t)});let executeTool=new Execute(toolRegistry,platformInfo.toolsInfo.executeImportantDescription,platformInfo.toolsInfo.executeDescription),executeToolOptions=includeOutputSchema?{description:executeTool.description(),inputSchema:executeTool.inputSchema(),outputSchema:executeTool.outputSchema()}:{description:executeTool.description(),inputSchema:executeTool.inputSchema()};return server.registerTool(executeTool.name(),executeToolOptions,createToolCallback(executeTool)),server}async function _createAndConnectServer(transport,opts){let server=_createServer({config:opts.config,sessionProvider:()=>sessions.get(transport.sessionId),transportType:opts.transportType});return await server.connect(transport),server}function _getConfig(){return{}}function _createSession(id,ctx,transport,server,closeOnTransportClose=!0){let session={id,transport,server,closed:!1,lastActiveAt:Date.now()},socket=ctx.env.incoming.socket;return socket._mcpRegistered||(socket._mcpRegistered=!0,socket.on("close",async()=>{debug(`Socket, which is for MCP session with id ${transport.sessionId}, has been closed`),SESSION_CLOSE_ON_SOCKET_CLOSE&&await transport.close()})),_registerMCPSessionClose(transport,session.server,closeOnTransportClose),debug(`Created MCP server session with id ${transport.sessionId}`),session}function _createOrUpdateSession(id,ctx,transport,server,closeOnTransportClose=!0){let session=sessions.get(id);return session?(session.transport=transport,session.server=server,session.closed=!1,session.lastActiveAt=Date.now(),session):_createSession(id,ctx,transport,server,closeOnTransportClose)}async function _createTransport(ctx){let serverConfig=_getConfig(),holder={},useSessionId=ctx.req.header("mcp-use-session-id"),transport=new StreamableHTTPTransport({enableJsonResponse:!0,sessionIdGenerator:()=>useSessionId||crypto.randomUUID(),onsessioninitialized:async sessionId=>{let session=_createOrUpdateSession(sessionId,ctx,transport,holder.server);sessions.set(sessionId,session),debug(`MCP session initialized with id ${sessionId}`)},onsessionclosed:async sessionId=>{debug(`Closing MCP session closed with id ${sessionId} ...`),await transport.close(),debug(`MCP session closed with id ${sessionId}`)}});return holder.server=await _createAndConnectServer(transport,{config:serverConfig,transportType:"streamable-http"}),transport}function _getSessionId(ctx){return ctx.req.header("mcp-session-id")}async function _getTransport(ctx){let sessionId=_getSessionId(ctx);if(sessionId){let session=sessions.get(sessionId);if(session)return debug(`Reusing MCP session with id ${sessionId}`),session.transport}}async function _getOrCreateTransport(ctx){let sessionId=_getSessionId(ctx);if(sessionId){let session=sessions.get(sessionId);if(session)return debug(`Reusing MCP session with id ${sessionId}`),session.transport;debug(`No MCP session could be found with id ${sessionId}`);return}return await _createTransport(ctx)}function _registerMCPSessionClose(transport,mcpServer,closeSession=!0){let closed=!1;transport.onclose=async()=>{if(debug(`Closing MCP session with id ${transport.sessionId} ...`),closed){debug(`MCP session with id ${transport.sessionId} has already been closed`);return}closed=!0;try{await mcpServer.close(),debug("Closed MCP server")}catch(err){error("Error occurred while closing MCP server",err)}if(closeSession&&transport.sessionId){let session=sessions.get(transport.sessionId);if(session&&(session.closed=!0,session.context))try{await session.context.close(),debug("Closed MCP session context")}catch(err){error("Error occurred while closing MCP session context",err)}sessions.delete(transport.sessionId),debug(`Closing MCP session with id ${transport.sessionId} ...`)}}}function _scheduleIdleSessionCheck(){setInterval(()=>{let currentTime=Date.now();for(let[sessionId,session]of sessions)debug(`Checking whether session with id ${sessionId} is idle or not ...`),currentTime-session.lastActiveAt>SESSION_IDLE_SECONDS*1e3&&(debug(`Session with id ${sessionId} is idle, so it will be closing ...`),session.transport.close().then(()=>{debug(`Session with id ${sessionId} was idle, so it has been closed`)}).catch(err=>{error(`Unable to delete idle session with id ${sessionId}`,err)}))},SESSION_IDLE_CHECK_SECONDS*1e3)}async function _logRequest(ctx){let reqClone=ctx.req.raw.clone();debug(`Got request: ${await reqClone.text()}`)}function _markSessionAsActive(ctx){let sessionId=_getSessionId(ctx);if(sessionId){let session=sessions.get(sessionId);session&&(session.lastActiveAt=Date.now())}}async function startStdioServer(){init({source:"mcp"});let transport=new StdioServerTransport;await _createAndConnectServer(transport,{config:_getConfig(),transportType:"stdio"})}var app=new Hono;async function startStreamableHTTPServer(port){init({source:"mcp"}),app.use("*",cors({origin:"*",allowMethods:["GET","POST","OPTIONS"],allowHeaders:["Content-Type","Authorization","MCP-Protocol-Version"]})),app.get("/health",ctx=>ctx.json({status:"ok"})),app.get("/ping",ctx=>ctx.json({status:"ok",message:"pong"})),app.get("/mcp",ctx=>ctx.json({status:"ok",protocol:"model-context-protocol",version:"1.0"})),app.post("/mcp",async ctx=>{try{isDebugEnabled()&&await _logRequest(ctx);let transport=await _getOrCreateTransport(ctx);return transport?(_markSessionAsActive(ctx),await transport.handleRequest(ctx)):ctx.json(MCP_ERRORS.sessionNotFound,400)}catch(err){return error("Error occurred while handling MCP request",err),ctx.json(MCP_ERRORS.internalServerError,500)}}),app.delete("/mcp",async ctx=>{try{let transport=await _getTransport(ctx);return transport?(await transport.close(),ctx.json({ok:!0},200)):ctx.json(MCP_ERRORS.sessionNotFound,400)}catch(err){return error("Error occurred while deleting MCP session",err),ctx.json(MCP_ERRORS.internalServerError,500)}}),app.notFound(ctx=>ctx.json({error:"Not Found",status:404},404)),serve({fetch:app.fetch,port},()=>info(`Listening on port ${port}`)),_scheduleIdleSessionCheck()}import{Command,Option,InvalidOptionArgumentError}from"commander";function _parsePort(value){let n=Number(value);if(!Number.isInteger(n)||n<1||n>65535)throw new InvalidOptionArgumentError("port must be an integer between 1 and 65535");return n}function _getOptions(){return new Command().addOption(new Option("--transport <type>","transport type").choices(["stdio","streamable-http"]).default("stdio")).addOption(new Option("--port <number>","port for Streamable HTTP transport").argParser(_parsePort).default(PORT)).allowUnknownOption().allowExcessArguments().parse(process.argv).opts()}async function _shutdown(){await shutdown()}async function main(){let options=_getOptions();for(let signal of["SIGTERM","SIGINT"])process.on(signal,async()=>{await _shutdown(),process.exit(0)});options.transport==="stdio"?(disable(),await startStdioServer()):options.transport==="streamable-http"?(info("Starting MCP server..."),await startStreamableHTTPServer(options.port),info("Started MCP Server")):(error(`Invalid transport: ${options.transport}`),process.exit(1))}main().catch(async err=>{enable(),error("MCP server error",err),await _shutdown(),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-devtools-mcp",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "description": "MCP Server for Browser Dev Tools",
5
5
  "private": false,
6
6
  "type": "module",