diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 14094e79..d3089608 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "devtools-for-agents", "description": "Help your agent build, debug, and verify your code correctly. With Chrome DevTools for agents, your AI agent can interact with the Chrome browser to test code, emulate users, and catch bugs using Chrome DevTools’ capabilities before shipping.", - "version": "1.7.0", + "version": "1.9.0", "author": { "name": "Google Chrome" }, @@ -12,7 +12,7 @@ "chrome-devtools": { "command": "npx", "args": [ - "chrome-devtools-mcp@1.7.0" + "chrome-devtools-mcp@1.9.0" ] } } diff --git a/.gitattributes b/.gitattributes index f133a008..e2e3fdb9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -28,6 +28,7 @@ src/version.ts merge=opera-ours # Generated artifacts: never hand-merge, always `npm run gen` after the merge. package-lock.json merge=opera-ours docs/tool-reference.md merge=opera-ours -src/bin/chrome-devtools-cli-options.ts merge=opera-ours +docs/configuration.md merge=opera-ours +src/config/cli-options.ts merge=opera-ours src/telemetry/tool_call_metrics.json merge=opera-ours src/telemetry/flag_usage_metrics.json merge=opera-ours diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cd95af73..9080f1b3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -61,5 +61,5 @@ updates: ignore: # Ignore tagged releases to fall back to branch commits. # Untagged commits get versions like `0.0.0-0.`. - - dependency-name: 'devtools-frontend' + - dependency-name: 'third_party/devtools-frontend' versions: ['> 0.0.0'] diff --git a/.prettierignore b/.prettierignore index d41b0d3f..85b7594e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,8 +3,10 @@ CHANGELOG.md src/third_party/lighthouse-devtools-mcp-bundle.js third_party/devtools-frontend -# Release-please formatting brakes CI checks +# Release-please formatting breaks CI checks .claude-plugin/plugin.json .cursor-plugin/plugin.json .github/plugin/plugin.json gemini-extension.json +mcp.json +plugin.json diff --git a/SECURITY.md b/SECURITY.md index 2757452a..56044db7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,12 +1,12 @@ ## Security policy -The Chrome DevTools MCP project takes security very seriously. Please use [Chromium’s process to report security issues](https://www.chromium.org/Home/chromium-security/reporting-security-bugs/). +The Chrome DevTools MCP project takes security very seriously. Please use [Google Open Source Software Vulnerability Reward Program](https://bughunters.google.com/about/rules/open-source/google-open-source-software-vulnerability-reward-program-rules) to report any security issues. ### Scope In general, it is the expectation that the AI agent or client using this MCP server validates any input (including tool calls and parameters) before sending it. The server provides powerful capabilities for browser automation and inspection, and it is the responsibility of the calling agent to ensure these are used safely and as intended. -Several tools in this project have the ability to perform actions such as writing files to disk (e.g., via browser downloads or screenshots) or dynamically loading Chrome extensions. These are intentional, documented features and are not vulnerabilities. +Several tools in this project have the ability to perform actions such as writing files to disk (e.g., via browser downloads or screenshots), loading source maps, or dynamically loading Chrome extensions. These are intentional, documented features and are not vulnerabilities. The server returns web content to the client as text (Markdown-like) or structured data (`--experimentalStructuredContent`). The web content is returned @@ -26,5 +26,5 @@ filesystem sandboxing, we recommend using OS sandbox mechanisms. ### Network guardrails -Optional `--allowed-url-pattern` and `--blocked-url-pattern` arguments configure the browser to reject access to the identified URLs. Note that this is not a complete network sandbox and it only applies to Chrome DevTools targets while `chrome-devtools-mcp` is attached to them. +Optional `--allowed-url-pattern` and `--blocked-url-pattern` arguments configure the **browser** to reject access to the identified URLs. Note that this is not a complete network sandbox and it only applies to Chrome DevTools targets while `chrome-devtools-mcp` is attached to them. To have a full network sandbox, we recommend using a separate OS/VM sandbox mechanism. diff --git a/docs/UPSTREAM.md b/docs/UPSTREAM.md index 753bb15f..d29d49cc 100644 --- a/docs/UPSTREAM.md +++ b/docs/UPSTREAM.md @@ -6,7 +6,7 @@ This document is the registry of every intentional divergence from upstream. `scripts/verify-upstream-seam.ts` enforces it: unregistered drift in an upstream-owned file fails CI. -- Current fork base: `ec014d1` (upstream, v1.7.0). +- Current fork base: `7a46acf` (upstream, v1.9.0). - Upstream remote: `git remote add upstream https://github.com/ChromeDevTools/chrome-devtools-mcp.git` ## Intake runbook @@ -89,7 +89,9 @@ a result: | `src/daemon/utils.ts` | App name + index script path from branding | yes | | `src/utils/check-for-updates.ts` | Env key + cache dir from branding | yes | | `src/bin/check-latest-version.ts` | Package name from branding (keep upstream's `getRegistry()`) | yes | -| `src/bin/chrome-devtools-mcp-cli-options.ts` | Branding strings; `performanceCrux` + `usageStatistics` default/help text from `opera/policy.ts` | yes | +| `src/config/mcp-options.ts` | Branding strings; `performanceCrux` + `usageStatistics` default/help text from `opera/policy.ts` | yes | +| `src/config/browser-options.ts` | Branding references (`REPO_URL`, `CACHE_DIR_NAME`, `MCP_BIN_NAME`) in the browser-option help text | yes | +| `src/config/category-options.ts` | Adds the `ToolCategory.OPERA` entry so the generated category flags include `categoryOpera` | yes | | `src/bin/chrome-devtools-mcp-main.ts` | Branding strings; calls `enforceTelemetryPolicy()` from `opera/policy.ts` | yes | | `src/bin/chrome-devtools-mcp.ts`, `src/bin/chrome-devtools.ts` | Branding strings | yes | | `src/telemetry/transformation.ts` | Adds `ZodRecord` to the supported telemetry zod types (used by `parameters` on `opera_call_mcp_tool`); maps it to a `_count` metric | yes | @@ -104,16 +106,17 @@ These are name changes, not behaviour changes: the fork's binary, package and en so the assertion strings do too. A test that needs a behavioural change indicates the seam is in the wrong place. -| Path | Divergence | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `tests/utils.ts` | `CLI_PATH` and the daemon status strings use the Opera names | -| `tests/index.test.ts` | Opera bin path + env var key | -| `tests/cli.test.ts` | Opera package name; `performanceCrux`/`usageStatistics` expected to default to `false` | -| `tests/utils/check-for-updates.test.ts` | Opera env var key; drops upstream's downgrade case (see `src/utils/check-for-updates.ts`) | -| `tests/daemon/utils.test.ts` | `APP_NAME` uses the Opera package name | -| `tests/ToolHandler.test.ts` | Opera env var keys, plus coverage for the `OperaToolHooks` seam | -| `tests/telemetry/transformation.test.ts` | Adds coverage for `ZodRecord` telemetry handling (count of record keys) | -| `tests/McpPage.test.ts` | Adds coverage for `resolveElementHandle` distinguishing a rejected lookup from a resolved-but-absent element | +| Path | Divergence | +| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `tests/utils.ts` | `CLI_PATH` and the daemon status strings use the Opera names | +| `tests/cli.test.ts` | Opera package name; `performanceCrux`/`usageStatistics` default to `false`; `categoryOpera` flag on by default | +| `tests/index.test.ts` | Opera bin path + env var key | +| `tests/utils/check-for-updates.test.ts` | Opera env var key; drops upstream's downgrade case (see `src/utils/check-for-updates.ts`) | +| `tests/daemon/utils.test.ts` | `APP_NAME` uses the Opera package name | +| `tests/ToolHandler.test.ts` | Opera env var keys, plus coverage for the `OperaToolHooks` seam | +| `tests/telemetry/transformation.test.ts` | Adds coverage for `ZodRecord` telemetry handling (count of record keys) | +| `tests/McpPage.test.ts` | Adds coverage for `resolveElementHandle` distinguishing a rejected lookup from a resolved-but-absent element | +| `tests/e2e/opera-devtools-commands.test.ts` | Inline assertion strings use `opera-devtools` binary name in restart-command suggestions | ### Upstream files we rename or delete @@ -122,17 +125,19 @@ resolve by hand, keeping the Opera side. Git leaves upstream's copy in the tree reflexive `git add -A` brings the file back — `npm run verify-upstream-seam` fails if any path in this table reappears. -| Path | What we did | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -| `tests/e2e/chrome-devtools-commands.test.ts`, `tests/e2e/chrome-devtools-disclaimers.test.ts`, `tests/e2e/chrome-devtools-start-stop.test.ts`, `tests/e2e/chrome-devtools-status.test.ts` | Renamed to `opera-devtools-*`; they drive the Opera-named bin | -| `tests/e2e/telemetry.test.ts` | Deleted — Opera forces telemetry off, so there is no upload path left to assert | -| `AGENTS.md` | Deleted in favour of Opera's own agent docs | -| `server.json`, `scripts/verify-server-json-version.ts`, `.github/workflows/publish-to-mcp-registry-on-tag.yml` | Deleted — the fork publishes to npm only, never to the MCP registry | +| Path | What we did | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `tests/e2e/chrome-devtools-commands.test.ts`, `tests/e2e/chrome-devtools-disclaimers.test.ts`, `tests/e2e/chrome-devtools-start-stop.test.ts`, `tests/e2e/chrome-devtools-status.test.ts` | Renamed to `opera-devtools-*`; they drive the Opera-named bin | +| `tests/e2e/telemetry.test.ts` | Deleted — Opera forces telemetry off, so there is no upload path left to assert | +| `AGENTS.md` | Deleted in favour of Opera's own agent docs | +| `server.json`, `scripts/verify-server-json-version.ts`, `.github/workflows/publish-to-mcp-registry-on-tag.yml` | Deleted — the fork publishes to npm only, never to the MCP registry | +| `.release-please-manifest.json`, `release-please-config.json` | Deleted — the fork releases via its own publish-on-tag workflow, not release-please | ### Generated — never hand-merge, always regenerate -`src/bin/chrome-devtools-cli-options.ts`, `src/telemetry/tool_call_metrics.json`, -`src/telemetry/flag_usage_metrics.json`, `docs/tool-reference.md`, `package-lock.json`. +`src/config/cli-options.ts`, `src/telemetry/tool_call_metrics.json`, +`src/telemetry/flag_usage_metrics.json`, `docs/tool-reference.md`, `docs/configuration.md`, +`package-lock.json`. ### Opera-owned prose / config (merge=opera-ours) diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md new file mode 100644 index 00000000..0e95bde6 --- /dev/null +++ b/docs/advanced-usage.md @@ -0,0 +1,172 @@ +# Advanced usage + +## Concurrent sessions + +Most MCP clients start one Chrome DevTools MCP server per conversation. If your +client shares a single server instance across concurrent agents or subagents, +start the server with `--experimentalPageIdRouting`. This exposes `pageId` on +page-scoped tools so each agent can route tool calls to the tab it is working +with. + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": [ + "-y", + "chrome-devtools-mcp@latest", + "--experimentalPageIdRouting" + ] + } + } +} +``` + +If you run multiple independent MCP client sessions and want each session to +launch its own temporary Chrome profile, also pass `--isolated`. This avoids +sharing the default Chrome DevTools MCP user data directory between those +server instances. + +## User data directory + +By default, `chrome-devtools-mcp` starts a Chrome's stable channel instance using the following user +data directory: + +- Linux / macOS: `$HOME/.cache/chrome-devtools-mcp/chrome-profile` +- Windows: `%USERPROFILE%\.cache\chrome-devtools-mcp\chrome-profile` + +For non-stable channels, the channel name is appended to the directory name, for example +`chrome-profile-canary`. + +The user data directory is not cleared between runs and is reused for subsequent +runs with the same channel. Only one browser can use it at a time. Set the `isolated` +option to `true` to use a temporary user data directory instead which will be cleared +automatically after the browser is closed. + +## Connecting to a running Chrome instance + +By default, the Chrome DevTools MCP server will start a new Chrome instance with a dedicated profile. This might not be ideal in all situations: + +- If you would like to maintain the same application state when alternating between manual site testing and agent-driven testing. +- When the MCP needs to sign into a website. Some accounts may prevent sign-in when the browser is controlled via WebDriver (the default launch mechanism for the Chrome DevTools MCP server). +- If you're running your LLM inside a sandboxed environment, but you would like to connect to a Chrome instance that runs outside the sandbox. + +In these cases, start Chrome first and let the Chrome DevTools MCP server connect to it. There are two ways to do so: + +- **Automatic connection (available in Chrome 144)**: best for sharing state between manual and agent-driven testing. +- **Manual connection via remote debugging port**: best when running inside a sandboxed environment. + +### Automatically connecting to a running Chrome instance + +**Step 1:** Set up remote debugging in Chrome + +In Chrome (\>= M144), do the following to set up remote debugging: + +1. Navigate to `chrome://inspect/#remote-debugging` to enable remote debugging. +2. Follow the dialog UI to allow or disallow incoming debugging connections. + +**Step 2:** Configure Chrome DevTools MCP server to automatically connect to a running Chrome Instance + +To connect the `chrome-devtools-mcp` server to the running Chrome instance, use +`--autoConnect` command line argument for the MCP server. + +The following code snippet is an example configuration for gemini-cli: + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": ["chrome-devtools-mcp@latest", "--autoConnect"] + } + } +} +``` + +**Step 3:** Test your setup + +Make sure your browser is running. Open gemini-cli and run the following prompt: + +```none +Check the performance of https://developers.chrome.com +``` + +> [!NOTE] +> The autoConnect option requires the user to start Chrome. If the user has multiple active profiles, the MCP server will connect to the default profile (as determined by Chrome). The MCP server has access to all open windows for the selected profile. + +The Chrome DevTools MCP server will try to connect to your running Chrome +instance. It shows a dialog asking for user permission. + +Clicking **Allow** results in the Chrome DevTools MCP server opening +[developers.chrome.com](http://developers.chrome.com) and taking a performance +trace. + +### Manual connection using port forwarding + +You can connect to a running Chrome instance by using the `--browser-url` option. This is useful if you are running the MCP server in a sandboxed environment that does not allow starting a new Chrome instance. + +Here is a step-by-step guide on how to connect to a running Chrome instance: + +**Step 1: Configure the MCP client** + +Add the `--browser-url` option to your MCP client configuration. The value of this option should be the URL of the running Chrome instance. `http://127.0.0.1:9222` is a common default. + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": [ + "chrome-devtools-mcp@latest", + "--browser-url=http://127.0.0.1:9222" + ] + } + } +} +``` + +**Step 2: Start the Chrome browser** + +> [!WARNING] +> Enabling the remote debugging port opens up a debugging port on the running browser instance. Any application on your machine can connect to this port and control the browser. Make sure that you are not browsing any sensitive websites while the debugging port is open. + +Start the Chrome browser with the remote debugging port enabled. Make sure to close any running Chrome instances before starting a new one with the debugging port enabled. The port number you choose must be the same as the one you specified in the `--browser-url` option in your MCP client configuration. + +For security reasons, [Chrome requires you to use a non-default user data directory](https://developer.chrome.com/blog/remote-debugging-port) when enabling the remote debugging port. You can specify a custom directory using the `--user-data-dir` flag. This ensures that your regular browsing profile and data are not exposed to the debugging session. + +**macOS** + +```bash +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile-stable +``` + +**Linux** + +```bash +/usr/bin/google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile-stable +``` + +**Windows** + +```bash +"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-profile-stable" +``` + +**Step 3: Test your setup** + +After configuring the MCP client and starting the Chrome browser, you can test your setup by running a simple prompt in your MCP client: + +``` +Check the performance of https://developers.chrome.com +``` + +Your MCP client should connect to the running Chrome instance and receive a performance report. + +If you hit VM-to-host port forwarding issues, see the “Remote debugging between virtual machine (VM) and host fails” section in [`troubleshooting.md`](./troubleshooting.md#remote-debugging-between-virtual-machine-vm-and-host-fails). + +For more details on remote debugging, see the [Chrome DevTools documentation](https://developer.chrome.com/docs/devtools/remote-debugging/). + +## Debugging Chrome on Android + +Please consult [these instructions](./debugging-android.md). diff --git a/docs/cli.md b/docs/cli.md index a0d8d291..af7a664c 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -11,6 +11,16 @@ npm i chrome-devtools-mcp@latest -g chrome-devtools status # check if install worked. ``` +The CLI enables unrestricted filesystem access by default. Use `--workspace` +to limit file tools to specific directories. Repeat the flag for more than one +directory. +`--allow-unrestricted-paths` still works on its own for compatibility, but it +cannot be combined with `--workspace`. + +```sh +chrome-devtools start --workspace=/path/to/project --workspace=/path/to/output +``` + ## How it works The CLI acts as a client to a background `chrome-devtools-mcp` daemon (uses Unix sockets on Linux/Mac and named pipes on Windows). @@ -23,11 +33,11 @@ The CLI acts as a client to a background `chrome-devtools-mcp` daemon (uses Unix # Check if the daemon is running chrome-devtools status -# Navigate the current page to a URL -chrome-devtools navigate_page "https://google.com" +# Navigate page 1 to a URL +chrome-devtools navigate_page 1 --url "https://google.com" -# Take a screenshot and save it to a file -chrome-devtools take_screenshot --filePath screenshot.png +# Take a screenshot of page 1 and save it to a file +chrome-devtools take_screenshot 1 --filePath screenshot.png # Stop the background daemon when finished chrome-devtools stop @@ -42,7 +52,7 @@ Thus, `--categoryExtensions` tools are currently not available in the CLI. chrome-devtools [arguments] [flags] ``` -- **Required Arguments**: Passed as positional arguments. +- **Required Arguments**: Passed as positional arguments. Page-scoped tools require `` as their first positional argument. - **Optional Arguments**: Passed as flags (e.g., `--filePath`, `--fullPage`). ### Examples @@ -51,24 +61,38 @@ chrome-devtools [arguments] [flags] ```sh chrome-devtools new_page "https://example.com" -chrome-devtools navigate_page "https://web.dev" --type url +chrome-devtools navigate_page 1 --url "https://web.dev" ``` **Interaction:** ```sh -# Click an element by its UID from a snapshot -chrome-devtools click "element-uid-123" +# Click an element by its UID from a snapshot on page 1 +chrome-devtools click 1 "element-uid-123" + +# Fill a form field on page 1 +chrome-devtools fill 1 "input-uid-456" "search query" +``` + +**Script Evaluation:** + +- When `--categoryExtensions` and `--pageIdRouting` are enabled: + - Target a page using `--pageId `: `chrome-devtools evaluate_script "() => document.title" --pageId 1` + - Target an extension service worker using `--serviceWorkerId `: `chrome-devtools evaluate_script "() => self.registration.scope" --serviceWorkerId sw-1` + +```sh +# Evaluate a JavaScript expression on page 1 +chrome-devtools evaluate_script "() => document.title" --pageId 1 -# Fill a form field -chrome-devtools fill "input-uid-456" "search query" +# Evaluate inside an extension service worker +chrome-devtools evaluate_script "() => self.registration.scope" --serviceWorkerId sw-1 ``` **Analysis:** ```sh -# Run a Lighthouse audit (defaults to navigation mode) -chrome-devtools lighthouse_audit --mode snapshot +# Run a Lighthouse audit on page 1 (defaults to navigation mode) +chrome-devtools lighthouse_audit 1 --mode snapshot ``` ## Output format diff --git a/docs/client-configurations.md b/docs/client-configurations.md new file mode 100644 index 00000000..634a9375 --- /dev/null +++ b/docs/client-configurations.md @@ -0,0 +1,406 @@ +# MCP Client configuration + + + +
+ Amp + Follow https://ampcode.com/manual#mcp and use the config provided above. You can also install the Chrome DevTools MCP server using the CLI: + +```bash +amp mcp add chrome-devtools -- npx chrome-devtools-mcp@latest +``` + +
+ +
+ Antigravity + +To use the Chrome DevTools MCP server follow the instructions from Antigravity's docs to install a custom MCP server. Add the following config to the MCP servers config: + +```bash +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": [ + "-y", + "chrome-devtools-mcp@latest", + "--browser-url=http://127.0.0.1:9222" + ] + } + } +} +``` + +This will make the Chrome DevTools MCP server automatically connect to the browser that Antigravity is using. If you are not using port 9222, make sure to adjust accordingly. + +Chrome DevTools MCP will not start the browser instance automatically using this approach because the Chrome DevTools MCP server connects to Antigravity's built-in browser. If the browser is not already running, you have to start it first by clicking the Chrome icon at the top right corner. + +
+ +
+ Bob + +Follow the IBM Bob MCP guide and add the Chrome DevTools MCP server to your Bob MCP configuration. Use the global config (`~/.bob/mcp.json`) to apply it across all workspaces, or a project config (`.bob/mcp.json`) to scope it to one project: + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": ["-y", "chrome-devtools-mcp@latest"] + } + } +} +``` + +You can edit these files from **Bob panel → Settings → MCP → Edit Global MCP** (or **Edit Project MCP**). Bob hot-reloads on save. Once the server appears in the MCP tab, switch to the **🌎 Browser Dev** mode to get guided browser debugging directly in Bob. + +
+ +
+ Claude Code + +**Install via CLI (MCP only)** + +Use the Claude Code CLI to add the Chrome DevTools MCP server (guide): + +```bash +claude mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest +``` + +**Install as a Plugin (MCP + Skills)** + +> [!NOTE] +> If you already had Chrome DevTools MCP installed previously for Claude Code, make sure to remove it first from your installation and configuration files. + +To install Chrome DevTools MCP with skills, add the marketplace registry in Claude Code: + +```sh +/plugin marketplace add ChromeDevTools/chrome-devtools-mcp +``` + +Then, install the plugin: + +```sh +/plugin install chrome-devtools-mcp@chrome-devtools-plugins +``` + +Restart Claude Code to have the MCP server and skills load (check with `/skills`). + +> [!TIP] +> If the plugin installation fails with a `Failed to clone repository` error (e.g., HTTPS connectivity issues behind a corporate firewall), see the [troubleshooting guide](./troubleshooting.md#claude-code-plugin-installation-fails-with-failed-to-clone-repository) for workarounds, or use the CLI installation method above instead. + +
+ +
+ Cline + Follow https://docs.cline.bot/mcp/configuring-mcp-servers and use the config provided above. +
+ +
+ Codex + Follow the configure MCP guide + using the standard config from above. You can also install the Chrome DevTools MCP server using the Codex CLI: + +```bash +codex mcp add chrome-devtools -- npx chrome-devtools-mcp@latest +``` + +**On Windows 11** + +Configure the Chrome install location and increase the startup timeout by updating `.codex/config.toml` and adding the following `env` and `startup_timeout_ms` parameters: + +``` +[mcp_servers.chrome-devtools] +command = "cmd" +args = [ + "/c", + "npx", + "-y", + "chrome-devtools-mcp@latest", +] +env = { SystemRoot="C:\\Windows", PROGRAMFILES="C:\\Program Files" } +startup_timeout_ms = 20_000 +``` + +
+ +
+ Command Code + +Use the Command Code CLI to add the Chrome DevTools MCP server (MCP guide): + +```bash +cmd mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest +``` + +
+ +
+ Copilot CLI + +Start Copilot CLI: + +``` +copilot +``` + +Start the dialog to add a new MCP server by running: + +``` +/mcp add +``` + +Configure the following fields and press `CTRL+S` to save the configuration: + +- **Server name:** `chrome-devtools` +- **Server Type:** `[1] Local` +- **Command:** `npx -y chrome-devtools-mcp@latest` + +
+ +
+ Copilot / VS Code + +**Install as a Plugin (Recommended)** + +The easiest way to get up and running is to install `chrome-devtools-mcp` as an agent plugin. +This bundles the **MCP server** and all **skills** together, so your agent gets both the tools +and the expert guidance it needs to use them effectively. + +1. Open the **Command Palette** (`Cmd+Shift+P` on macOS or `Ctrl+Shift+P` on Windows/Linux). +2. Search for and run the **Chat: Install Plugin From Source** command. +3. Paste in our repository name: `ChromeDevTools/chrome-devtools-mcp`. + +That's it! Your agent is now supercharged with Chrome DevTools capabilities. + +--- + +**Install as an MCP Server (MCP only)** + +**Click the button to install:** + +[Install in VS Code](https://vscode.dev/redirect/mcp/install?name=io.github.ChromeDevTools%2Fchrome-devtools-mcp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22chrome-devtools-mcp%22%5D%2C%22env%22%3A%7B%7D%7D) + +[Install in VS Code Insiders](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522io.github.ChromeDevTools%252Fchrome-devtools-mcp%2522%252C%2522config%2522%253A%257B%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522chrome-devtools-mcp%2522%255D%252C%2522env%2522%253A%257B%257D%257D%257D) + +**Or install manually:** + +Follow the VS Code [MCP configuration guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server) using the standard config from above, or use the CLI: + +For macOS and Linux: + +```bash +code --add-mcp '{"name":"io.github.ChromeDevTools/chrome-devtools-mcp","command":"npx","args":["-y","chrome-devtools-mcp"],"env":{}}' +``` + +For Windows (PowerShell): + +```powershell +code --add-mcp '{"""name""":"""io.github.ChromeDevTools/chrome-devtools-mcp""","""command""":"""npx""","""args""":["""-y""","""chrome-devtools-mcp"""]}' +``` + +
+ +
+ Cursor + +**Click the button to install:** + +[Install in Cursor](https://cursor.com/en/install-mcp?name=chrome-devtools&config=eyJjb21tYW5kIjoibnB4IC15IGNocm9tZS1kZXZ0b29scy1tY3BAbGF0ZXN0In0%3D) + +**Or install manually:** + +Go to `Cursor Settings` -> `MCP` -> `New MCP Server`. Use the config provided above. + +
+ +
+ Devin CLI + +**Install via CLI (MCP only)** + +Use the Devin CLI to add the Chrome DevTools MCP server (guide): + +```bash +devin mcp add chrome-devtools -- npx chrome-devtools-mcp@latest +``` + +
+ +
+ Factory CLI +Use the Factory CLI to add the Chrome DevTools MCP server (guide): + +```bash +droid mcp add chrome-devtools "npx -y chrome-devtools-mcp@latest" +``` + +
+ +
+ Gemini CLI +Install the Chrome DevTools MCP server using the Gemini CLI. + +**Project wide:** + +```bash +# Either MCP only: +gemini mcp add chrome-devtools npx chrome-devtools-mcp@latest +# Or as a Gemini extension (MCP+Skills): +gemini extensions install --auto-update https://github.com/ChromeDevTools/chrome-devtools-mcp +``` + +**Globally:** + +```bash +gemini mcp add -s user chrome-devtools npx chrome-devtools-mcp@latest +``` + +Alternatively, follow the MCP guide and use the standard config from above. + +
+ +
+ Gemini Code Assist + Follow the configure MCP guide + using the standard config from above. +
+ +
+ Grok Build CLI + +```bash +grok mcp add chrome-devtools npx chrome-devtools-mcp@latest +``` + +See the docs for more options +
+ +
+ JetBrains AI Assistant & Junie + +Go to `Settings | Tools | AI Assistant | Model Context Protocol (MCP)` -> `Add`. Use the config provided above. +The same way chrome-devtools-mcp can be configured for JetBrains Junie in `Settings | Tools | Junie | MCP Settings` -> `Add`. Use the config provided above. + +
+ +
+ Kiro + +In **Kiro Settings**, go to `Configure MCP` > `Open Workspace or User MCP Config` > Use the configuration snippet provided above. + +Or, from the IDE **Activity Bar** > `Kiro` > `MCP Servers` > `Click Open MCP Config`. Use the configuration snippet provided above. + +
+ +
+ Katalon Studio + +The Chrome DevTools MCP server can be used with Katalon StudioAssist via an MCP proxy. + +**Step 1:** Install the MCP proxy by following the MCP proxy setup guide. + +**Step 2:** Start the Chrome DevTools MCP server with the proxy: + +```bash +mcp-proxy --transport streamablehttp --port 8080 -- npx -y chrome-devtools-mcp@latest +``` + +**Note:** You may need to pick another port if 8080 is already in use. + +**Step 3:** In Katalon Studio, add the server to StudioAssist with the following settings: + +- **Connection URL:** `http://127.0.0.1:8080/mcp` +- **Transport type:** `HTTP` + +Once connected, the Chrome DevTools MCP tools will be available in StudioAssist. + +
+ +
+ Mistral Vibe + +Add in ~/.vibe/config.toml: + +```toml +[[mcp_servers]] +name = "chrome-devtools" +transport = "stdio" +command = "npx" +args = ["chrome-devtools-mcp@latest"] +``` + +
+ +
+ OpenCode + +Add the following configuration to your `opencode.json` file. If you don't have one, create it at `~/.config/opencode/opencode.json` (guide): + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "chrome-devtools": { + "type": "local", + "command": ["npx", "-y", "chrome-devtools-mcp@latest"] + } + } +} +``` + +
+ +
+ Qoder + +In **Qoder Settings**, go to `MCP Server` > `+ Add` > Use the configuration snippet provided above. + +Alternatively, follow the MCP guide and use the standard config from above. + +
+ +
+ Qoder CLI + +Install the Chrome DevTools MCP server using the Qoder CLI (guide): + +**Project wide:** + +```bash +qodercli mcp add chrome-devtools -- npx chrome-devtools-mcp@latest +``` + +**Globally:** + +```bash +qodercli mcp add -s user chrome-devtools -- npx chrome-devtools-mcp@latest +``` + +
+ +
+ Visual Studio + +**Click the button to install:** + +[Install in Visual Studio](https://vs-open.link/mcp-install?%7B%22name%22%3A%22chrome-devtools%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22chrome-devtools-mcp%40latest%22%5D%7D) + +
+ +
+ Warp + +Go to `Settings | AI | Manage MCP Servers` -> `+ Add` to [add an MCP Server](https://docs.warp.dev/knowledge-and-collaboration/mcp#adding-an-mcp-server). Use the config provided above. + +
+ +
+ Windsurf + Follow the configure MCP guide + using the standard config from above. +
diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..f2d6da4e --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,275 @@ +# Configuration + +The Chrome DevTools MCP server supports the following configuration option: + + + +- **`--categoryEmulation`/ `--category-emulation`** + Set to false to exclude tools related to emulation. + - **Type:** boolean + - **Default:** `true` + +- **`--categoryPerformance`/ `--category-performance`** + Set to false to exclude tools related to performance. + - **Type:** boolean + - **Default:** `true` + +- **`--categoryNetwork`/ `--category-network`** + Set to false to exclude tools related to network. + - **Type:** boolean + - **Default:** `true` + +- **`--categoryExtensions`/ `--category-extensions`** + Set to true to include tools related to extensions. Note: This feature is currently only supported with a pipe connection. autoConnect, browserUrl, and wsEndpoint are not supported with this feature until 149 will be released. + - **Type:** boolean + - **Default:** `false` + +- **`--categoryExperimentalThirdParty`/ `--category-experimental-third-party`** + Set to true to enable third-party developer tools exposed by the inspected page itself + - **Type:** boolean + - **Default:** `false` + +- **`--categoryPwa`/ `--category-pwa`** + Set to true to include tools for automating Progressive Web Apps (install, launch, uninstall, and OS state). This feature is only supported with a pipe connection; autoConnect, browserUrl, and wsEndpoint are not supported. + - **Type:** boolean + - **Default:** `false` + +- **`--autoConnect`/ `--auto-connect`** + If specified, automatically connects to a browser (Chrome 144+) running locally from the user data directory identified by the channel param (default channel is stable). Requires the remote debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging. + - **Type:** boolean + - **Default:** `false` + +- **`--browserUrl`/ `--browser-url`, `-u`** + Connect to a running, debuggable Chrome instance (e.g. `http://127.0.0.1:9222`). For more details see: https://github.com/operasoftware/opera-devtools-mcp#connecting-to-a-running-chrome-instance. + - **Type:** string + - **Default:** `false` + +- **`--wsEndpoint`/ `--ws-endpoint`, `-w`** + WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/). Alternative to --browserUrl. + - **Type:** string + - **Default:** `false` + +- **`--wsHeaders`/ `--ws-headers`** + Custom headers for WebSocket connection in JSON format (e.g., '{"Authorization":"Bearer token"}'). Only works with --wsEndpoint. + - **Type:** string + - **Default:** `false` + +- **`--headless`** + Whether to run in headless (no UI) mode. + - **Type:** boolean + - **Default:** `false` + +- **`--executablePath`/ `--executable-path`, `-e`** + Path to custom Chrome executable. + - **Type:** string + - **Default:** `false` + +- **`--isolated`** + If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to false. + - **Type:** boolean + - **Default:** `false` + +- **`--userDataDir`/ `--user-data-dir`** + Path to the user data directory for Chrome. Default is $HOME/.cache/opera-devtools-mcp/chrome-profile$CHANNEL_SUFFIX_IF_NON_STABLE + - **Type:** string + - **Default:** `false` + +- **`--channel`** + Specify a different Chrome channel that should be used. The default is the stable channel version. + - **Type:** string + - **Choices:** `canary`, `dev`, `beta`, `stable` + - **Default:** `false` + +- **`--proxyServer`/ `--proxy-server`** + Proxy server configuration for Chrome passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details. + - **Type:** string + - **Default:** `false` + +- **`--chromeArg`/ `--chrome-arg`** + Additional arguments for Chrome. Only applies when Chrome is launched by opera-devtools-mcp. + - **Type:** array + - **Default:** `false` + +- **`--ignoreDefaultChromeArg`/ `--ignore-default-chrome-arg`** + Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by opera-devtools-mcp. + - **Type:** array + - **Default:** `false` + +- **`--logFile`/ `--log-file`** + Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports. + - **Type:** string + - **Default:** `false` + +- **`--viewport`** + Initial viewport size for the Chrome instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px. + - **Type:** string + - **Default:** `false` + +- **`--acceptInsecureCerts`/ `--accept-insecure-certs`** + If enabled, ignores errors relative to self-signed and expired certificates. Use with caution. + - **Type:** boolean + - **Default:** `false` + +- **`--pageIdRouting`/ `--page-id-routing`** + Require pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions). Use --no-page-id-routing to disable. + - **Type:** boolean + - **Default:** `true` + +- **`--experimentalDevtools`/ `--experimental-devtools`** + Whether to enable automation over DevTools targets + - **Type:** boolean + - **Default:** `false` + +- **`--experimentalVision`/ `--experimental-vision`** + Whether to enable coordinate-based tools such as click_at(x,y). Usually requires a computer-use model able to produce accurate coordinates by looking at screenshots. + - **Type:** boolean + - **Default:** `false` + +- **`--memoryDebugging`/ `--memory-debugging`, `--experimentalMemory`** + Whether to enable memory debugging tools. + - **Type:** boolean + - **Default:** `false` + +- **`--experimentalStructuredContent`/ `--experimental-structured-content`** + Whether to output structured formatted content. + - **Type:** boolean + - **Default:** `false` + +- **`--experimentalIncludeAllPages`/ `--experimental-include-all-pages`** + Whether to include all kinds of pages such as webviews or background pages as pages. + - **Type:** boolean + - **Default:** `false` + +- **`--experimentalScreencast`/ `--experimental-screencast`** + Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH. + - **Type:** boolean + - **Default:** `false` + +- **`--experimentalFfmpegPath`/ `--experimental-ffmpeg-path`** + Path to ffmpeg executable for screencast recording. + - **Type:** string + - **Default:** `false` + +- **`--experimentalScreencastFps`/ `--experimental-screencast-fps`** + Frames per second to use for screencast recording. Lower values can reduce memory pressure on pages that produce frames faster than ffmpeg can encode them. + - **Type:** number + - **Default:** `false` + +- **`--blockedUrlPattern`/ `--blocked-url-pattern`** + Restricts browser's network access by blocking specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns. + - **Type:** array + - **Default:** `false` + +- **`--allowedUrlPattern`/ `--allowed-url-pattern`** + Restricts browser's network access by allowing only specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Requires Chrome 149+. Silently detaches from targets with unallowed URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns. + - **Type:** array + - **Default:** `false` + +- **`--performanceCrux`/ `--performance-crux`** + Set to true to enable sending URLs from performance traces to CrUX API to get field performance data. + - **Type:** boolean + - **Default:** `false` + +- **`--usageStatistics`/ `--usage-statistics`** + Set to true to opt-in to usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if `OPERA_DEVTOOLS_NO_USAGE_STATISTICS` or `CI` env variables are set. + - **Type:** boolean + - **Default:** `false` + +- **`--javascriptEvaluation`/ `--javascript-evaluation`** + Set to false to disable JavaScript execution. When disabled, evaluation tools (evaluate_script and slim evaluate) are disabled, the initScript parameter in navigate_page is turned off, and navigating to javascript:, data:, or vbscript: URLs is disallowed. + - **Type:** boolean + - **Default:** `true` + +- **`--sourceMaps`/ `--source-maps`** + Whether to enable source maps in DevTools. Use --no-source-maps to disable. + - **Type:** boolean + - **Default:** `true` + +- **`--screenshotFormat`/ `--screenshot-format`** + Override the default output format used by take_screenshot when the caller does not specify one. JPEG and WebP are ~3-5x smaller than PNG, which reduces transfer and storage size. To reduce context size use --screenshotMaxWidth / --screenshotMaxHeight, since image tokens scale with dimensions rather than encoded bytes. Unset preserves the existing default ("png"). + - **Type:** string + - **Choices:** `jpeg`, `png`, `webp` + - **Default:** `false` + +- **`--screenshotQuality`/ `--screenshot-quality`** + Override the default compression quality (0-100) used by take_screenshot for JPEG and WebP when the caller does not specify one. Lower values mean smaller files. Ignored for PNG. Unset preserves the Puppeteer default. + - **Type:** number + - **Default:** `false` + +- **`--screenshotMaxWidth`/ `--screenshot-max-width`** + Maximum width in pixels for screenshots. If the captured image is wider, it is downscaled (preserving aspect ratio) before being returned. Reduces context size in AI conversations. Unset means no resize. + - **Type:** number + - **Default:** `false` + +- **`--screenshotMaxHeight`/ `--screenshot-max-height`** + Maximum height in pixels for screenshots. If the captured image is taller, it is downscaled (preserving aspect ratio) before being returned. Can be combined with --screenshot-max-width; the smaller scale factor wins. Unset means no resize. + - **Type:** number + - **Default:** `false` + +- **`--slim`** + Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks. + - **Type:** boolean + - **Default:** `false` + +- **`--redactNetworkHeaders`/ `--redact-network-headers`** + If true, redacts some of the network headers considered sensitive before returning to the client. + - **Type:** boolean + - **Default:** `false` + +- **`--allowUnrestrictedPaths`/ `--allow-unrestricted-paths`** + If set, disables the default path restriction that applies when the MCP client does not negotiate the roots capability. By default, file-writing tools are restricted to the OS temp directory when no roots are configured. Use this only when connecting a trusted local client that does not implement MCP roots and requires access to paths outside the temp directory. + - **Type:** boolean + - **Default:** `false` + +- **`--filesystemRoot`/ `--filesystem-root`, `--workspace`** + A directory that filesystem tools are allowed to access. May be specified more than once. + - **Type:** array + - **Default:** `OS temp directory` + +- **`--config`** + Path to JSON configuration file. + - **Type:** string + - **Default:** `false` + + + +Pass them via the `args` property in the JSON configuration. For example: + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": [ + "chrome-devtools-mcp@latest", + "--channel=canary", + "--headless=true", + "--isolated=true" + ] + } + } +} +``` + +## Connecting via WebSocket with custom headers + +You can connect directly to a Chrome WebSocket endpoint and include custom headers (e.g., for authentication): + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": [ + "chrome-devtools-mcp@latest", + "--wsEndpoint=ws://127.0.0.1:9222/devtools/browser/", + "--wsHeaders={\"Authorization\":\"Bearer YOUR_TOKEN\"}" + ] + } + } +} +``` + +To get the WebSocket endpoint from a running Chrome instance, visit `http://127.0.0.1:9222/json/version` and look for the `webSocketDebuggerUrl` field. + +You can also run `npx chrome-devtools-mcp@latest --help` to see all available configuration options. diff --git a/docs/tool-reference.md b/docs/tool-reference.md index dba00260..09aa576c 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -39,7 +39,7 @@ - [`take_snapshot`](#take_snapshot) - [`screencast_start`](#screencast_start) - [`screencast_stop`](#screencast_stop) -- **[Memory](#memory)** (12 tools) +- **[Memory](#memory)** (13 tools) - [`take_heapsnapshot`](#take_heapsnapshot) - [`close_heapsnapshot`](#close_heapsnapshot) - [`compare_heapsnapshots`](#compare_heapsnapshots) @@ -52,6 +52,7 @@ - [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers) - [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths) - [`get_heapsnapshot_summary`](#get_heapsnapshot_summary) + - [`query_heapsnapshot_objects`](#query_heapsnapshot_objects) - **[Opera](#opera)** (14 tools) - [`opera_authenticate_mcp_server`](#opera_authenticate_mcp_server) - [`opera_call_mcp_tool`](#opera_call_mcp_tool) @@ -93,6 +94,7 @@ **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **uid** (string) **(required)**: The uid of an element on the page from the page content snapshot - **dblClick** (boolean) _(optional)_: Set to true for double clicks. Default is false. - **includeSnapshot** (boolean) _(optional)_: Whether to include a snapshot in the response. Default is false. @@ -106,6 +108,7 @@ **Parameters:** - **from_uid** (string) **(required)**: The uid of the element to [`drag`](#drag) +- **pageId** (number) **(required)**: Targets a specific page by ID. - **to_uid** (string) **(required)**: The uid of the element to drop into - **includeSnapshot** (boolean) _(optional)_: Whether to include a snapshot in the response. Default is false. @@ -117,6 +120,7 @@ **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **uid** (string) **(required)**: The uid of an element on the page from the page content snapshot - **value** (string) **(required)**: The value to [`fill`](#fill) in. "true" or "false" for checkboxes and toggles, "true" for radio buttons. - **includeSnapshot** (boolean) _(optional)_: Whether to include a snapshot in the response. Default is false. @@ -130,6 +134,7 @@ **Parameters:** - **elements** (array) **(required)**: Elements from snapshot to [`fill`](#fill) out. +- **pageId** (number) **(required)**: Targets a specific page by ID. - **includeSnapshot** (boolean) _(optional)_: Whether to include a snapshot in the response. Default is false. --- @@ -141,6 +146,7 @@ **Parameters:** - **action** (enum: "accept", "dismiss") **(required)**: Whether to dismiss or accept the dialog +- **pageId** (number) **(required)**: Targets a specific page by ID. - **promptText** (string) _(optional)_: Optional prompt text to enter into the dialog. --- @@ -151,6 +157,7 @@ **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **uid** (string) **(required)**: The uid of an element on the page from the page content snapshot - **includeSnapshot** (boolean) _(optional)_: Whether to include a snapshot in the response. Default is false. @@ -163,6 +170,7 @@ **Parameters:** - **key** (string) **(required)**: A key or a combination (e.g., "Enter", "Control+A", "Control++", "Control+Shift+R"). Modifiers: Control, Shift, Alt, Meta +- **pageId** (number) **(required)**: Targets a specific page by ID. - **includeSnapshot** (boolean) _(optional)_: Whether to include a snapshot in the response. Default is false. --- @@ -173,6 +181,7 @@ **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **text** (string) **(required)**: The text to type - **submitKey** (string) _(optional)_: Optional key to press after typing. E.g., "Enter", "Tab", "Escape" @@ -185,6 +194,7 @@ **Parameters:** - **filePaths** (array) **(required)**: One or more files paths to upload. File paths have to be local to the browser instance (not the MCP). +- **pageId** (number) **(required)**: Targets a specific page by ID. - **uid** (string) **(required)**: The uid of the file input element or an element that will open file chooser on the page from the page content snapshot - **includeSnapshot** (boolean) _(optional)_: Whether to include a snapshot in the response. Default is false. @@ -196,6 +206,7 @@ **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **x** (number) **(required)**: The x coordinate - **y** (number) **(required)**: The y coordinate - **dblClick** (boolean) _(optional)_: Set to true for double clicks. Default is false. @@ -229,6 +240,7 @@ **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **handleBeforeUnload** (enum: "accept", "dismiss") _(optional)_: Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept. - **ignoreCache** (boolean) _(optional)_: Whether to ignore cache on reload. - **initScript** (string) _(optional)_: A JavaScript script to be executed on each new document before any other scripts for the next navigation. @@ -246,7 +258,7 @@ - **url** (string) **(required)**: URL to load in a new page. - **background** (boolean) _(optional)_: Whether to open the page in the background without bringing it to the front. Default is false (foreground). -- **isolatedContext** (string) _(optional)_: If specified, the page is created in an isolated browser context with the given name. Pages in the same browser context share cookies and storage. Pages in different browser contexts are fully isolated. +- **isolatedContext** (string) _(optional)_: If specified, the page is created in an isolated browser context with the given name. Pages in the same browser context share cookies and storage. Pages in different browser contexts are fully isolated (useful for clean-slate testing of cookies and authentication). - **timeout** (integer) _(optional)_: Maximum wait time in milliseconds. If set to 0, the default timeout will be used. --- @@ -268,6 +280,7 @@ **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **text** (array) **(required)**: Non-empty list of texts. Resolves when any value appears on the page. - **timeout** (integer) _(optional)_: Maximum wait time in milliseconds. If set to 0, the default timeout will be used. @@ -277,10 +290,11 @@ ### `emulate` -**Description:** Emulates various features on the selected page. +**Description:** Emulates various features on the target page. **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **colorScheme** (enum: "dark", "light", "auto") _(optional)_: [`Emulate`](#emulate) the dark or the light mode. Set to "auto" to reset to the default. - **cpuThrottlingRate** (number) _(optional)_: Represents the CPU slowdown factor. Omit or set the rate to 1 to disable throttling - **extraHttpHeaders** (string) _(optional)_: Extra HTTP headers as a JSON string object, e.g. {"X-Custom": "value", "Authorization": "Bearer token"}. Headers are included into every HTTP request originating from the page and persist across navigations until cleared. Pass an empty string to clear all extra headers. @@ -293,11 +307,12 @@ ### `resize_page` -**Description:** Resizes the selected page's window so that the page has specified dimension +**Description:** Resizes the page's window so that the page has specified dimension **Parameters:** - **height** (number) **(required)**: Page height +- **pageId** (number) **(required)**: Targets a specific page by ID. - **width** (number) **(required)**: Page width --- @@ -312,27 +327,30 @@ - **insightName** (string) **(required)**: The name of the Insight you want more information on. For example: "DocumentLatency" or "LCPBreakdown" - **insightSetId** (string) **(required)**: The id for the specific insight set. Only use the ids given in the "Available insight sets" list. +- **pageId** (number) **(required)**: Targets a specific page by ID. --- ### `performance_start_trace` -**Description:** Start a performance trace on the selected webpage. Use to find frontend performance issues, Core Web Vitals (LCP, INP, CLS), and improve page load speed. +**Description:** Start a performance trace on the target webpage. Use to find frontend performance issues, Core Web Vitals (LCP, INP, CLS), and improve page load speed. **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **autoStop** (boolean) _(optional)_: Determines if the trace recording should be automatically stopped. - **filePath** (string) _(optional)_: The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed). -- **reload** (boolean) _(optional)_: Determines if, once tracing has started, the current selected page should be automatically reloaded. Navigate the page to the right URL using the [`navigate_page`](#navigate_page) tool BEFORE starting the trace if reload or autoStop is set to true. +- **reload** (boolean) _(optional)_: Determines if, once tracing has started, the target page should be automatically reloaded. Navigate the page to the right URL using the [`navigate_page`](#navigate_page) tool BEFORE starting the trace if reload or autoStop is set to true. --- ### `performance_stop_trace` -**Description:** Stop the active performance trace recording on the selected webpage. +**Description:** Stop the active performance trace recording on the target webpage. **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **filePath** (string) _(optional)_: The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed). --- @@ -341,10 +359,11 @@ ### `get_network_request` -**Description:** Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel. +**Description:** Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel. Useful for inspecting request headers (including 'Cookie') and response headers (including 'Set-Cookie' and directives). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **reqid** (number) _(optional)_: The reqid of the network request. If omitted returns the currently selected request in the DevTools Network panel. - **requestFilePath** (string) _(optional)_: The absolute or relative path to a .network-request file to save the request body to. If omitted, the body is returned inline. - **responseFilePath** (string) _(optional)_: The absolute or relative path to a .network-response file to save the response body to. If omitted, the body is returned inline. @@ -353,10 +372,11 @@ ### `list_network_requests` -**Description:** Lists the most recent requests for the currently selected page since the last navigation. +**Description:** Lists the most recent requests for the target page since the last navigation. **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **includePreservedRequests** (boolean) _(optional)_: Set to true to return the preserved requests over the last 3 navigations. - **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page. - **pageSize** (integer) _(optional)_: Maximum number of requests to return. When omitted, returns all requests. @@ -368,14 +388,15 @@ ### `evaluate_script` -**Description:** Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON, so returned values have to be JSON-serializable. +**Description:** Evaluate a JavaScript function inside the target page. Returns the response as JSON, so returned values have to be JSON-serializable. **Parameters:** -- **function** (string) **(required)**: A JavaScript function declaration to be executed by the tool in the currently selected page. +- **function** (string) **(required)**: A JavaScript function declaration to be executed by the tool in the target page. Example without arguments: `() => document.title` or `async () => await fetch("example.com")`. Example with arguments: `(el) => el.innerText` +- **pageId** (number) **(required)**: Targets a specific page by ID. - **args** (array) _(optional)_: An optional list of arguments to pass to the function. - **dialogAction** (string) _(optional)_: Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept. - **filePath** (string) _(optional)_: The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline. @@ -390,6 +411,7 @@ **Parameters:** - **msgid** (number) **(required)**: The msgid of a console message on the page from the listed console messages +- **pageId** (number) **(required)**: Targets a specific page by ID. --- @@ -399,6 +421,7 @@ **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **device** (enum: "desktop", "mobile") _(optional)_: Device to [`emulate`](#emulate). - **mode** (enum: "navigation", "snapshot") _(optional)_: "navigation" reloads & audits. "snapshot" analyzes current state. - **outputDirPath** (string) _(optional)_: Directory for reports. If omitted, uses temporary files. @@ -407,10 +430,11 @@ ### `list_console_messages` -**Description:** List all console messages for the currently selected page since the last navigation. +**Description:** List all console messages for the target page since the last navigation. **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **includePreservedMessages** (boolean) _(optional)_: Set to true to return the preserved messages over the last 3 navigations. - **includeStackTraces** (boolean) _(optional)_: Set to true to include the stack trace for each message when available. Increases the response size. - **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page. @@ -426,6 +450,7 @@ **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **filePath** (string) _(optional)_: The absolute path, or a path relative to the current working directory, to save the screenshot to instead of attaching it to the response. - **format** (enum: "png", "jpeg", "webp") _(optional)_: Type of format to save the screenshot as. Default is "png" - **fullPage** (boolean) _(optional)_: If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid. @@ -436,12 +461,13 @@ ### `take_snapshot` -**Description:** Take a text snapshot of the currently selected page based on the a11y tree. The snapshot lists page elements along with a unique +**Description:** Take a text snapshot of the target page based on the a11y tree. The snapshot lists page elements along with a unique identifier (uid). Always use the latest snapshot. Prefer taking a snapshot over taking a screenshot. The snapshot indicates the element selected in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **filePath** (string) _(optional)_: The absolute path, or a path relative to the current working directory, to save the snapshot to instead of attaching it to the response. - **verbose** (boolean) _(optional)_: Whether to include all possible information available in the full a11y tree. Default is false. @@ -449,19 +475,22 @@ in the DevTools Elements panel (if any). ### `screencast_start` -**Description:** Starts recording a screencast (video) of the selected page in specified format. (requires flag: --experimentalScreencast=true) +**Description:** Starts recording a screencast (video) of the target page in specified format. (requires flag: --experimentalScreencast=true) **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **filePath** (string) _(optional)_: Output file path (.webm,.mp4 are supported). Uses mkdtemp to generate a unique path if not provided. --- ### `screencast_stop` -**Description:** Stops the active screencast recording on the selected page. (requires flag: --experimentalScreencast=true) +**Description:** Stops the active screencast recording on the target page. (requires flag: --experimentalScreencast=true) -**Parameters:** None +**Parameters:** + +- **pageId** (number) **(required)**: Targets a specific page by ID. --- @@ -469,11 +498,12 @@ in the DevTools Elements panel (if any). ### `take_heapsnapshot` -**Description:** Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks. +**Description:** Capture a heap snapshot of the target page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks. **Parameters:** - **filePath** (string) **(required)**: A path to a .heapsnapshot file to save the heapsnapshot to. +- **pageId** (number) **(required)**: Targets a specific page by ID. --- @@ -560,9 +590,9 @@ in the DevTools Elements panel (if any). - **filePath** (string) **(required)**: A path to a .heapsnapshot file to read. - **nodeId** (number) **(required)**: The node ID to get outgoing edges for. - **excludePrimitives** (boolean) _(optional)_: Whether to exclude primitive target nodes. Default is true. -- **minRetainedSize** (number) _(optional)_: Minimum retained size in bytes for target nodes. - **pageIdx** (number) _(optional)_: The page index for pagination. - **pageSize** (number) _(optional)_: The page size for pagination. +- **retainedSize** (string) _(optional)_: Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-") for target nodes. A single value is treated as a minimum. Currently, only the lower bound is applied. - **sortBy** (enum: "retainedSize", "selfSize", "name") _(optional)_: Sort order for edges. Default is retainedSize. --- @@ -615,6 +645,25 @@ in the DevTools Elements panel (if any). --- +### `query_heapsnapshot_objects` + +**Description:** Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, retainedSize, selfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true) + +**Parameters:** + +- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read. +- **className** (string) _(optional)_: Optional regex or text matching object class name. +- **isDetached** (boolean) _(optional)_: Whether to filter for detached DOM nodes. +- **nodeType** (string) _(optional)_: Optional V8 node type filter (e.g. object, closure, string, array, code). +- **pageIdx** (number) _(optional)_: The page index for pagination. +- **pageSize** (number) _(optional)_: The page size for pagination. +- **propertyName** (string) _(optional)_: Optional property name filter for outgoing reference edges. +- **retainedSize** (string) _(optional)_: Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum. +- **selfSize** (string) _(optional)_: Inclusive self size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum. +- **sortBy** (enum: "retainedSize", "selfSize", "id") _(optional)_: Sort order for results. Default is retainedSize. + +--- + ## Opera ### `opera_authenticate_mcp_server` @@ -623,6 +672,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **server** (string) **(required)**: The MCP server name to authenticate. --- @@ -633,6 +683,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **server** (string) **(required)**: The MCP server name (from [`opera_list_mcp_servers`](#opera_list_mcp_servers)). - **tool** (string) **(required)**: The tool name to execute on the server. - **parameters** (unknown) _(optional)_: Parameters to pass to the tool. Omit if the tool takes none. @@ -645,6 +696,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **prompt** (string) **(required)**: The prompt to send to Opera AI. - **conversationId** (string) _(optional)_: Conversation ID to continue an existing conversation. Omit to start a new conversation. - **model** (string) _(optional)_: Model ID to use for the chat. Omit to use the browser default. Use [`opera_list_models`](#opera_list_models) to discover available IDs. @@ -658,6 +710,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **server** (string) **(required)**: The MCP server name to connect. --- @@ -668,6 +721,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **server** (string) **(required)**: The MCP server name to disable. --- @@ -678,6 +732,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **prompt** (string) **(required)**: The action to perform, described in natural language. - **openFullTabView** (boolean) _(optional)_: When true, activate the created do tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions. @@ -689,6 +744,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **server** (string) **(required)**: The MCP server name to enable. --- @@ -697,7 +753,9 @@ in the DevTools Elements panel (if any). **Description:** List MCP servers registered in the browser, including their connection status. Only available when connected to Opera Neon. -**Parameters:** None +**Parameters:** + +- **pageId** (number) **(required)**: Targets a specific page by ID. --- @@ -707,6 +765,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **server** (string) **(required)**: The MCP server name (from [`opera_list_mcp_servers`](#opera_list_mcp_servers)). --- @@ -715,7 +774,9 @@ in the DevTools Elements panel (if any). **Description:** List available AI models for Opera chat. Returns model IDs, display names, and which is the default. Only available when connected to Opera Neon. -**Parameters:** None +**Parameters:** + +- **pageId** (number) **(required)**: Targets a specific page by ID. --- @@ -725,6 +786,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **prompt** (string) **(required)**: Description of what to create or generate. - **conversationId** (string) _(optional)_: Conversation ID to continue an existing conversation. Omit to start a new conversation. - **openFullTabView** (boolean) _(optional)_: When true, activate the created make tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions. @@ -737,6 +799,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **server** (string) **(required)**: The MCP server name to register. - **url** (string) **(required)**: The HTTP URL of the MCP server. @@ -748,6 +811,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **prompt** (string) **(required)**: The topic or question to research. - **openFullTabView** (boolean) _(optional)_: When true, activate the created research tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions. - **researchType** (enum: "local", "one-minute", "deep") _(optional)_: Depth of research. "local" uses only on-page context, "one-minute" performs a quick web search, "deep" performs a thorough multi-source search. @@ -760,6 +824,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **server** (string) **(required)**: The MCP server name to unregister. --- @@ -826,6 +891,7 @@ in the DevTools Elements panel (if any). **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **toolName** (string) **(required)**: The name of the tool to execute - **params** (string) _(optional)_: The JSON-stringified parameters to pass to the tool @@ -841,7 +907,9 @@ following command to the script: This might be helpful when the third-party developer tools return non-serializable values or when composing third-party developer tools with additional functionality. (requires flag: --categoryExperimentalThirdParty=true) -**Parameters:** None +**Parameters:** + +- **pageId** (number) **(required)**: Targets a specific page by ID. --- @@ -855,6 +923,7 @@ third-party developer tools with additional functionality. (requires flag: --cat **Parameters:** +- **pageId** (number) **(required)**: Targets a specific page by ID. - **toolName** (string) **(required)**: The name of the WebMCP tool to execute - **input** (string) _(optional)_: The JSON-stringified parameters to pass to the WebMCP tool @@ -864,7 +933,9 @@ third-party developer tools with additional functionality. (requires flag: --cat **Description:** Lists all WebMCP tools the page exposes. (requires flag: --categoryExperimentalWebmcp=true) -**Parameters:** None +**Parameters:** + +- **pageId** (number) **(required)**: Targets a specific page by ID. --- diff --git a/gemini-extension.json b/gemini-extension.json index 7d12bcaf..13cdf143 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,11 +1,11 @@ { "name": "chrome-devtools-mcp", - "version": "1.7.0", + "version": "1.9.0", "mcpServers": { "chrome-devtools": { "command": "npx", "args": [ - "chrome-devtools-mcp@1.7.0" + "chrome-devtools-mcp@1.9.0" ] } } diff --git a/mcp.json b/mcp.json new file mode 100644 index 00000000..9e194da0 --- /dev/null +++ b/mcp.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "chrome-devtools": { + "type": "stdio", + "command": "npx", + "args": [ + "--prefix", + "${PLUGIN_DATA}", + "chrome-devtools-mcp@1.9.0" + ] + } + } +} diff --git a/package-lock.json b/package-lock.json index 1f6c3c59..2bd68289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,15 +29,15 @@ "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.43.0", "@typescript-eslint/parser": "^8.43.0", - "core-js": "3.49.0", + "core-js": "3.50.0", "eslint": "^10.7.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "globals": "^17.0.0", "lighthouse": "13.4.1", "prettier": "^3.6.2", - "puppeteer": "25.6.0", - "rollup": "4.62.4", + "puppeteer": "25.10.0", + "rollup": "4.63.1", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-license": "^3.6.0", "semver": "^7.7.4", @@ -49,6 +49,18 @@ }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" + }, + "peerDependencies": { + "@blackwell-systems/gcf": "^2.2.2", + "@toon-format/toon": "^4.1.0" + }, + "peerDependenciesMeta": { + "@blackwell-systems/gcf": { + "optional": true + }, + "@toon-format/toon": { + "optional": true + } } }, "node_modules/@apm-js-collab/code-transformer": { @@ -735,13 +747,13 @@ "license": "BSD-3-Clause" }, "node_modules/@puppeteer/browsers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.0.tgz", - "integrity": "sha512-LlBrE8oqGfU7b1Nk2d5Q1SbuPhZxTj0cJEMDPEws28OjNMELlflekmPPuf4FnK03x0ZRjKaYwJElUcKK4kyqJA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.2.tgz", + "integrity": "sha512-q2BU4YfO9h/Wt7IcWPcggpOOqLk2Tbs1hDwolvKZrweRjy751OJBKMN9zO5bfD0pzU7X/tvKw/exQds4pM/LOg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "modern-tar": "^0.8.0", + "modern-tar": "^0.8.4", "yargs": "^18.0.0" }, "bin": { @@ -860,9 +872,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", - "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", "cpu": [ "arm" ], @@ -874,9 +886,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", - "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", "cpu": [ "arm64" ], @@ -888,9 +900,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", - "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", "cpu": [ "arm64" ], @@ -902,9 +914,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", - "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", "cpu": [ "x64" ], @@ -916,9 +928,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", - "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", "cpu": [ "arm64" ], @@ -930,9 +942,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", - "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", "cpu": [ "x64" ], @@ -944,9 +956,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", - "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", "cpu": [ "arm" ], @@ -961,9 +973,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", - "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", "cpu": [ "arm" ], @@ -978,9 +990,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", - "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", "cpu": [ "arm64" ], @@ -995,9 +1007,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", - "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", "cpu": [ "arm64" ], @@ -1012,9 +1024,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", - "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", "cpu": [ "loong64" ], @@ -1029,9 +1041,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", - "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", "cpu": [ "loong64" ], @@ -1046,9 +1058,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", - "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", "cpu": [ "ppc64" ], @@ -1063,9 +1075,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", - "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", "cpu": [ "ppc64" ], @@ -1080,9 +1092,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", - "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", "cpu": [ "riscv64" ], @@ -1097,9 +1109,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", - "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", "cpu": [ "riscv64" ], @@ -1114,9 +1126,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", - "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", "cpu": [ "s390x" ], @@ -1131,9 +1143,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", - "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", "cpu": [ "x64" ], @@ -1148,9 +1160,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", - "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", "cpu": [ "x64" ], @@ -1165,9 +1177,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", - "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", "cpu": [ "x64" ], @@ -1179,9 +1191,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", - "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", "cpu": [ "arm64" ], @@ -1193,9 +1205,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", - "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", "cpu": [ "arm64" ], @@ -1207,9 +1219,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", - "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", "cpu": [ "ia32" ], @@ -1221,9 +1233,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", - "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", "cpu": [ "x64" ], @@ -1235,9 +1247,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", - "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", "cpu": [ "x64" ], @@ -2731,12 +2743,15 @@ } }, "node_modules/core-js": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", - "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", "dev": true, "hasInstallScript": true, "license": "MIT", + "engines": { + "node": "*" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -2931,9 +2946,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1653615", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1653615.tgz", - "integrity": "sha512-pGVkY3T/qXxAp2nFPodwYqOevk6ncNMSmvL8QfRCx5ZWGd6Vor7AFNmyaA8Zs6uJyP1QAfjuLandCgvSix1BNA==", + "version": "0.0.1666840", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1666840.tgz", + "integrity": "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg==", "dev": true, "license": "BSD-3-Clause" }, @@ -5509,9 +5524,9 @@ "license": "MIT" }, "node_modules/modern-tar": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.1.tgz", - "integrity": "sha512-G/OFF6yTWgdGWS5IvhcrETxsgjsUdotEtHhW2nffB5vP5uCwkhnn38qRO7h6RF4uV0UlqyHx/bq2Vd5Oco4uoQ==", + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", "dev": true, "license": "MIT", "engines": { @@ -6034,18 +6049,18 @@ } }, "node_modules/puppeteer": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.6.0.tgz", - "integrity": "sha512-TXUolDddU4AwISjOOrGk2AhJDpbM/ZDt2KvGIqz74EOk+8bKwXFo+acUvP1sQx3hUda7owOeNuuT1UnJT1o0qA==", + "version": "25.10.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.10.0.tgz", + "integrity": "sha512-9ZfkiaZDQWpGPJp9XTS+Bkn/D78hPvYmtjPfIBeybn05oeY6Jj7aiSbYdfcSQD2UMvC0vE7Yi9PSDo179euRzw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "3.2.0", + "@puppeteer/browsers": "3.2.2", "chromium-bidi": "17.0.2", - "devtools-protocol": "0.0.1653615", + "devtools-protocol": "0.0.1666840", "lilconfig": "^3.1.3", - "puppeteer-core": "25.6.0", + "puppeteer-core": "25.10.0", "typed-query-selector": "^2.12.2" }, "bin": { @@ -6056,18 +6071,18 @@ } }, "node_modules/puppeteer-core": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.6.0.tgz", - "integrity": "sha512-GJ67rjZdVQzZmD2Ab0cgttfQN9j387QYMv3t6MN3/4nmjursNt6M5Utj4/T/4y0AwNrSwJzjw6Q/zuWFEIizOg==", + "version": "25.10.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.10.0.tgz", + "integrity": "sha512-Hy5eMQshOEMil4JUUx03h5pw1HYkYCso1RG/gcpPlFSd4cYPOcopxcXEAxpLPOkOPJb9LIJtwxuj66bSdvknFg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "3.2.0", + "@puppeteer/browsers": "3.2.2", "chromium-bidi": "17.0.2", - "devtools-protocol": "0.0.1653615", + "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.2", - "ws": "^8.21.1" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { "node": ">=22.12.0" @@ -6245,9 +6260,9 @@ } }, "node_modules/rollup": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", - "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", "dev": true, "license": "MIT", "dependencies": { @@ -6262,31 +6277,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.62.4", - "@rollup/rollup-android-arm64": "4.62.4", - "@rollup/rollup-darwin-arm64": "4.62.4", - "@rollup/rollup-darwin-x64": "4.62.4", - "@rollup/rollup-freebsd-arm64": "4.62.4", - "@rollup/rollup-freebsd-x64": "4.62.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", - "@rollup/rollup-linux-arm-musleabihf": "4.62.4", - "@rollup/rollup-linux-arm64-gnu": "4.62.4", - "@rollup/rollup-linux-arm64-musl": "4.62.4", - "@rollup/rollup-linux-loong64-gnu": "4.62.4", - "@rollup/rollup-linux-loong64-musl": "4.62.4", - "@rollup/rollup-linux-ppc64-gnu": "4.62.4", - "@rollup/rollup-linux-ppc64-musl": "4.62.4", - "@rollup/rollup-linux-riscv64-gnu": "4.62.4", - "@rollup/rollup-linux-riscv64-musl": "4.62.4", - "@rollup/rollup-linux-s390x-gnu": "4.62.4", - "@rollup/rollup-linux-x64-gnu": "4.62.4", - "@rollup/rollup-linux-x64-musl": "4.62.4", - "@rollup/rollup-openbsd-x64": "4.62.4", - "@rollup/rollup-openharmony-arm64": "4.62.4", - "@rollup/rollup-win32-arm64-msvc": "4.62.4", - "@rollup/rollup-win32-ia32-msvc": "4.62.4", - "@rollup/rollup-win32-x64-gnu": "4.62.4", - "@rollup/rollup-win32-x64-msvc": "4.62.4", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" } }, @@ -7347,9 +7362,9 @@ } }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", - "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", "dev": true, "license": "Apache-2.0" }, @@ -7514,9 +7529,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.21.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", - "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index acf015a7..8585c484 100644 --- a/package.json +++ b/package.json @@ -68,15 +68,15 @@ "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.43.0", "@typescript-eslint/parser": "^8.43.0", - "core-js": "3.49.0", + "core-js": "3.50.0", "eslint": "^10.7.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "globals": "^17.0.0", "lighthouse": "13.4.1", "prettier": "^3.6.2", - "puppeteer": "25.6.0", - "rollup": "4.62.4", + "puppeteer": "25.10.0", + "rollup": "4.63.1", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-license": "^3.6.0", "semver": "^7.7.4", @@ -86,6 +86,18 @@ "yargs": "18.1.0", "zod": "^3.25.76" }, + "peerDependencies": { + "@blackwell-systems/gcf": "^2.2.2", + "@toon-format/toon": "^4.1.0" + }, + "peerDependenciesMeta": { + "@toon-format/toon": { + "optional": true + }, + "@blackwell-systems/gcf": { + "optional": true + } + }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" }, @@ -94,5 +106,12 @@ "eslint": "$eslint" }, "puppeteer-core": "$puppeteer" + }, + "allowScripts": { + "core-js": false, + "@google/genai": false, + "protobufjs": false, + "unrs-resolver": false, + "puppeteer": true } } diff --git a/plugin.json b/plugin.json new file mode 100644 index 00000000..1d2d40fb --- /dev/null +++ b/plugin.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "chrome-devtools", + "version": "1.9.0", + "description": "Reliable Chrome automation, debugging, and performance analysis for AI agents.", + "author": { + "name": "Google Chrome", + "url": "https://developer.chrome.com/" + }, + "homepage": "https://developer.chrome.com/docs/devtools/agents", + "repository": "https://github.com/ChromeDevTools/chrome-devtools-mcp", + "license": "Apache-2.0", + "keywords": [ + "browser", + "chrome", + "debugging", + "mcp", + "performance" + ] +} diff --git a/scripts/eval_gemini.ts b/scripts/eval_gemini.ts index 3fcd4c45..6041feb8 100644 --- a/scripts/eval_gemini.ts +++ b/scripts/eval_gemini.ts @@ -19,8 +19,8 @@ const ROOT_DIR = path.resolve(import.meta.dirname, '..'); const SCENARIOS_DIR = path.join(import.meta.dirname, 'eval_scenarios'); const SKILL_PATH = path.join(ROOT_DIR, 'skills', 'chrome-devtools', 'SKILL.md'); -import type {CapturedFunctionCall, TestScenario} from './eval_result.ts'; -import {Result} from './eval_result.ts'; +import type {CapturedFunctionCall, TestScenario} from './eval_result.js'; +import {Result} from './eval_result.js'; export type {CapturedFunctionCall, TestScenario}; export {Result}; @@ -41,6 +41,7 @@ async function runSingleScenario( modelId: string, debug: boolean, includeSkill: boolean, + skillPath: string = SKILL_PATH, extraServerArgs: string[] = [], ): Promise { const debugLog = (...args: unknown[]) => { @@ -62,12 +63,12 @@ async function runSingleScenario( // Prepend skill content if requested if (includeSkill) { - if (!fs.existsSync(SKILL_PATH)) { + if (!fs.existsSync(skillPath)) { throw new Error( - `Skill file not found at ${SKILL_PATH}. Please ensure the skill file exists.`, + `Skill file not found at ${skillPath}. Please ensure the skill file exists.`, ); } - const skillContent = fs.readFileSync(SKILL_PATH, 'utf-8'); + const skillContent = fs.readFileSync(skillPath, 'utf-8'); scenario.prompt = `${skillContent}\n\n---\n\n${scenario.prompt}`; } @@ -106,7 +107,7 @@ async function runSingleScenario( }); env['CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS'] = 'true'; - const args = [serverPath]; + const args = [serverPath, '--isolated']; if (!debug) { args.push('--headless'); } @@ -200,6 +201,9 @@ async function main() { type: 'boolean', default: false, }, + 'skill-path': { + type: 'string', + }, 'server-args': { type: 'string', }, @@ -210,7 +214,10 @@ async function main() { const modelId = values.model; const debug = values.debug; const repeat = values.repeat; - const includeSkill = values['include-skill']; + const includeSkill = values['include-skill'] || Boolean(values['skill-path']); + const skillPath = values['skill-path'] + ? path.resolve(ROOT_DIR, values['skill-path']) + : SKILL_PATH; const extraServerArgs = values['server-args'] ? values['server-args'].split(/\s+/) : []; @@ -245,6 +252,7 @@ async function main() { modelId, debug, includeSkill, + skillPath, extraServerArgs, ); console.log(`✔ ${path.relative(ROOT_DIR, scenarioPath)} (Run ${i})`); diff --git a/scripts/eval_result.ts b/scripts/eval_result.ts index 9b734b29..8460b56d 100644 --- a/scripts/eval_result.ts +++ b/scripts/eval_result.ts @@ -22,7 +22,7 @@ export class Result { } get hasPageIdRouting(): boolean { - return this.serverArgs.includes('--experimental-page-id-routing'); + return !this.serverArgs.includes('--no-page-id-routing'); } get remainingCalls(): CapturedFunctionCall[] { @@ -31,7 +31,7 @@ export class Result { /** * Consumes initial page navigation/setup boilerplate. - * - Ignores/skips leading list_pages calls. + * - Ignores/skips leading or trailing list_pages calls. * - Asserts that new_page or navigate_page was called. * - Determines the expected pageId. * - Returns the active pageId. @@ -49,6 +49,10 @@ export class Result { ); this.nextCallIndex++; + if (this.calls[this.nextCallIndex]?.name === 'list_pages') { + this.nextCallIndex++; + } + const isNewPage = navCall.name === 'new_page'; let pageId: number | undefined; if (this.hasPageIdRouting) { @@ -102,6 +106,6 @@ export interface TestScenario { path: string; htmlContent: string; }; - /** Extra CLI flags passed to the MCP server (e.g. '--experimental-page-id-routing'). */ + /** Extra CLI flags passed to the MCP server (e.g. '--no-page-id-routing'). */ serverArgs?: string[]; } diff --git a/scripts/eval_scenarios/cookie_banner_conformance_test.ts b/scripts/eval_scenarios/cookie_banner_conformance_test.ts new file mode 100644 index 00000000..0871ed7c --- /dev/null +++ b/scripts/eval_scenarios/cookie_banner_conformance_test.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; + +import type {TestScenario} from '../eval_gemini.js'; + +export const scenario: TestScenario = { + prompt: + 'Open in an isolated browser context called banner-test to check the cookie consent banner, take a snapshot, and click Decline.', + maxTurns: 5, + htmlRoute: { + path: '/cookie_banner_test.html', + htmlContent: ` +

Cookie Consent Test

+ + `, + }, + expectations: result => { + const newPageCall = result.calls.find(c => c.name === 'new_page'); + assert.ok( + newPageCall, + 'Expected new_page to be called for isolated context testing', + ); + assert.strictEqual( + newPageCall.args.isolatedContext, + 'banner-test', + "Expected isolatedContext to be 'banner-test'", + ); + + const pageId = result.consumePageNavigation(); + assert.ok(result.remainingCalls.length >= 2); + const snapshotCall = result.calls.find(c => c.name === 'take_snapshot'); + assert.ok(snapshotCall, 'Expected take_snapshot to be called'); + const clickCall = result.calls.find(c => c.name === 'click'); + assert.ok(clickCall, 'Expected click to be called'); + assert.ok( + clickCall.args.uid, + 'Expected click to specify a valid element uid', + ); + if (result.hasPageIdRouting && pageId !== undefined) { + assert.strictEqual(clickCall.args.pageId, pageId); + } + }, +}; diff --git a/scripts/eval_scenarios/cookie_debugging_test.ts b/scripts/eval_scenarios/cookie_debugging_test.ts new file mode 100644 index 00000000..2947a56c --- /dev/null +++ b/scripts/eval_scenarios/cookie_debugging_test.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; + +import type {TestScenario} from '../eval_gemini.js'; + +export const scenario: TestScenario = { + prompt: + 'Navigate to and inspect the network request headers to diagnose the authentication failure.', + maxTurns: 6, + htmlRoute: { + path: '/cookie_auth_test.html', + htmlContent: ` +

Authentication Test

+ + `, + }, + expectations: result => { + result.consumePageNavigation(); + const listRequestsCall = result.calls.find( + c => c.name === 'list_network_requests', + ); + assert.ok(listRequestsCall, 'Expected list_network_requests to be called'); + const getRequestCall = result.calls.find( + c => c.name === 'get_network_request', + ); + assert.ok( + getRequestCall, + 'Expected get_network_request to be called to inspect headers', + ); + }, +}; diff --git a/scripts/eval_scenarios/cookie_httponly_trigger_test.ts b/scripts/eval_scenarios/cookie_httponly_trigger_test.ts new file mode 100644 index 00000000..0411e20d --- /dev/null +++ b/scripts/eval_scenarios/cookie_httponly_trigger_test.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; + +import type {TestScenario} from '../eval_gemini.js'; + +export const scenario: TestScenario = { + prompt: + 'Reload the page and inspect the network request headers to view the active HttpOnly cookie.', + maxTurns: 4, + htmlRoute: { + path: '/cookie_httponly_test.html', + htmlContent: ` +

HttpOnly Session Test

+ `, + }, + expectations: result => { + result.consumePageNavigation(); + const getRequestCall = result.calls.find( + c => c.name === 'get_network_request', + ); + assert.ok( + getRequestCall, + 'Expected get_network_request to be called to inspect HttpOnly headers', + ); + }, +}; diff --git a/scripts/eval_scenarios/cookie_issues_audit_test.ts b/scripts/eval_scenarios/cookie_issues_audit_test.ts new file mode 100644 index 00000000..4682c558 --- /dev/null +++ b/scripts/eval_scenarios/cookie_issues_audit_test.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; + +import type {TestScenario} from '../eval_gemini.ts'; + +export const scenario: TestScenario = { + prompt: + 'Navigate to and inspect the console issues to check for cookie security or SameSite policy warnings.', + maxTurns: 3, + htmlRoute: { + path: '/cookie_issues_test.html', + htmlContent: ` +

Cookie Issues Test

+

Testing SameSite and CHIPS issues

+ `, + }, + expectations: result => { + const pageId = result.consumePageNavigation(); + assert.ok(result.remainingCalls.length >= 1); + result.assertNextCall('list_console_messages', { + types: ['issue'], + ...(result.hasPageIdRouting ? {pageId} : {}), + }); + }, +}; diff --git a/scripts/eval_scenarios/page_focus_keyboard_test.ts b/scripts/eval_scenarios/page_focus_keyboard_test.ts index f56c7c62..9812b868 100644 --- a/scripts/eval_scenarios/page_focus_keyboard_test.ts +++ b/scripts/eval_scenarios/page_focus_keyboard_test.ts @@ -9,7 +9,6 @@ import assert from 'node:assert'; import type {TestScenario} from '../eval_gemini.ts'; export const scenario: TestScenario = { - serverArgs: ['--experimental-page-id-routing'], prompt: `Open two pages in the same isolated context "session": - Page 1 at data:text/html, - Page 2 at data:text/html,

Other

diff --git a/scripts/eval_scenarios/page_id_routing_concurrent_form_test.ts b/scripts/eval_scenarios/page_id_routing_concurrent_form_test.ts new file mode 100644 index 00000000..2443f460 --- /dev/null +++ b/scripts/eval_scenarios/page_id_routing_concurrent_form_test.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; + +import type {TestScenario} from '../eval_gemini.ts'; + +const PAGE_A_URL = + 'data:text/html,
'; +const PAGE_B_URL = + 'data:text/html,
'; + +export const scenario: TestScenario = { + prompt: `Open two new pages in isolated contexts: +- Page A (isolatedContext "login_ctx") at ${PAGE_A_URL} +- Page B (isolatedContext "feedback_ctx") at ${PAGE_B_URL} + +Take a snapshot of both pages. Then, perform the following actions individually in the exact order specified below: +1. Fill "admin" into the Username input on Page A. +2. Fill "user@example.com" into the Email input on Page B. +3. Fill "secret123" into the Password input on Page A. +4. Fill "Great tools!" into the Comments textarea on Page B. + +Finally, submit both forms by clicking the submit buttons on Page A and Page B.`, + maxTurns: 15, + expectations: result => { + const newPages = result.calls.filter(c => c.name === 'new_page'); + assert.strictEqual(newPages.length, 2, 'Should open 2 pages'); + const snapshots = result.calls.filter(c => c.name === 'take_snapshot'); + assert.ok(snapshots.length >= 2, 'Should snapshot both pages'); + + const fills = result.calls.filter(c => c.name === 'fill'); + assert.strictEqual( + fills.length, + 4, + 'Should fill 4 inputs across the forms', + ); + + // Verify that each fill targeted the correct pageId based on its value/element + for (const fill of fills) { + const value = String(fill.args['value'] || ''); + if (value === 'admin' || value === 'secret123') { + assert.strictEqual( + fill.args['pageId'], + 2, + `Filling '${value}' should target login page (pageId 2)`, + ); + } else if (value === 'user@example.com' || value === 'Great tools!') { + assert.strictEqual( + fill.args['pageId'], + 3, + `Filling '${value}' should target feedback page (pageId 3)`, + ); + } else { + assert.fail(`Unexpected fill value: ${value}`); + } + } + + // Verify no select_page calls were made between the interleaved actions + const selects = result.calls.filter(c => c.name === 'select_page'); + assert.strictEqual( + selects.length, + 0, + 'Should not use select_page when pageId routing is active', + ); + }, +}; diff --git a/scripts/eval_scenarios/page_id_routing_cross_page_state_test.ts b/scripts/eval_scenarios/page_id_routing_cross_page_state_test.ts new file mode 100644 index 00000000..9f0fda54 --- /dev/null +++ b/scripts/eval_scenarios/page_id_routing_cross_page_state_test.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; + +import type {TestScenario} from '../eval_gemini.ts'; + +const PAGE_COUNTER = + 'data:text/html,

Counter Page

0
'; +const PAGE_INPUT = + 'data:text/html,

Input Page

'; + +export const scenario: TestScenario = { + prompt: `Open two new pages: +- Page A at ${PAGE_COUNTER} +- Page B at ${PAGE_INPUT} + +Take snapshot of both pages and then perform the following steps: +1. Click the "Increment" button on Page A twice. +2. Take a snapshot of Page A to read the updated counter value. +3. On Page B, fill that exact counter value into the input field, and click the "Submit" button.`, + maxTurns: 12, + expectations: result => { + const newPages = result.calls.filter(c => c.name === 'new_page'); + assert.strictEqual(newPages.length, 2, 'Should open 2 pages'); + + const clicks = result.calls.filter(c => c.name === 'click'); + assert.ok( + clicks.length >= 3, + 'Should click increment twice and then submit', + ); + + // First click and second click should target the increment button on Page A + const counterClicks = clicks.filter(c => c.args['pageId'] === 2); + assert.strictEqual( + counterClicks.length, + 2, + 'Should click increment button on Page A exactly twice', + ); + + // There should be a snapshot of Page A to read the value + const snapshots = result.calls.filter(c => c.name === 'take_snapshot'); + const counterSnapshot = snapshots.find(s => s.args['pageId'] === 2); + assert.ok( + counterSnapshot, + 'Should snapshot Page A to read the counter value', + ); + + // The fill and final click should target Page B + const fills = result.calls.filter( + c => c.name === 'fill' || c.name === 'fill_form', + ); + assert.strictEqual( + fills.length, + 1, + 'Should fill the input field on Page B', + ); + assert.strictEqual(fills[0].args['pageId'], 3, 'Fill should target Page B'); + + let filledValue = ''; + if (fills[0].name === 'fill_form') { + const elements = fills[0].args['elements']; + assert.ok(Array.isArray(elements), 'elements should be an array'); + filledValue = elements[0]['value']; + } else if (fills[0].name === 'fill') { + filledValue = String(fills[0].args['value']); + } + + assert.strictEqual( + filledValue, + '2', + 'Should fill the value "2" (since we incremented twice)', + ); + + const finalClick = clicks[clicks.length - 1]; + assert.strictEqual( + finalClick.args['pageId'], + 3, + 'Submit click should target Page B', + ); + + // Verify no select_page calls were made between the interleaved actions + const selects = result.calls.filter(c => c.name === 'select_page'); + assert.strictEqual( + selects.length, + 0, + 'Should not use select_page when pageId routing is active', + ); + }, +}; diff --git a/scripts/eval_scenarios/page_id_routing_test.ts b/scripts/eval_scenarios/page_id_routing_test.ts index a65c9e83..864c6bf6 100644 --- a/scripts/eval_scenarios/page_id_routing_test.ts +++ b/scripts/eval_scenarios/page_id_routing_test.ts @@ -9,7 +9,6 @@ import assert from 'node:assert'; import type {TestScenario} from '../eval_gemini.ts'; export const scenario: TestScenario = { - serverArgs: ['--experimental-page-id-routing'], prompt: `Open two new pages in isolated contexts: - Page A (isolatedContext "contextA") at data:text/html, - Page B (isolatedContext "contextB") at data:text/html, diff --git a/scripts/generate-cli.ts b/scripts/generate-cli.ts index 2afd354f..15cf2d8d 100644 --- a/scripts/generate-cli.ts +++ b/scripts/generate-cli.ts @@ -12,18 +12,17 @@ import path from 'node:path'; import {Client} from '@modelcontextprotocol/sdk/client/index.js'; import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js'; -import {parseArguments} from '../build/src/bin/chrome-devtools-mcp-cli-options.js'; -import {buildFlag} from '../build/src/index.js'; +import {mcpOptions, parseArguments} from '../build/src/config/mcp-options.js'; import { - labels, - ToolCategory, - OFF_BY_DEFAULT_CATEGORIES, -} from '../build/src/tools/categories.js'; + isCategoryOffByDefault, + categoryToFlagName, +} from '../build/src/config/category-options.js'; +import {labels, ToolCategory} from '../build/src/tools/categories.js'; import {createTools} from '../build/src/tools/tools.js'; const OUTPUT_PATH = path.join( import.meta.dirname, - '../src/bin/chrome-devtools-cli-options.ts', + '../src/config/cli-options.ts', ); async function fetchTools() { @@ -110,7 +109,7 @@ function schemaToCLIOptions(schema: JsonSchema): CliOption[] { async function generateCli() { const tools = await fetchTools(); - const staticTools = createTools(parseArguments()); + const staticTools = createTools(parseArguments('0.0.0', [], {})); const toolNameToCategoryEnum = new Map(); const toolNameToConditions = new Map(); @@ -168,15 +167,18 @@ async function generateCli() { let description = tool.description; const requiredFlags: string[] = []; - const isOffByDefault = OFF_BY_DEFAULT_CATEGORIES.includes(categoryEnum); + const isOffByDefault = isCategoryOffByDefault(categoryEnum); if (isOffByDefault) { - const categoryFlag = buildFlag(categoryEnum); + const categoryFlag = categoryToFlagName(categoryEnum); requiredFlags.push(`--${categoryFlag}=true`); } const conditions = toolNameToConditions.get(tool.name) || []; for (const condition of conditions) { - requiredFlags.push(`--${condition}=true`); + const option = mcpOptions[condition as keyof typeof mcpOptions]; + if (!option || !('default' in option) || option.default !== true) { + requiredFlags.push(`--${condition}=true`); + } } if (requiredFlags.length > 0) { @@ -199,7 +201,12 @@ async function generateCli() { * Modified by Opera Software AS. */ -// NOTE: do not edit manually. Auto-generated by 'npm run cli:generate'. +/** + * @fileoverview + * WARNING: This file is auto-generated by 'npm run cli:generate'. + * Do not edit this file manually. + */ + export interface ArgDef { name: string; diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 10bfdf33..e496d335 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -8,19 +8,20 @@ import fs from 'node:fs'; import type {Tool} from '@modelcontextprotocol/sdk/types.js'; -import {cliOptions} from '../build/src/bin/chrome-devtools-mcp-cli-options.js'; -import type {ParsedArguments} from '../build/src/bin/chrome-devtools-mcp-cli-options.js'; -import {buildFlag} from '../build/src/index.js'; import { - ToolCategory, - OFF_BY_DEFAULT_CATEGORIES, - labels, -} from '../build/src/tools/categories.js'; + mcpOptions, + type ParsedArguments, +} from '../build/src/config/mcp-options.js'; +import { + isCategoryOffByDefault, + categoryToFlagName, +} from '../build/src/config/category-options.js'; +import {ToolCategory, labels} from '../build/src/tools/categories.js'; +import {pageIdSchema} from '../build/src/tools/ToolDefinition.js'; import {createTools} from '../build/src/tools/tools.js'; const OUTPUT_PATH = './docs/tool-reference.md'; const SLIM_OUTPUT_PATH = './docs/slim-tool-reference.md'; -const README_PATH = './README.md'; // Extend the MCP Tool type to include our annotations interface ToolWithAnnotations extends Tool { @@ -92,9 +93,19 @@ function addCrossLinks(text: string, tools: ToolWithAnnotations[]): string { return result; } +function hasOffByDefaultConditions(tool: ToolWithAnnotations): boolean { + for (const condition of tool.annotations?.conditions || []) { + const option = mcpOptions[condition as keyof typeof mcpOptions]; + if (!option || !('default' in option) || option.default !== true) { + return true; + } + } + return false; +} + function sortTools(a: ToolWithAnnotations, b: ToolWithAnnotations): number { - const aHasConditions = Boolean(a.annotations?.conditions?.length > 0); - const bHasConditions = Boolean(b.annotations?.conditions?.length > 0); + const aHasConditions = hasOffByDefaultConditions(a); + const bHasConditions = hasOffByDefaultConditions(b); if (aHasConditions && !bHasConditions) { return 1; @@ -106,61 +117,30 @@ function sortTools(a: ToolWithAnnotations, b: ToolWithAnnotations): number { return a.name.localeCompare(b.name); } -function generateToolsTOC( - categories: Record, - sortedCategories: string[], -): string { - let toc = ''; - - for (const category of sortedCategories) { - const categoryTools = categories[category]; - const categoryName = labels[category]; - toc += `- **${categoryName}** (${categoryTools.length} tools)\n`; - - // Sort tools within category for TOC - categoryTools.sort(sortTools); - for (const tool of categoryTools) { - const anchorLink = tool.name.toLowerCase(); - toc += ` - [\`${tool.name}\`](docs/tool-reference.md#${anchorLink})\n`; - } - } - - return toc; -} - -function updateReadmeWithToolsTOC(toolsTOC: string): void { - const readmeContent = fs.readFileSync(README_PATH, 'utf8'); - - const beginMarker = ''; - const endMarker = ''; - - const beginIndex = readmeContent.indexOf(beginMarker); - const endIndex = readmeContent.indexOf(endMarker); - - if (beginIndex === -1 || endIndex === -1) { - console.warn('Could not find auto-generated tools markers in README.md'); - return; - } - - const before = readmeContent.substring(0, beginIndex + beginMarker.length); - const after = readmeContent.substring(endIndex); - - const updatedContent = before + '\n\n' + toolsTOC + '\n' + after; - - fs.writeFileSync(README_PATH, updatedContent); - console.log('Updated README.md with tools table of contents'); +interface OptionConfig { + hidden?: boolean; + alias?: string; + description?: string; + describe?: string; + type?: string; + choices?: string[]; + default?: unknown; } function generateConfigOptionsMarkdown(): string { let markdown = ''; - for (const [optionName, optionConfig] of Object.entries(cliOptions)) { + for (const [optionName, optionConfig] of Object.entries( + mcpOptions as Record, + )) { // Skip hidden options if (optionConfig.hidden) { continue; } - const aliasText = optionConfig.alias ? `, \`-${optionConfig.alias}\`` : ''; + const aliasText = optionConfig.alias + ? `, \`${optionConfig.alias.length === 1 ? '-' : '--'}${optionConfig.alias}\`` + : ''; const description = optionConfig.description || optionConfig.describe || ''; // Convert camelCase to dash-case @@ -185,7 +165,7 @@ function generateConfigOptionsMarkdown(): string { } // Add default if available - markdown += ` - **Default:** \`${optionConfig.default ?? 'false'}\`\n`; + markdown += ` - **Default:** \`${optionConfig.defaultDescription ?? optionConfig.default ?? 'false'}\`\n`; markdown += '\n'; } @@ -193,8 +173,9 @@ function generateConfigOptionsMarkdown(): string { return markdown.trim(); } -function updateReadmeWithOptionsMarkdown(optionsMarkdown: string): void { - const readmeContent = fs.readFileSync(README_PATH, 'utf8'); +function updateConfigurationWithOptionsMarkdown(optionsMarkdown: string): void { + const configPath = './docs/configuration.md'; + const readmeContent = fs.readFileSync(configPath, 'utf8'); const beginMarker = ''; const endMarker = ''; @@ -203,7 +184,9 @@ function updateReadmeWithOptionsMarkdown(optionsMarkdown: string): void { const endIndex = readmeContent.indexOf(endMarker); if (beginIndex === -1 || endIndex === -1) { - console.warn('Could not find auto-generated options markers in README.md'); + console.warn( + 'Could not find auto-generated options markers in ./docs/configuration.md', + ); return; } @@ -212,8 +195,8 @@ function updateReadmeWithOptionsMarkdown(optionsMarkdown: string): void { const updatedContent = before + '\n\n' + optionsMarkdown + '\n\n' + after; - fs.writeFileSync(README_PATH, updatedContent); - console.log('Updated README.md with options markdown'); + fs.writeFileSync(configPath, updatedContent); + console.log('Updated configuration.md with options markdown'); } // Helper to convert Zod schema to JSON schema-like object for docs @@ -328,8 +311,8 @@ async function generateReference( markdown += `## ${categoryName}\n\n`; - if (OFF_BY_DEFAULT_CATEGORIES.includes(category)) { - const flagName = `--${buildFlag(category)}`; + if (isCategoryOffByDefault(category)) { + const flagName = `--${categoryToFlagName(category)}`; markdown += `> NOTE: The ${categoryName} category is not active by default. Use the '${flagName}' flag.\n\n`; } @@ -346,15 +329,18 @@ async function generateReference( const requiredFlags: string[] = []; - const isOffByDefault = OFF_BY_DEFAULT_CATEGORIES.includes(category); + const isOffByDefault = isCategoryOffByDefault(category); if (isOffByDefault) { - const categoryFlag = buildFlag(category); + const categoryFlag = categoryToFlagName(category); requiredFlags.push(`--${categoryFlag}=true`); } const conditions = tool.annotations?.conditions || []; for (const condition of conditions) { - requiredFlags.push(`--${condition}=true`); + const option = mcpOptions[condition as keyof typeof mcpOptions]; + if (!option || !('default' in option) || option.default !== true) { + requiredFlags.push(`--${condition}=true`); + } } if (requiredFlags.length > 0) { @@ -432,7 +418,7 @@ async function generateReference( } // eslint-disable-next-line @typescript-eslint/no-explicit-any -function getToolsAndCategories(tools: any) { +function getToolsAndCategories(tools: any, slim = false) { // Convert ToolDefinitions to ToolWithAnnotations const toolsWithAnnotations: ToolWithAnnotations[] = tools .filter(tool => { @@ -453,8 +439,12 @@ function getToolsAndCategories(tools: any) { const properties: Record = {}; const required: string[] = []; + const toolSchema = { + ...tool.schema, + ...(tool.pageScoped && !slim ? pageIdSchema : {}), + }; for (const [key, schema] of Object.entries( - tool.schema as unknown as Record, + toolSchema as unknown as Record, )) { const info = getZodTypeInfo(schema); properties[key] = info; @@ -487,8 +477,8 @@ function getToolsAndCategories(tools: any) { // Sort categories using the enum order const categoryOrder = Object.values(ToolCategory); const sortedCategories = Object.keys(categories).sort((a, b) => { - const aOff = OFF_BY_DEFAULT_CATEGORIES.includes(a as ToolCategory); - const bOff = OFF_BY_DEFAULT_CATEGORIES.includes(b as ToolCategory); + const aOff = isCategoryOffByDefault(a); + const bOff = isCategoryOffByDefault(b); if (aOff !== bOff) { return aOff ? 1 : -1; @@ -518,7 +508,9 @@ async function generateToolDocumentation(): Promise { { const {toolsWithAnnotations, categories, sortedCategories} = - getToolsAndCategories(createTools({slim: false} as ParsedArguments)); + getToolsAndCategories( + createTools({slim: false, pageIdRouting: true} as ParsedArguments), + ); await generateReference( 'Chrome DevTools MCP Tool Reference', OUTPUT_PATH, @@ -526,15 +518,14 @@ async function generateToolDocumentation(): Promise { categories, sortedCategories, ); - - // Generate tools TOC and update README - const toolsTOC = generateToolsTOC(categories, sortedCategories); - updateReadmeWithToolsTOC(toolsTOC); } { const {toolsWithAnnotations, categories, sortedCategories} = - getToolsAndCategories(createTools({slim: true} as ParsedArguments)); + getToolsAndCategories( + createTools({slim: true} as ParsedArguments), + true, + ); await generateReference( 'Chrome DevTools MCP Slim Tool Reference', SLIM_OUTPUT_PATH, @@ -546,7 +537,7 @@ async function generateToolDocumentation(): Promise { // Generate and update configuration options const optionsMarkdown = generateConfigOptionsMarkdown(); - updateReadmeWithOptionsMarkdown(optionsMarkdown); + updateConfigurationWithOptionsMarkdown(optionsMarkdown); process.exit(0); } catch (error) { console.error('Error generating documentation:', error); diff --git a/scripts/update_metrics.ts b/scripts/update_metrics.ts index f6bb5ef0..75037831 100644 --- a/scripts/update_metrics.ts +++ b/scripts/update_metrics.ts @@ -7,10 +7,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { - cliOptions, - parseArguments, -} from '../build/src/bin/chrome-devtools-mcp-cli-options.js'; +import {mcpOptions, parseArguments} from '../build/src/config/mcp-options.js'; import {ErrorCode} from '../build/src/telemetry/errors.js'; import { getPossibleFlagMetrics, @@ -92,7 +89,7 @@ function writeFlagUsageMetrics() { } } - const newMetrics = getPossibleFlagMetrics(cliOptions); + const newMetrics = getPossibleFlagMetrics(mcpOptions); const mergedMetrics = applyToExisting( existingMetrics, newMetrics, diff --git a/skills/a11y-debugging/SKILL.md b/skills/a11y-debugging/SKILL.md index 183a78f2..324c81c2 100644 --- a/skills/a11y-debugging/SKILL.md +++ b/skills/a11y-debugging/SKILL.md @@ -5,6 +5,8 @@ description: Uses Chrome DevTools MCP for accessibility (a11y) debugging and aud ## Core Concepts +**Page Targeting**: Page-scoped tools (`take_snapshot`, `list_console_messages`, `evaluate_script`, `press_key`, `take_screenshot`, `lighthouse_audit`, etc.) require a `pageId` parameter. Retrieve available page IDs using `list_pages` or from `new_page`. + **Accessibility Tree vs DOM**: Visually hiding an element (e.g., `CSS opacity: 0`) behaves differently for screen readers than `display: none` or `aria-hidden="true"`. The `take_snapshot` tool returns the accessibility tree of the page, which represents what assistive technologies "see", making it the most reliable source of truth for semantic structure. **Reading web.dev documentation**: If you need to research specific accessibility guidelines (like `https://web.dev/articles/accessible-tap-targets`), you can append `.md.txt` to the URL (e.g., `https://web.dev/articles/accessible-tap-targets.md.txt`) to fetch the clean, raw markdown version. This is much easier to read! diff --git a/skills/chrome-devtools-cli/SKILL.md b/skills/chrome-devtools-cli/SKILL.md index 6b7749a3..093c3a36 100644 --- a/skills/chrome-devtools-cli/SKILL.md +++ b/skills/chrome-devtools-cli/SKILL.md @@ -11,9 +11,9 @@ _Note: If this is your very first time using the CLI, see [references/installati ## AI Workflow -1. **Execute**: Run tools directly (e.g., `chrome-devtools list_pages`). The background server starts implicitly; **do not** run `start`/`status`/`stop` before each use. -2. **Inspect**: Use `take_snapshot` to get an element ``. -3. **Act**: Use `click`, `fill`, etc. State persists across commands. +1. **Execute**: Run tools directly. If you don't know the target page's ID, run `chrome-devtools list_pages` to find it. The background server starts implicitly; **do not** run `start`/`status`/`stop` before each use. +2. **Inspect**: Use `chrome-devtools take_snapshot ` to get an element ``. +3. **Act**: Use `chrome-devtools click `, `chrome-devtools fill `, etc. State persists across commands. Snapshot example: @@ -24,12 +24,7 @@ uid=1_0 RootWebArea "Example Domain" url="https://example.com/" ## Permissions & File Access -By default, the server only has access to the **OS temp directory** (as defined by Node APIs, `os.tmpdir()`). File-saving parameters (`--filePath`, `--outputDirPath`) and `upload_file` outside the temp directory require unrestricted filesystem access: - -```bash -# Start daemon with full filesystem access -chrome-devtools start --allowUnrestrictedPaths=true -``` +By default, the CLI has full filesystem access (`--allowUnrestrictedPaths=true`), allowing file-saving parameters (`--filePath`, `--outputDirPath`) and `upload_file` to access files anywhere on the system. Pass `--allowUnrestrictedPaths=false` if you want to restrict file access to the OS temp directory. ## Command Usage @@ -44,23 +39,23 @@ chrome-devtools [arguments] [flags] ## Input Automation ( from snapshot) ```bash -chrome-devtools take_snapshot # Take a text snapshot of the page to get UIDs for elements -chrome-devtools click "id" # Clicks on the provided element -chrome-devtools click "id" --dblClick true --includeSnapshot true # Double clicks and returns a snapshot -chrome-devtools drag "src" "dst" # Drag an element onto another element -chrome-devtools drag "src" "dst" --includeSnapshot true # Drag an element and return a snapshot -chrome-devtools fill "id" "text" # Type text into an input, textarea, or select an option -chrome-devtools fill "id" "text" --includeSnapshot true # Fill an element and return a snapshot -chrome-devtools handle_dialog accept # Handle a browser dialog (accept/dismiss) -chrome-devtools handle_dialog dismiss --promptText "hi" # Dismiss a dialog with prompt text -chrome-devtools hover "id" # Hover over the provided element -chrome-devtools hover "id" --includeSnapshot true # Hover over an element and return a snapshot -chrome-devtools press_key "Enter" # Press a key or key combination ("Control+A", "Escape") -chrome-devtools press_key "Control+A" --includeSnapshot true # Press a key and return a snapshot -chrome-devtools type_text "hello" # Type text using keyboard into a focused input -chrome-devtools type_text "hello" --submitKey "Enter" # Type text and press a submit key -chrome-devtools upload_file "id" "file.txt" # Upload a file through a provided element -chrome-devtools upload_file "id" "file.txt" --includeSnapshot true # Upload a file and return a snapshot +chrome-devtools take_snapshot 1 # Take a text snapshot of the page to get UIDs for elements +chrome-devtools click 1 "id" # Clicks on the provided element +chrome-devtools click 1 "id" --dblClick true --includeSnapshot true # Double clicks and returns a snapshot +chrome-devtools drag 1 "src" "dst" # Drag an element onto another element +chrome-devtools drag 1 "src" "dst" --includeSnapshot true # Drag an element and return a snapshot +chrome-devtools fill 1 "id" "text" # Type text into an input, textarea, or select an option +chrome-devtools fill 1 "id" "text" --includeSnapshot true # Fill an element and return a snapshot +chrome-devtools handle_dialog 1 accept # Handle a browser dialog (accept/dismiss) +chrome-devtools handle_dialog 1 dismiss --promptText "hi" # Dismiss a dialog with prompt text +chrome-devtools hover 1 "id" # Hover over the provided element +chrome-devtools hover 1 "id" --includeSnapshot true # Hover over an element and return a snapshot +chrome-devtools press_key 1 "Enter" # Press a key or key combination ("Control+A", "Escape") +chrome-devtools press_key 1 "Control+A" --includeSnapshot true # Press a key and return a snapshot +chrome-devtools type_text 1 "hello" # Type text using keyboard into a focused input +chrome-devtools type_text 1 "hello" --submitKey "Enter" # Type text and press a submit key +chrome-devtools upload_file 1 "id" "file.txt" # Upload a file through a provided element +chrome-devtools upload_file 1 "id" "file.txt" --includeSnapshot true # Upload a file and return a snapshot ``` ## Navigation @@ -68,11 +63,11 @@ chrome-devtools upload_file "id" "file.txt" --includeSnapshot true # Upload a fi ```bash chrome-devtools close_page 1 # Closes the page by its index chrome-devtools list_pages # Get a list of pages open in the browser -chrome-devtools navigate_page --url "https://example.com" # Navigates the currently selected page to a URL -chrome-devtools navigate_page --type "reload" --ignoreCache true # Reload page ignoring cache -chrome-devtools navigate_page --url "https://example.com" --timeout 5000 # Navigate with a timeout -chrome-devtools navigate_page --handleBeforeUnload "accept" # Handle before unload dialog -chrome-devtools navigate_page --type "back" --initScript "foo()" # Navigate back and run an init script +chrome-devtools navigate_page 1 --url "https://example.com" # Navigates the currently selected page to a URL +chrome-devtools navigate_page 1 --type "reload" --ignoreCache true # Reload page ignoring cache +chrome-devtools navigate_page 1 --url "https://example.com" --timeout 5000 # Navigate with a timeout +chrome-devtools navigate_page 1 --handleBeforeUnload "accept" # Handle before unload dialog +chrome-devtools navigate_page 1 --type "back" --initScript "foo()" # Navigate back and run an init script chrome-devtools new_page "https://example.com" # Creates a new page chrome-devtools new_page "https://example.com" --background true --timeout 5000 # Create new page in background chrome-devtools new_page "https://example.com" --isolatedContext "ctx" # Create new page with isolated context @@ -83,27 +78,27 @@ chrome-devtools select_page 1 --bringToFront true # Select a page and bring it t ## Emulation ```bash -chrome-devtools emulate --networkConditions "Offline" # Emulate network conditions -chrome-devtools emulate --cpuThrottlingRate 4 --geolocation "0x0" # Emulate CPU throttling and geolocation -chrome-devtools emulate --colorScheme "dark" --viewport "1920x1080" # Emulate color scheme and viewport -chrome-devtools emulate --userAgent "Mozilla/5.0..." # Emulate user agent -chrome-devtools resize_page 1920 1080 # Resizes the selected page's window +chrome-devtools emulate 1 --networkConditions "Offline" # Emulate network conditions +chrome-devtools emulate 1 --cpuThrottlingRate 4 --geolocation "0x0" # Emulate CPU throttling and geolocation +chrome-devtools emulate 1 --colorScheme "dark" --viewport "1920x1080" # Emulate color scheme and viewport +chrome-devtools emulate 1 --userAgent "Mozilla/5.0..." # Emulate user agent +chrome-devtools resize_page 1 1920 1080 # Resizes the selected page's window ``` ## Performance ```bash -chrome-devtools performance_analyze_insight "1" "LCPBreakdown" # Get more details on a specific Performance Insight -chrome-devtools performance_start_trace true false # Starts a performance trace recording (reload, autoStop) -chrome-devtools performance_start_trace true true --filePath "t.json.gz" # Start trace and save to a file -chrome-devtools performance_stop_trace # Stops the active performance trace -chrome-devtools performance_stop_trace --filePath "t.json.gz" # Stop trace and save to a file +chrome-devtools performance_analyze_insight 1 "1" "LCPBreakdown" # Get more details on a specific Performance Insight (pageId, insightSetId, insightName) +chrome-devtools performance_start_trace 1 --reload true --autoStop false # Starts a performance trace recording (reload, autoStop) +chrome-devtools performance_start_trace 1 --reload true --autoStop true --filePath "t.json.gz" # Start trace and save to a file +chrome-devtools performance_stop_trace 1 # Stops the active performance trace +chrome-devtools performance_stop_trace 1 --filePath "t.json.gz" # Stop trace and save to a file ``` ## Memory ```bash -chrome-devtools take_heapsnapshot "./snap.heapsnapshot" # Capture a memory heap snapshot +chrome-devtools take_heapsnapshot 1 "./snap.heapsnapshot" # Capture a memory heap snapshot ``` ### Memory Debugging (requires `--memoryDebugging=true`) @@ -125,33 +120,33 @@ chrome-devtools close_heapsnapshot "./snap.heapsnapshot" # Free memory from load ## Network ```bash -chrome-devtools get_network_request # Get the currently selected network request -chrome-devtools get_network_request --reqid 1 --requestFilePath "req.md" # Get request by id and save to file -chrome-devtools get_network_request --responseFilePath "res.md" # Save response body to file -chrome-devtools list_network_requests # List all network requests -chrome-devtools list_network_requests --pageSize 50 --pageIdx 0 # List network requests with pagination -chrome-devtools list_network_requests --resourceTypes Fetch # Filter requests by resource type -chrome-devtools list_network_requests --includePreservedRequests true # Include preserved requests +chrome-devtools get_network_request 1 # Get the currently selected network request for page 1 +chrome-devtools get_network_request 1 --reqid 1 --requestFilePath "req.md" # Get request by id and save to file +chrome-devtools get_network_request 1 --responseFilePath "res.md" # Save response body to file +chrome-devtools list_network_requests 1 # List all network requests for page 1 +chrome-devtools list_network_requests 1 --pageSize 50 --pageIdx 0 # List network requests with pagination +chrome-devtools list_network_requests 1 --resourceTypes Fetch # Filter requests by resource type +chrome-devtools list_network_requests 1 --includePreservedRequests true # Include preserved requests ``` ## Debugging & Inspection ```bash -chrome-devtools evaluate_script "() => document.title" # Evaluate a JavaScript function on the page -chrome-devtools evaluate_script "(a) => a.innerText" --args 1_4 # Evaluate JS with UID arguments -chrome-devtools get_console_message 1 # Gets a console message by its ID -chrome-devtools lighthouse_audit --mode "navigation" # Run Lighthouse audit for navigation -chrome-devtools lighthouse_audit --mode "snapshot" --device "mobile" # Run Lighthouse audit for a snapshot on mobile -chrome-devtools lighthouse_audit --outputDirPath ./out # Run Lighthouse audit and save reports -chrome-devtools list_console_messages # List all console messages -chrome-devtools list_console_messages --pageSize 20 --pageIdx 1 # List console messages with pagination -chrome-devtools list_console_messages --types error --types info # Filter console messages by type -chrome-devtools list_console_messages --includePreservedMessages true # Include preserved messages -chrome-devtools take_screenshot # Take a screenshot of the page viewport -chrome-devtools take_screenshot --fullPage true --format "jpeg" --quality 80 # Take a full page screenshot as JPEG with quality -chrome-devtools take_screenshot --uid "id" --filePath "s.png" # Take a screenshot of an element -chrome-devtools take_snapshot # Take a text snapshot of the page from the a11y tree -chrome-devtools take_snapshot --verbose true --filePath "s.txt" # Take a verbose snapshot and save to file +chrome-devtools evaluate_script "() => document.title" --pageId 1 # Evaluate a JavaScript function on page 1 +chrome-devtools evaluate_script "(a) => a.innerText" --pageId 1 --args 1_4 # Evaluate JS with UID arguments on page 1 +chrome-devtools get_console_message 1 1 # Gets a console message by its ID +chrome-devtools lighthouse_audit 1 --mode "navigation" # Run Lighthouse audit for navigation +chrome-devtools lighthouse_audit 1 --mode "snapshot" --device "mobile" # Run Lighthouse audit for a snapshot on mobile +chrome-devtools lighthouse_audit 1 --outputDirPath ./out # Run Lighthouse audit and save reports +chrome-devtools list_console_messages 1 # List all console messages +chrome-devtools list_console_messages 1 --pageSize 20 --pageIdx 1 # List console messages with pagination +chrome-devtools list_console_messages 1 --types error --types info # Filter console messages by type +chrome-devtools list_console_messages 1 --includePreservedMessages true # Include preserved messages +chrome-devtools take_screenshot 1 # Take a screenshot of the page viewport +chrome-devtools take_screenshot 1 --fullPage true --format "jpeg" --quality 80 # Take a full page screenshot as JPEG with quality +chrome-devtools take_screenshot 1 --uid "id" --filePath "s.png" # Take a screenshot of an element +chrome-devtools take_snapshot 1 # Take a text snapshot of the page from the a11y tree +chrome-devtools take_snapshot 1 --verbose true --filePath "s.txt" # Take a verbose snapshot and save to file ``` ## Extensions @@ -178,20 +173,19 @@ chrome-devtools uninstall_pwa "https://example.com/" # Uninstall PWA and close w Experimental tools are disabled by default. Enable them with the corresponding flag during `start`. ```bash -chrome-devtools click_at 100 200 # Clicks at the provided coordinates (requires --experimentalVision=true) -chrome-devtools screencast_start --filePath "screen.mp4" # Starts a screencast recording (requires --experimentalScreencast=true and ffmpeg) -chrome-devtools screencast_stop # Stops the active screencast -chrome-devtools list_webmcp_tools # List all WebMCP tools (requires --categoryExperimentalWebmcp=true) -chrome-devtools execute_webmcp_tool "tool_name" '{"arg":"val"}' # Execute a WebMCP tool (requires --categoryExperimentalWebmcp=true) -chrome-devtools list_3p_developer_tools # List third-party developer tools (requires --categoryExperimentalThirdParty=true) -chrome-devtools execute_3p_developer_tool "tool_name" '{"arg":"val"}' # Execute third-party developer tool (requires --categoryExperimentalThirdParty=true) +chrome-devtools click_at 1 100 200 # Clicks at the provided coordinates on page 1 (requires --experimentalVision=true) +chrome-devtools screencast_start 1 --filePath "screen.mp4" # Starts a screencast recording on page 1 (requires --experimentalScreencast=true and ffmpeg) +chrome-devtools screencast_stop 1 # Stops the active screencast on page 1 +chrome-devtools list_webmcp_tools 1 # List all WebMCP tools on page 1 (requires --categoryExperimentalWebmcp=true) +chrome-devtools execute_webmcp_tool 1 "tool_name" --input '{"arg":"val"}' # Execute a WebMCP tool on page 1 (requires --categoryExperimentalWebmcp=true) +chrome-devtools list_3p_developer_tools 1 # List third-party developer tools on page 1 (requires --categoryExperimentalThirdParty=true) +chrome-devtools execute_3p_developer_tool 1 "tool_name" --params '{"arg":"val"}' # Execute third-party developer tool on page 1 (requires --categoryExperimentalThirdParty=true) ``` ## Service Management ```bash chrome-devtools start # Start or restart chrome-devtools-mcp -chrome-devtools start --allowUnrestrictedPaths=true # Start with full filesystem access chrome-devtools start --headless=false # Start with visible browser window chrome-devtools status # Checks if chrome-devtools-mcp is running chrome-devtools stop # Stop chrome-devtools-mcp if any diff --git a/skills/chrome-devtools/SKILL.md b/skills/chrome-devtools/SKILL.md index d62f66ec..d74727c3 100644 --- a/skills/chrome-devtools/SKILL.md +++ b/skills/chrome-devtools/SKILL.md @@ -11,7 +11,8 @@ Addional tooling can be enabled by providing the following flags: - For extension tooling, use the `--categoryExtensions` flag. - For memory tooling, use the `--memoryDebugging` flag. -**Page selection**: Tools operate on the currently selected page. Use `list_pages` to see available pages, then `select_page` to switch context. +**Page targeting**: Page-scoped tools require a `pageId` parameter to target a specific page. Use `list_pages` to see available pages and their IDs (e.g. `pageId: 1`), or use the ID returned when creating a page with `new_page`. +Note: For `evaluate_script`, `pageId` is required when targeting pages. However, when `--categoryExtensions` is enabled, `pageId` is optional so you can pass `serviceWorkerId` instead to evaluate inside an extension background service worker. **Element interaction**: Use `take_snapshot` to get page structure with element `uid`s. Each element has a unique `uid` for interaction. If an element isn't found, take a fresh snapshot - the element may have been removed or the page changed. ## Workflow Patterns @@ -20,8 +21,8 @@ Addional tooling can be enabled by providing the following flags: 1. Navigate: `navigate_page` or `new_page` 2. Wait: `wait_for` to ensure content is loaded if you know what you look for. -3. Snapshot: `take_snapshot` to understand page structure -4. Interact: Use element `uid`s from snapshot for `click`, `fill`, etc. +3. Snapshot: `take_snapshot` with `pageId` to understand page structure +4. Interact: Use element `uid`s from snapshot for `click`, `fill`, etc., passing the corresponding `pageId`. ### Efficient data retrieval @@ -59,7 +60,7 @@ You can send multiple tool calls in parallel, but maintain correct order: naviga 1. **Install**: Use `install_extension` with the path to the unpacked extension. 2. **Identify**: Get the extension ID from the response or by calling `list_extensions`. 3. **Trigger Action**: Use `trigger_extension_action` to open the popup or side panel if applicable. -4. **Verify Service Worker**: Use `evaluate_script` with `serviceWorkerId` to check extension state or trigger background actions. +4. **Verify Service Worker**: Use `evaluate_script` with `serviceWorkerId` (omitting `pageId` and `args`) to check extension state or trigger background actions. When evaluating in a page, pass `pageId` (omitting `serviceWorkerId`). 5. **Verify Page Behavior**: Navigate to a page where the extension operates and use `take_snapshot` to check if content scripts injected elements or modified the page correctly. ## Troubleshooting diff --git a/skills/cookie-debugging/SKILL.md b/skills/cookie-debugging/SKILL.md new file mode 100644 index 00000000..85929b96 --- /dev/null +++ b/skills/cookie-debugging/SKILL.md @@ -0,0 +1,149 @@ +--- +name: cookie-debugging +description: Uses Chrome DevTools MCP for inspecting, debugging, and testing cookies, session state, authentication issues, and cookie consent compliance. Use when diagnosing 401/403 errors, authentication redirects, session expiration, Cookie/Set-Cookie header issues, cookie banner consent conformance, or third-party cookie/SameSite/Partitioned cookie warnings. +--- + +## Core Concepts + +### HttpOnly vs Client-Side Storage + +Cookies marked `HttpOnly` cannot be accessed or modified by client-side JavaScript (`cookieStore` or `document.cookie`). However, the browser **automatically attaches active HttpOnly cookies to outgoing HTTP request headers (`Cookie`)**. + +- To inspect current `HttpOnly` values: Look at the `Cookie` request header of any outgoing HTTP request via `get_network_request`. +- To inspect how cookies were created or configured: Look at the `Set-Cookie` response header of login/auth responses. +- To inspect non-`HttpOnly` cookies: Use `evaluate_script` with the modern `cookieStore` API (`async () => await cookieStore.getAll()`). + +### Session Strategy: Live Tab vs Isolated Context + +Choose the right session environment to avoid state contamination (e.g., residual analytics or auth tokens): + +| Strategy | When to Use | Setup / Teardown | +| :---------------------------------- | :---------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ | +| **Live Tab (Active Page)** | Diagnosing an active user session, live 401/403 error, or current state. | Operates directly on the currently selected page. | +| **Clean-Slate (`isolatedContext`)** | Testing cookie consent banners, first-time visits, or zero-cookie guarantees. | Call `new_page` with a unique `isolatedContext` (e.g. `"consent-audit-1"`). When finished, call `close_page`. | + +### Client-Side Capabilities & Limitations + +| Action | Client JavaScript (`cookieStore` / `document.cookie`) | DevTools Network & Context Tools | +| :--------------------------------------------------------------- | :---------------------------------------------------- | :------------------------------------------------------ | +| **Read Non-HttpOnly** | ✅ `async () => await cookieStore.getAll()` | ✅ `get_network_request` (Request `Cookie`) | +| **Read HttpOnly** | ❌ Blocked by browser security | ✅ `get_network_request` (Request `Cookie`) | +| **Inspect Attributes** (`Domain`, `Path`, `SameSite`, `Expires`) | ✅ `async () => await cookieStore.getAll()` | ✅ `get_network_request` (Response `Set-Cookie`) | +| **Modify / Delete Non-HttpOnly** | ✅ `async () => await cookieStore.set(...)` | N/A | +| **Modify / Delete HttpOnly** | ❌ **Silent failure** in JavaScript | ✅ Use `new_page(isolatedContext: ...)` for clean state | + +> [!WARNING] +> Attempting to clear an `HttpOnly` cookie via JavaScript (`cookieStore.delete` or `document.cookie = "...; max-age=0"`) will silently fail. To test in an unauthenticated or fresh state, always spawn a new isolated context using `new_page` with `isolatedContext`. + +--- + +## Workflow Patterns + +### 1. Diagnosing Authentication Failures & Redirects (401 / 403) + +When an authenticated page request fails, returns 401/403, or redirects to login: + +1. **List Recent Requests**: Call `list_network_requests` with `includePreservedRequests: true`. +2. **Find the Target Request**: Locate the failing request (401/403) or redirect (302/307). +3. **Inspect Outgoing `Cookie` Header**: Call `get_network_request` with the `reqid`. + - Verify if the `Cookie` header was attached and whether required tokens (e.g. `SESSION_ID`, `auth_token`) were sent. +4. **Trigger Active Inspection (If no recent request exists)**: + - If the cookie was set in a previous session and no network call is listed, trigger a request: + - Use `navigate_page` with `reload: true`, OR + - Call `evaluate_script` with `() => fetch(window.location.href)` + - Then call `get_network_request` on the new request to inspect the active `Cookie` header. +5. **Trace the Setting Request**: If the cookie is missing or rejected: + - Check earlier login/handshake responses for `Set-Cookie` directives: + - **Path mismatch**: e.g., `Path=/api` when the request is to `/`. + - **Domain mismatch**: e.g., `Domain=api.example.com` preventing cookies on `sub.example.com`. + - **Secure flag on HTTP**: `Secure` cookies are never sent over unencrypted `http://`. + - **SameSite blocking**: `SameSite=Strict` cookies are omitted on cross-site navigations. + - **Expiration**: Check if `Expires` or `Max-Age` elapsed. + +### 2. Cookie Banner & Consent Conformance Testing + +To verify that no non-essential or tracking cookies are set before consent or when declining: + +1. **Start Clean**: Open a fresh isolated context with a dedicated name: + ```json + {"url": "", "isolatedContext": "consent-test-1"} + ``` +2. **Record Baseline Cookies**: Before interacting with the banner, run `evaluate_script` with `async () => await cookieStore.getAll()`. +3. **Inspect Premature Network Requests & Issues**: + - Call `list_network_requests` to ensure no third-party tracking beacons fired before consent. + - Call `list_console_messages` with `types: ["issue"]` to check for tracking warnings. +4. **Interact with Consent Banner**: + - Capture snapshot with `take_snapshot` to locate the "Decline" or "Reject All" button `uid`. + - Click the button with `click`. +5. **Verify Cookie Difference**: + - Run `evaluate_script` with `async () => await cookieStore.getAll()` after clicking to assert that only strictly necessary or consent-state cookies exist. +6. **Test Consent Revocation (Lifecycle Audit)**: + - When auditing consent withdrawal or preference changes: + - Locate and click the "Cookie Settings", "Manage Preferences", or footer privacy trigger (`take_snapshot` $\rightarrow$ `click`). + - Deselect non-essential categories or click "Revoke All" / "Save Preferences". + - Re-query `cookieStore.getAll()` to verify previously accepted non-essential cookies were cleared or expired. + - Call `list_network_requests` on subsequent actions to ensure tracking beacons are no longer fired. +7. **Teardown Context**: Call `close_page` when the audit is complete to prevent leftover cookies from affecting subsequent tasks. + +### 3. Auditing Cookie Security, SameSite & CHIPS (Partitioned Cookies) + +1. **Fast-Track: Native DevTools Issues (Recommended)**: + - Call `list_console_messages` with: + ```json + { + "types": ["issue"], + "includePreservedMessages": true + } + ``` + - Check for `CookieIssue` entries, such as: + - `SameSiteNoneInsecure`: `SameSite=None` without `Secure`. + - `ThirdPartyCookiePhaseout`: Third-party cookie blocked or restricted. + - `SchemefulSameSite`: Cross-scheme cookie issues. + - `PartitionedCookies`: Invalid CHIPS partitioning attributes. +2. **Deep Audit: Lighthouse Third-Party Cookies**: + - Run `lighthouse_audit` with `mode: "navigation"` and `outputDirPath: "/tmp/lh-report"`. + - **Extract the specific cookie audit** without loading the full report into context: + ```bash + node -e "const r=require('/tmp/lh-report/report.json'); const a=r.audits['third-party-cookies']; console.log(JSON.stringify({score: a?.score, displayValue: a?.displayValue, items: a?.details?.items}))" + ``` + +### 4. Client-Side Cookie Inspection & Manipulation + +For client-accessible, non-`HttpOnly` cookies (e.g., UI preferences, non-sensitive feature flags): + +1. **Read Cookies & Attributes**: + - Use the modern asynchronous Cookie Store API: + ```js + async () => await cookieStore.getAll(); + ``` + - _Fallback for insecure HTTP origins_: `() => document.cookie`. +2. **Set / Modify Cookie**: + - Set client cookie via `cookieStore`: + ```js + async () => + await cookieStore.set({ + name: 'theme', + value: 'dark', + expires: Date.now() + 86400000, + sameSite: 'lax', + }); + ``` +3. **Delete Cookie**: + - Clear client cookie: + ```js + async () => await cookieStore.delete('theme'); + ``` + +--- + +## Troubleshooting + +- **`cookieStore` is undefined**: `cookieStore` requires a Secure Context (`https://`, `localhost`, or `127.0.0.1`). On non-secure HTTP origins, use `() => document.cookie` or test over HTTPS. +- **`evaluate_script` returns empty / unresolved Promise**: `cookieStore` methods are asynchronous. Always wrap calls with `async () => await cookieStore.getAll()`. +- **Cookie not visible in JavaScript**: The cookie is marked `HttpOnly`. Trigger a network request and call `get_network_request` to view it in the `Cookie` request header. +- **JavaScript deletion did not remove cookie**: The cookie is `HttpOnly` or requires matching `Path` and `Domain` parameters. Use a fresh `isolatedContext` with `new_page` for a clean slate. +- **Cookie set in response but not sent in requests**: + - Verify if page is `http://` while cookie specifies `Secure`. + - Check if `Domain` restricts subdomains. + - Check `list_console_messages(types: ["issue"])` for browser rejection reasons. +- **Residual cookies contaminating audits**: Always use `new_page` with a unique `isolatedContext` when running compliance tests, and call `close_page` when done. diff --git a/skills/debug-optimize-lcp/SKILL.md b/skills/debug-optimize-lcp/SKILL.md index ebd0aea3..4067d908 100644 --- a/skills/debug-optimize-lcp/SKILL.md +++ b/skills/debug-optimize-lcp/SKILL.md @@ -36,8 +36,8 @@ Follow these steps in order. Each step builds on the previous one. Navigate to the page, then record a trace with reload to capture the full page load including LCP: -1. `navigate_page` to the target URL. -2. `performance_start_trace` with `reload: true` and `autoStop: true`. +1. `navigate_page` with `pageId` to the target URL. +2. `performance_start_trace` with `pageId`, `reload: true` and `autoStop: true`. The trace results will include LCP timing and available insight sets. Note the insight set IDs from the output — you'll need them in the next step. @@ -50,11 +50,11 @@ Use `performance_analyze_insight` to drill into LCP-specific insights. Look for - **RenderBlocking** — Resources blocking the LCP element from rendering. - **LCPDiscovery** — Whether the LCP resource was discoverable early. -Call `performance_analyze_insight` with the insight set ID and the insight name from the trace results. +Call `performance_analyze_insight` with `pageId`, the insight set ID, and the insight name from the trace results. ### Step 3: Identify the LCP Element -Use `evaluate_script` with the **"Identify LCP Element" snippet** found in [references/lcp-snippets.md](references/lcp-snippets.md) to reveal the LCP element's tag, resource URL, and raw timing data. +Use `evaluate_script` (with `pageId`) and the **"Identify LCP Element" snippet** found in [references/lcp-snippets.md](references/lcp-snippets.md) to reveal the LCP element's tag, resource URL, and raw timing data. The `url` field tells you what resource to look for in the network waterfall. If `url` is empty, the LCP element is text-based (no resource to load). @@ -62,8 +62,8 @@ The `url` field tells you what resource to look for in the network waterfall. If Use `list_network_requests` to see when the LCP resource loaded relative to other resources: -- Call `list_network_requests` filtered by `resourceTypes: ["Image", "Font"]` (adjust based on Step 3). -- Then use `get_network_request` with the LCP resource's request ID for full details. +- Call `list_network_requests` with `pageId` filtered by `resourceTypes: ["Image", "Font"]` (adjust based on Step 3). +- Then use `get_network_request` with `pageId` and the LCP resource's request ID for full details. **Key Checks:** @@ -72,7 +72,7 @@ Use `list_network_requests` to see when the LCP resource loaded relative to othe ### Step 5: Inspect HTML for Common Issues -Use `evaluate_script` with the **"Audit Common Issues" snippet** found in [references/lcp-snippets.md](references/lcp-snippets.md) to check for lazy-loaded images in the viewport, missing fetchpriority, and render-blocking scripts. +Use `evaluate_script` (with `pageId`) and the **"Audit Common Issues" snippet** found in [references/lcp-snippets.md](references/lcp-snippets.md) to check for lazy-loaded images in the viewport, missing fetchpriority, and render-blocking scripts. ## Optimization Strategies @@ -115,7 +115,7 @@ The HTML document itself takes too long to arrive. ## Verifying Fixes & Emulation -- **Verification**: Re-run the trace (`performance_start_trace` with `reload: true`) and compare the new subpart breakdown. The bottleneck should shrink. +- **Verification**: Re-run the trace (`performance_start_trace` with `pageId` and `reload: true`) and compare the new subpart breakdown. The bottleneck should shrink. - **Emulation**: Lab measurements differ from real-world experience. Use `emulate` to test under constraints: - - `emulate` with `networkConditions: "Fast 3G"` and `cpuThrottlingRate: 4`. + - `emulate` with `pageId`, `networkConditions: "Fast 3G"` and `cpuThrottlingRate: 4`. - This surfaces issues visible only on slower connections/devices. diff --git a/skills/memory-leak-debugging/SKILL.md b/skills/memory-leak-debugging/SKILL.md index 8e77df8b..5f2adc3a 100644 --- a/skills/memory-leak-debugging/SKILL.md +++ b/skills/memory-leak-debugging/SKILL.md @@ -7,6 +7,10 @@ description: Diagnoses and resolves memory leaks in JavaScript/Node.js applicati This skill provides expert guidance and workflows for finding, diagnosing, and fixing memory leaks in JavaScript and Node.js applications using Chrome DevTools MCP tools. +## Prerequisites + +Advanced memory debugging tools (`compare_heapsnapshots`, `get_heapsnapshot_details`, etc.) are only available when the server is started with the `--memoryDebugging` flag. First check if these tools are available; if not, try to read the MCP configuration file to check if `--memoryDebugging` is enabled. + ## Core Principles - **Prefer MCP memory tools:** Do NOT attempt to read raw `.heapsnapshot` files directly, as they are extremely large and will consume too many tokens. Use the Chrome DevTools MCP heap snapshot tools to summarize, compare, and inspect snapshots. @@ -20,10 +24,10 @@ This skill provides expert guidance and workflows for finding, diagnosing, and f When investigating a frontend web application memory leak, utilize the `chrome-devtools-mcp` tools to interact with the application and take snapshots. -- Use tools like `click`, `navigate_page`, `fill`, etc., to manipulate the page into the desired state. +- Use page-scoped tools like `click`, `navigate_page`, `fill`, etc. (specifying `pageId`) to manipulate the page into the desired state. - Revert the page back to the original state after interactions to see if memory is released. - Repeat the same user interactions 10 times to amplify the leak. -- Use `take_heapsnapshot` to save `.heapsnapshot` files to disk at baseline, target (after actions), and final (after reverting actions) states. +- Use `take_heapsnapshot` (with `pageId`) to save `.heapsnapshot` files to disk at baseline, target (after actions), and final (after reverting actions) states. ### 2. Comparing Snapshots diff --git a/skills/troubleshooting/SKILL.md b/skills/troubleshooting/SKILL.md index 6793f996..bf5b98c3 100644 --- a/skills/troubleshooting/SKILL.md +++ b/skills/troubleshooting/SKILL.md @@ -60,6 +60,7 @@ Identify other error messages from the failed tool call or the MCP initializatio - `Target closed` - "Tool not found" (check if they are using `--slim` which only enables navigation and screenshot tools). +- Missing `pageId`: Page-scoped tools require a `pageId` argument. Call `list_pages` to find active page IDs. - `ProtocolError: Network.enable timed out` or `The socket connection was closed unexpectedly` - `Error [ERR_MODULE_NOT_FOUND]: Cannot find module` - Any sandboxing or host validation errors. diff --git a/src/McpContext.ts b/src/McpContext.ts index 5578e6d4..639216e5 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -17,6 +17,7 @@ import type { HeapSnapshotDetailedClassDiff, DuplicateStringGroup, HeapEdgesQueryOptions, + HeapQueryOptions, } from './processors/HeapSnapshotManager.js'; import {McpPage} from './McpPage.js'; import {type UncaughtError} from './collectors/PageCollector.js'; @@ -49,6 +50,7 @@ import type {TraceResult} from './processors/PerformanceTrace.js'; import type {Logger} from './types.js'; import type {ExtensionServiceWorker} from './types.js'; import {getTempFilePath, resolveCanonicalPath} from './utils/files.js'; +import {isAllowedUrl} from './utils/url.js'; interface McpContextOptions { // Whether the DevTools windows are exposed as pages for debugging of DevTools. experimentalDevToolsDebugging: boolean; @@ -56,6 +58,8 @@ interface McpContextOptions { experimentalIncludeAllPages?: boolean; // Whether CrUX data should be fetched. performanceCrux: boolean; + // Whether source maps are enabled in DevTools. + sourceMaps?: boolean; // The allow list of URL patterns to allow loading resources. allowList?: string[]; // The block list of URL patterns to block loading resources. @@ -69,6 +73,8 @@ interface McpContextOptions { reconnected?: boolean; // Custom navigation timeout in milliseconds to override default. navigationTimeout?: number; + // Whether extension tools and targets are enabled. + categoryExtensions?: boolean; } // Page ids are handed out from a process-wide counter so they stay unique @@ -163,6 +169,14 @@ export class McpContext implements Context { #onTargetCreated = async (target: Target) => { try { + const url = target.url(); + if ( + !isAllowedUrl(url, { + categoryExtensions: this.#options.categoryExtensions, + }) + ) { + return; + } const page = await target.page(); if (!page) { return; @@ -225,21 +239,18 @@ export class McpContext implements Context { this.#roots = roots; } - async validatePath(filePath?: string): Promise { + /** + * Validates that the filePath is allowed according to the roots configuration. + * Tolerates if parts of the filePath do not exist yet but the file access to + * the resolved should only be allowed without following symlinks. + */ + async validatePath(filePath: string): Promise; + async validatePath(filePath?: undefined): Promise; + async validatePath(filePath?: string): Promise; + async validatePath(filePath?: string): Promise { if (filePath === undefined) { - return; - } - // If the client never negotiated roots and the operator has explicitly - // opted into unrestricted access via --allow-unrestricted-paths, restore - // the previous permissive behavior and skip validation. - if (this.#roots === undefined && this.#allowUnrestrictedPaths) { - return; + return undefined; } - // roots() always returns at least the temp directory, even if the - // connecting client never negotiated the optional `roots` capability. - // Path validation must not be skipped just because no workspace roots - // were configured. - const roots = this.roots(); let canonicalPath: string; @@ -255,6 +266,20 @@ export class McpContext implements Context { ); } + // If the client never negotiated roots and the operator has explicitly + // opted into unrestricted access via --allow-unrestricted-paths, restore + // the previous permissive behavior and skip validation. + if (this.#roots === undefined && this.#allowUnrestrictedPaths) { + // Canonical path might not exist yet so we fallback to + // path.resolve(filePath). Consumers should not follow symlinks. + return canonicalPath || path.resolve(filePath); + } + // roots() always returns at least the temp directory, even if the + // connecting client never negotiated the optional `roots` capability. + // Path validation must not be skipped just because no workspace roots + // were configured. + const roots = this.roots(); + let allowed = false; const resolvedRoots = await Promise.allSettled( roots.map(async root => { @@ -293,19 +318,20 @@ export class McpContext implements Context { `Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`, ); } + + return canonicalPath || path.resolve(filePath); } async ensureExtension( filePath: string, extension: Extension, ): Promise<`${string}${Extension}`> { - const resolvedPath = path.resolve(filePath); - const currentExtension = path.extname(resolvedPath); - const outputPath: `${string}${Extension}` = `${resolvedPath.slice( + const resolved = await this.validatePath(filePath); + const currentExtension = path.extname(resolved); + const outputPath: `${string}${Extension}` = `${resolved.slice( 0, - resolvedPath.length - currentExtension.length, + resolved.length - currentExtension.length, )}${extension}`; - await this.validatePath(outputPath); return outputPath; } @@ -320,7 +346,7 @@ export class McpContext implements Context { ctx = await this.browser.createBrowserContext(); this.#isolatedContexts.set(isolatedContextName, ctx); } - page = await ctx.newPage(); + page = await ctx.newPage({background}); } else { page = await this.browser.newPage({background}); } @@ -400,6 +426,21 @@ export class McpContext implements Context { return page; } + getSelectedMcpPageUrl(page?: McpPage): string | undefined { + let targetPage = page; + if (!targetPage) { + try { + targetPage = this.getSelectedMcpPage(); + } catch { + return undefined; + } + } + if (targetPage?.pptrPage?.isClosed() === false) { + return targetPage.pptrPage.url(); + } + return undefined; + } + async getDevToolsData(page?: McpPage): Promise { const targetPage = page ?? this.#selectedPage; if (!targetPage) { @@ -528,6 +569,7 @@ export class McpContext implements Context { page.browserContext(), ), navigationTimeout: this.#options.navigationTimeout, + sourceMaps: this.#options.sourceMaps, }); this.#mcpPages.set(page, mcpPage); await mcpPage.init(); @@ -576,36 +618,47 @@ export class McpContext implements Context { const allPages = ( await this.browser.pages(this.#options.experimentalIncludeAllPages) ).filter(page => { - return ( - this.#options.experimentalDevToolsDebugging || - !page.url().startsWith('devtools://') - ); + if ( + !this.#options.experimentalDevToolsDebugging && + page.url().startsWith('devtools://') + ) { + return false; + } + return isAllowedUrl(page.url(), { + categoryExtensions: this.#options.categoryExtensions, + }); }); - const allTargets = this.browser.targets(); - const extensionTargets = allTargets.filter(target => { - return ( - target.url().startsWith('chrome-extension://') && - target.type() === 'page' - ); - }); + if (this.#options.categoryExtensions) { + const allTargets = this.browser.targets(); + const extensionTargets = allTargets.filter(target => { + return ( + target.url().startsWith('chrome-extension://') && + target.type() === 'page' + ); + }); - await Promise.allSettled( - extensionTargets.map(async target => { - try { - let page = await target.page(); - if (!page) { - page = await target.asPage(); - } - this.#extensionPages.set(target, page); - if (page && !allPages.includes(page)) { - allPages.push(page); + await Promise.allSettled( + extensionTargets.map(async target => { + try { + let page = await target.page(); + if (!page) { + page = await target.asPage(); + } + this.#extensionPages.set(target, page); + if ( + page && + isAllowedUrl(page.url(), {categoryExtensions: true}) && + !allPages.includes(page) + ) { + allPages.push(page); + } + } catch (e) { + this.logger?.('Failed to get page for extension target', e); } - } catch (e) { - this.logger?.('Failed to get page for extension target', e); - } - }), - ); + }), + ); + } return allPages; } @@ -624,17 +677,17 @@ export class McpContext implements Context { filepath: string, data: Uint8Array, ): Promise { - await this.validatePath(filepath); + const resolved = await this.validatePath(filepath); try { - await fs.mkdir(path.dirname(filepath), {recursive: true}); + await fs.mkdir(path.dirname(resolved), {recursive: true}); // Open the file with flags to: // - O_WRONLY: Write-only // - O_CREAT: Create if it doesn't exist // - O_TRUNC: Truncate to zero length if it exists // - O_NOFOLLOW: DO NOT follow symlinks. // - 0o600: Permissions: read/write for owner, no permissions for others. - await fs.writeFile(filepath, data, { + await fs.writeFile(resolved, data, { flag: fs.constants.O_WRONLY | fs.constants.O_CREAT | @@ -738,6 +791,13 @@ export class McpContext implements Context { return await this.#heapSnapshotManager.getDuplicateStrings(filePath); } + async queryHeapSnapshotObjects( + filePath: string, + options: HeapQueryOptions, + ): Promise { + return await this.#heapSnapshotManager.queryObjects(filePath, options); + } + async getHeapSnapshotStats( filePath: string, ): Promise { @@ -866,8 +926,8 @@ export class McpContext implements Context { } case 'file:': { - await this.validatePath(fileURLToPath(url)); - return await fs.readFile(url, 'utf-8'); + const resolved = await this.validatePath(fileURLToPath(url)); + return await fs.readFile(resolved, 'utf-8'); } default: diff --git a/src/McpPage.ts b/src/McpPage.ts index e1ce94bc..4e4f088b 100644 --- a/src/McpPage.ts +++ b/src/McpPage.ts @@ -138,6 +138,7 @@ export class McpPage implements ContextPage { #hasNetworkBlockOrAllowlist: boolean; #locatorClass: typeof Locator; #navigationTimeout: number; + #sourceMaps: boolean; constructor( page: Page, @@ -147,11 +148,13 @@ export class McpPage implements ContextPage { locatorClass: typeof Locator; isolatedContextName?: string; navigationTimeout?: number; + sourceMaps?: boolean; }, ) { this.#hasNetworkBlockOrAllowlist = options.hasNetworkBlockOrAllowlist; this.#locatorClass = options.locatorClass; this.#navigationTimeout = options.navigationTimeout ?? NAVIGATION_TIMEOUT; + this.#sourceMaps = options.sourceMaps ?? true; this.pptrPage = page; this.id = id; this.isolatedContextName = options.isolatedContextName; @@ -196,7 +199,9 @@ export class McpPage implements ContextPage { } try { const session = await this.pptrPage.createCDPSession(); - this.#devtoolsUniverse = await createTargetUniverse(session); + this.#devtoolsUniverse = await createTargetUniverse(session, { + sourceMaps: this.#sourceMaps, + }); } catch (e) { logger?.('Failed to initialize DevTools universe', e); } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 2f7d6ff1..083e2f4f 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -8,7 +8,7 @@ import type {WebMCPTool} from 'puppeteer-core'; -import type {ParsedArguments} from './bin/chrome-devtools-mcp-cli-options.js'; +import type {ParsedArguments} from './config/mcp-options.js'; import {ConsoleFormatter} from './formatters/ConsoleFormatter.js'; import { HeapSnapshotFormatter, diff --git a/src/ToolHandler.ts b/src/ToolHandler.ts index 21d28230..9f06e746 100644 --- a/src/ToolHandler.ts +++ b/src/ToolHandler.ts @@ -8,7 +8,7 @@ * streaming. Keep the diff to the three `this.hooks?.` call sites. */ -import type {parseArguments} from './bin/chrome-devtools-mcp-cli-options.js'; +import type {ParsedArguments} from './config/mcp-options.js'; import type {McpContext} from './McpContext.js'; import type {McpPage} from './McpPage.js'; import type {DataFormat} from './McpResponse.js'; @@ -20,11 +20,10 @@ import type { } from './opera/toolHandlerHooks.js'; import {SlimMcpResponse} from './SlimMcpResponse.js'; import {ClearcutLogger} from './telemetry/ClearcutLogger.js'; -import {bucketizeLatency, buildContext} from './telemetry/transformation.js'; import type {CallToolResult} from './third_party/index.js'; import {zod} from './third_party/index.js'; -import type {ToolCategory} from './tools/categories.js'; -import {labels, OFF_BY_DEFAULT_CATEGORIES} from './tools/categories.js'; +import {labels} from './tools/categories.js'; +import {categoryToFlagName} from './config/category-options.js'; import type { DefinedPageTool, DevToolsData, @@ -34,13 +33,9 @@ import type { import {pageIdSchema} from './tools/ToolDefinition.js'; import {logger} from './utils/logger.js'; import type {Mutex} from './third_party/index.js'; -import {fileURLToPath} from 'node:url'; +import {fileURLToPath, pathToFileURL} from 'node:url'; import {isLocalhost} from './utils/url.js'; -export function buildFlag(category: ToolCategory) { - return `category${category.charAt(0).toUpperCase() + category.slice(1)}`; -} - function buildDisabledMessage( toolName: string, flag: string, @@ -48,85 +43,31 @@ function buildDisabledMessage( ): string { const reason = categoryLabel ? `is in category ${categoryLabel} which` - : `requires experimental feature ${flag} and`; + : `requires ${flag.startsWith('--experimental') ? 'experimental feature' : 'flag'} ${flag} and`; return `Tool ${toolName} ${reason} is currently disabled. Enable it by running ${CLI_BIN_NAME} start ${flag}=true. For more information check the README.`; } -function getCategoryStatus( - category: ToolCategory, - serverArgs: ReturnType, -): {categoryFlag?: string; disabled: boolean} { - const categoryFlag = buildFlag(category); - - const flagValue = serverArgs[categoryFlag]; - - const isDisabled = OFF_BY_DEFAULT_CATEGORIES.includes(category) - ? !flagValue - : flagValue === false; - - if (isDisabled) { - return { - categoryFlag, - disabled: true, - }; - } - - return { - disabled: false, - }; -} - -function getConditionStatus( - condition: string, - serverArgs: ReturnType, -): {conditionFlag?: string; disabled: boolean} { - if (condition && !serverArgs[condition]) { - return {conditionFlag: condition, disabled: true}; - } - - return {disabled: false}; -} - function getToolStatusInfo( tool: ToolDefinition | DefinedPageTool, - serverArgs: ReturnType, + serverArgs: ParsedArguments, ): {disabled: boolean; reason?: string} { const category = tool.annotations.category; - const categoryCheck = getCategoryStatus(category, serverArgs); - - if (category && categoryCheck.disabled) { - if (!categoryCheck.categoryFlag) { - throw new Error( - 'when the category is disabled there should always be a flag set', - ); + if (category) { + const flag = categoryToFlagName(category); + if (!serverArgs[flag]) { + return { + disabled: true, + reason: buildDisabledMessage(tool.name, `--${flag}`, labels[category]), + }; } - - return { - disabled: true, - reason: buildDisabledMessage( - tool.name, - `--${categoryCheck.categoryFlag}`, - labels[category!], - ), - }; } for (const condition of tool.annotations.conditions || []) { - const conditionCheck = getConditionStatus(condition, serverArgs); - if (conditionCheck.disabled) { - if (!conditionCheck.conditionFlag) { - throw new Error( - 'when the condition is disabled there should always be a flag set', - ); - } - + if (!serverArgs[condition]) { return { disabled: true, - reason: buildDisabledMessage( - tool.name, - `--${conditionCheck.conditionFlag}`, - ), + reason: buildDisabledMessage(tool.name, `--${condition}`), }; } } @@ -160,14 +101,21 @@ function buildUnknownArgumentsMessage( return `Unknown ${unknownLabel} for tool "${toolName}": ${formatArgumentNames(unknownArgumentNames)}. ${expectedArguments} ${correction} and retry.`; } -function extractPaths(value: unknown): string[] { - if (typeof value === 'string') { - return [value]; - } - if (Array.isArray(value)) { - return value.filter(item => typeof item === 'string'); +async function validateAndResolvePathOrUrl( + filePathOrUrl: string, + context: McpContext, +): Promise { + try { + const url = new URL(filePathOrUrl); + if (url.protocol === 'file:') { + return pathToFileURL(await context.validatePath(fileURLToPath(url))).href; + } else if (['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) { + return filePathOrUrl; + } + } catch { + // Suppress parsing errors for regular file paths. } - return []; + return await context.validatePath(filePathOrUrl); } function isLocalBrowser(context: McpContext): boolean { @@ -203,25 +151,25 @@ async function validateToolFiles( context: McpContext, ): Promise { const isLocal = isLocalBrowser(context); - const pathsOrUrlsToValidate: string[] = []; for (const [key, option] of Object.entries(tool.verifyFilesSchema)) { if (shouldValidateFile(option, isLocal)) { - pathsOrUrlsToValidate.push(...extractPaths(params[key])); - } - } - for (const filePathOrUrl of pathsOrUrlsToValidate) { - let filePath = filePathOrUrl; - try { - const url = new URL(filePathOrUrl); - if (url.protocol === 'file:') { - filePath = fileURLToPath(url); - } else if (['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) { - continue; + const val = params[key]; + if (typeof val === 'string') { + params[key] = await validateAndResolvePathOrUrl(val, context); + } else if (Array.isArray(val)) { + const updated: unknown[] = []; + for (const item of val) { + if (typeof item === 'string') { + updated.push(await validateAndResolvePathOrUrl(item, context)); + } else { + throw new Error( + 'Unexpected non-string value as a file path or URL', + ); + } + } + params[key] = updated; } - } catch { - // Suppress parsing errors for regular file paths. } - await context.validatePath(filePath); } } @@ -233,7 +181,7 @@ export class ToolHandler { constructor( private readonly tool: ToolDefinition | DefinedPageTool, - private readonly serverArgs: ReturnType, + private readonly serverArgs: ParsedArguments, private readonly getContext: () => Promise, private readonly toolMutex: Mutex, private readonly hooks?: OperaToolHooks, @@ -245,7 +193,7 @@ export class ToolHandler { this.inputSchema = 'pageScoped' in tool && tool.pageScoped && - serverArgs.experimentalPageIdRouting && + serverArgs.pageIdRouting && !serverArgs.slim ? {...pageIdSchema, ...tool.schema} : tool.schema; @@ -322,7 +270,7 @@ export class ToolHandler { const pageId = typeof params.pageId === 'number' ? params.pageId : undefined; page = - this.serverArgs.experimentalPageIdRouting && + this.serverArgs.pageIdRouting && pageId !== undefined && !this.serverArgs.slim ? context.getPageById(pageId) @@ -354,10 +302,7 @@ export class ToolHandler { response.setError(err); } devToolsData = await context.getDevToolsData(page); - const targetPage = page ?? context.getSelectedMcpPage(); - if (targetPage?.pptrPage?.isClosed() === false) { - pageUrl = targetPage.pptrPage.url(); - } + pageUrl = context.getSelectedMcpPageUrl(page); // Resolve data format: --experimentalDataFormat takes precedence, fall back to legacy --experimentalToonFormat let dataFormat: DataFormat = 'default'; if (this.serverArgs.experimentalDataFormat) { @@ -399,14 +344,14 @@ export class ToolHandler { isError: true, }; } finally { - const context = buildContext(devToolsData, pageUrl); void ClearcutLogger.get()?.logToolInvocation({ toolName: this.tool.name, params, schema: this.inputSchema, success, - latencyMs: bucketizeLatency(Date.now() - startTime), - context, + latencyMs: Date.now() - startTime, + devToolsData, + pageUrl, }); guard?.[Symbol.dispose](); } diff --git a/src/bin/chrome-devtools-mcp-main.ts b/src/bin/chrome-devtools-mcp-main.ts index 2142af65..cd354b50 100644 --- a/src/bin/chrome-devtools-mcp-main.ts +++ b/src/bin/chrome-devtools-mcp-main.ts @@ -11,7 +11,7 @@ import '../utils/polyfill.js'; import process from 'node:process'; import {closeBrowser} from '../browser.js'; -import {createMcpServer, logDisclaimers} from '../index.js'; +import {McpServer, logDisclaimers} from '../index.js'; import { ENV_CRASH_ON_UNCAUGHT, PACKAGE_NAME, @@ -25,7 +25,7 @@ import {checkForUpdates} from '../utils/check-for-updates.js'; import {logger, saveLogsToFile} from '../utils/logger.js'; import {VERSION} from '../version.js'; -import {cliOptions, parseArguments} from './chrome-devtools-mcp-cli-options.js'; +import {mcpOptions, parseArguments} from '../config/mcp-options.js'; await checkForUpdates(`Run \`npm install ${PACKAGE_NAME}@latest\` to update.`); @@ -80,7 +80,7 @@ process.on('SIGHUP', () => { void shutdown('SIGHUP'); }); -const {server} = await createMcpServer(args, { +const server = await McpServer.from(args, { logFile, }); const transport = new StdioServerTransport(); @@ -88,4 +88,4 @@ await server.connect(transport); logger?.(`${PRODUCT_NAME} connected`); logDisclaimers(args); void ClearcutLogger.get()?.logDailyActiveIfNeeded(); -void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, cliOptions)); +void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, mcpOptions)); diff --git a/src/bin/chrome-devtools.ts b/src/bin/chrome-devtools.ts index 0c710d9c..802f12b4 100644 --- a/src/bin/chrome-devtools.ts +++ b/src/bin/chrome-devtools.ts @@ -31,8 +31,12 @@ import {hideBin, yargs, type CallToolResult} from '../third_party/index.js'; import {checkForUpdates} from '../utils/check-for-updates.js'; import {VERSION} from '../version.js'; -import {commands} from './chrome-devtools-cli-options.js'; -import {cliOptions, parseArguments} from './chrome-devtools-mcp-cli-options.js'; +import {commands} from '../config/cli-options.js'; +import { + mcpOptions, + parseArguments, + getMcpOptionsForViaCli, +} from '../config/mcp-options.js'; process.title = CLI_BIN_NAME; @@ -40,35 +44,28 @@ await checkForUpdates( `Run \`npm install -g ${PACKAGE_NAME}@latest\` and \`${CLI_BIN_NAME} start\` to update and restart the daemon.`, ); +const DEFAULT_CLI_ARGS = ['--viaCli']; + async function start(args: string[], sessionId: string) { - const combinedArgs = [...args, ...defaultArgs]; + const combinedArgs = [...DEFAULT_CLI_ARGS, ...args]; await startDaemon(combinedArgs, sessionId); logDisclaimers(parseArguments(VERSION, combinedArgs)); } -const defaultArgs = ['--viaCli', '--experimentalStructuredContent']; +function getCliOptions() { + const options: Partial = { + ...getMcpOptionsForViaCli(), + }; -const startCliOptions = { - ...cliOptions, -} as Partial; + // Missing CLI serialization. + delete options.viewport; -// Missing CLI serialization. -delete startCliOptions.viewport; + // Change the defaults for the CLI. + delete options.experimentalStructuredContent; + delete options.experimentalInteropTools; -// Change the defaults for the CLI. -delete startCliOptions.experimentalStructuredContent; -delete startCliOptions.experimentalInteropTools; -delete startCliOptions.experimentalPageIdRouting; -if (!('default' in cliOptions.headless)) { - throw new Error('headless cli option unexpectedly does not have a default'); -} -if ('default' in cliOptions.isolated) { - throw new Error('isolated cli option unexpectedly has a default'); + return options; } -startCliOptions.headless!.default = true; -startCliOptions.isolated!.description = - 'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to true unless userDataDir is provided.'; -startCliOptions.categoryExtensions!.default = true; const y = yargs(hideBin(process.argv)) .locale('en') // Force English to ensure error string matching works in .fail, all custom messages we output are in English anyways @@ -108,13 +105,11 @@ const y = yargs(hideBin(process.argv)) '1. Required parameters MUST be passed as positional arguments (without flags).', ); console.error( - ' - INCORRECT: chrome-devtools evaluate_script --expression "() => document.title"', - ); - console.error( - ' - CORRECT: chrome-devtools evaluate_script "() => document.title"', + ' - INCORRECT: chrome-devtools click --pageId 1 --uid "1_2"', ); + console.error(' - CORRECT: chrome-devtools click 1 "1_2"'); console.error( - '2. Optional parameters are passed as double-dash options/flags (e.g. --pageId 1).', + '2. Optional parameters are passed as double-dash options/flags (e.g. --dblClick true).', ); console.error( '3. Make sure to escape quotes properly for your shell environment.', @@ -135,7 +130,7 @@ y.command( `Start or restart ${MCP_BIN_NAME}`, y => y - .options(startCliOptions) + .options(getCliOptions()) .example( '$0 start --browserUrl http://localhost:9222', 'Start the server connecting to an existing browser', @@ -146,13 +141,24 @@ y.command( await stopDaemon(argv.sessionId); } // Defaults but we do not want to affect the yargs conflict resolution. - if (argv.isolated === undefined && argv.userDataDir === undefined) { + if ( + argv.isolated === undefined && + argv.userDataDir === undefined && + !argv.autoConnect && + !argv.browserUrl && + !argv.wsEndpoint + ) { argv.isolated = true; } - if (argv.headless === undefined) { + if ( + argv.headless === undefined && + !argv.autoConnect && + !argv.browserUrl && + !argv.wsEndpoint + ) { argv.headless = true; } - const args = serializeArgs(cliOptions, argv); + const args = serializeArgs(mcpOptions, argv); await start(args, argv.sessionId); process.exit(0); }, @@ -279,7 +285,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) { : Promise.resolve(undefined); if (!isDaemonRunning(sessionId)) { - await start(serializeArgs(cliOptions, argv), sessionId); + await start(serializeArgs(mcpOptions, argv), sessionId); } const commandArgs: Record = {}; diff --git a/src/bin/opera-devtools-cli-options.ts b/src/bin/opera-devtools-cli-options.ts index f3367239..f3d46f53 100644 --- a/src/bin/opera-devtools-cli-options.ts +++ b/src/bin/opera-devtools-cli-options.ts @@ -5,4 +5,4 @@ */ // Compatibility shim. The canonical file is generated by `npm run cli:generate`. -export * from './chrome-devtools-cli-options.js'; +export * from '../config/cli-options.js'; diff --git a/src/bin/opera-devtools-mcp-cli-options.ts b/src/bin/opera-devtools-mcp-cli-options.ts index e685bae9..0580449f 100644 --- a/src/bin/opera-devtools-mcp-cli-options.ts +++ b/src/bin/opera-devtools-mcp-cli-options.ts @@ -5,4 +5,4 @@ */ // Compatibility shim. Prefer importing the canonical path directly. -export * from './chrome-devtools-mcp-cli-options.js'; +export * from '../config/mcp-options.js'; diff --git a/src/browser.ts b/src/browser.ts index 7b7e4638..527cd6e5 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -15,34 +15,21 @@ import type { Browser, ChromeReleaseChannel, LaunchOptions, - Target, } from './third_party/index.js'; import {puppeteer} from './third_party/index.js'; import {logger, puppeteerLogger} from './utils/logger.js'; +import {isAllowedUrl} from './utils/url.js'; let browser: Browser | undefined; let browserMode: 'launched' | 'connected' | undefined; -function makeTargetFilter(enableExtensions = false) { - const ignoredPrefixes = new Set(['chrome://', 'chrome-untrusted://']); - if (!enableExtensions) { - ignoredPrefixes.add('chrome-extension://'); - } - - return function targetFilter(target: Target): boolean { - if (target.url() === 'chrome://newtab/') { - return true; - } - // Could be the only page opened in the browser. - if (target.url().startsWith('chrome://inspect')) { +export function makeTargetFilter(enableExtensions = false) { + return function targetFilter(target: {url(): string}): boolean { + const url = target.url(); + if (!url) { return true; } - for (const prefix of ignoredPrefixes) { - if (target.url().startsWith(prefix)) { - return false; - } - } - return true; + return isAllowedUrl(url, {categoryExtensions: enableExtensions}); }; } diff --git a/src/config/browser-options.ts b/src/config/browser-options.ts new file mode 100644 index 00000000..89eb37fe --- /dev/null +++ b/src/config/browser-options.ts @@ -0,0 +1,136 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + * + * Modified by Opera Software AS. + */ + +import {CACHE_DIR_NAME, MCP_BIN_NAME, REPO_URL} from '../opera/branding.js'; + +import type {YargsOptions} from '../third_party/index.js'; + +export const browserOptions = { + autoConnect: { + type: 'boolean', + description: + 'If specified, automatically connects to a browser (Chrome 144+) running locally from the user data directory identified by the channel param (default channel is stable). Requires the remote debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.', + conflicts: ['isolated', 'executablePath'], + default: false, + coerce: (value: boolean | undefined) => { + if (!value) { + return; + } + return value; + }, + }, + browserUrl: { + type: 'string', + description: `Connect to a running, debuggable Chrome instance (e.g. \`http://127.0.0.1:9222\`). For more details see: ${REPO_URL}#connecting-to-a-running-chrome-instance.`, + alias: 'u', + conflicts: ['wsEndpoint'], + coerce: (url: string | undefined) => { + if (!url) { + return; + } + try { + new URL(url); + } catch { + throw new Error(`Provided browserUrl ${url} is not valid URL.`); + } + return url; + }, + }, + wsEndpoint: { + type: 'string', + description: + 'WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/). Alternative to --browserUrl.', + alias: 'w', + conflicts: ['browserUrl'], + coerce: (url: string | undefined) => { + if (!url) { + return; + } + try { + const parsed = new URL(url); + if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') { + throw new Error( + `Provided wsEndpoint ${url} must use ws:// or wss:// protocol.`, + ); + } + return url; + } catch (error) { + if ((error as Error).message.includes('ws://')) { + throw error; + } + throw new Error(`Provided wsEndpoint ${url} is not valid URL.`); + } + }, + }, + wsHeaders: { + type: 'string', + description: + 'Custom headers for WebSocket connection in JSON format (e.g., \'{"Authorization":"Bearer token"}\'). Only works with --wsEndpoint.', + implies: 'wsEndpoint', + coerce: (val: string | undefined) => { + if (!val) { + return; + } + try { + const parsed = JSON.parse(val); + if (typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('Headers must be a JSON object'); + } + return parsed as Record; + } catch (error) { + throw new Error( + `Invalid JSON for wsHeaders: ${(error as Error).message}`, + ); + } + }, + }, + headless: { + type: 'boolean', + description: 'Whether to run in headless (no UI) mode.', + default: false, + }, + executablePath: { + type: 'string', + description: 'Path to custom Chrome executable.', + conflicts: ['browserUrl', 'wsEndpoint'], + alias: 'e', + }, + isolated: { + type: 'boolean', + description: + 'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to false.', + }, + userDataDir: { + type: 'string', + description: `Path to the user data directory for Chrome. Default is $HOME/.cache/${CACHE_DIR_NAME}/chrome-profile$CHANNEL_SUFFIX_IF_NON_STABLE`, + conflicts: ['browserUrl', 'wsEndpoint', 'isolated'], + }, + channel: { + type: 'string', + description: + 'Specify a different Chrome channel that should be used. The default is the stable channel version.', + choices: ['canary', 'dev', 'beta', 'stable'] as const, + conflicts: ['browserUrl', 'wsEndpoint', 'executablePath'], + }, + proxyServer: { + type: 'string', + description: `Proxy server configuration for Chrome passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details.`, + }, + chromeArg: { + type: 'array', + describe: `Additional arguments for Chrome. Only applies when Chrome is launched by ${MCP_BIN_NAME}.`, + }, + ignoreDefaultChromeArg: { + type: 'array', + describe: `Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by ${MCP_BIN_NAME}.`, + }, +} satisfies Record; + +export function getBrowserOptions(): typeof browserOptions { + return browserOptions; +} diff --git a/src/config/category-options.ts b/src/config/category-options.ts new file mode 100644 index 00000000..85f198ad --- /dev/null +++ b/src/config/category-options.ts @@ -0,0 +1,116 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {ToolCategory} from '../tools/categories.js'; + +export interface CategoryOption { + type: 'boolean'; + describe: string; + default?: boolean; + hidden?: boolean; + conflicts?: string[]; +} + +export type CategoryFlagName = + `category${Capitalize}`; + +export type CategoryFlags = { + [K in ToolCategory as CategoryFlagName]: CategoryOption; +}; + +const categoryOverrides: Record< + ToolCategory, + { + describe?: string; + hidden?: boolean; + conflicts?: string[]; + offByDefault?: boolean; + } +> = { + [ToolCategory.INPUT]: {}, + [ToolCategory.NAVIGATION]: {}, + [ToolCategory.EMULATION]: { + hidden: false, + }, + [ToolCategory.PERFORMANCE]: { + hidden: false, + }, + [ToolCategory.NETWORK]: { + hidden: false, + }, + [ToolCategory.DEBUGGING]: {}, + [ToolCategory.MEMORY]: {}, + [ToolCategory.WEBMCP]: { + describe: + 'Set to true to enable debugging WebMCP tools. Requires Chrome 150+ with the following flag: `--enable-features=WebMCP`', + offByDefault: true, + }, + [ToolCategory.EXTENSIONS]: { + describe: + 'Set to true to include tools related to extensions. Note: This feature is currently only supported with a pipe connection. autoConnect, browserUrl, and wsEndpoint are not supported with this feature until 149 will be released.', + hidden: false, + offByDefault: true, + }, + [ToolCategory.THIRD_PARTY]: { + describe: + 'Set to true to enable third-party developer tools exposed by the inspected page itself', + hidden: false, + offByDefault: true, + }, + [ToolCategory.PWA]: { + describe: + 'Set to true to include tools for automating Progressive Web Apps (install, launch, uninstall, and OS state). This feature is only supported with a pipe connection; autoConnect, browserUrl, and wsEndpoint are not supported.', + conflicts: ['autoConnect', 'browserUrl', 'wsEndpoint'], + hidden: false, + offByDefault: true, + }, + [ToolCategory.OPERA]: {}, +}; + +function createOption(category: ToolCategory): CategoryOption { + const overrides = categoryOverrides[category]; + const describe = overrides.offByDefault + ? `Set to true to include tools related to ${category}.` + : `Set to false to exclude tools related to ${category}.`; + + return { + type: 'boolean', + describe, + hidden: true, + ...overrides, + ...(overrides.offByDefault ? {} : {default: true}), + }; +} + +export function categoryToFlagName(category: ToolCategory): CategoryFlagName { + return `category${category.charAt(0).toUpperCase()}${category.slice(1)}` as CategoryFlagName; +} + +export function getCategoryOptions(): CategoryFlags { + const options = {} as CategoryFlags; + for (const category of Object.values(ToolCategory)) { + const flagName = categoryToFlagName(category); + options[flagName] = createOption(category); + } + + return options; +} + +export function isCategoryOffByDefault(category: ToolCategory): boolean { + const flagName = categoryToFlagName(category); + const option = getCategoryOptions()[flagName]; + return !('default' in option) || option.default !== true; +} + +export function getOffByDefaultCategories(): ToolCategory[] { + const result: ToolCategory[] = []; + for (const category of Object.values(ToolCategory)) { + if (isCategoryOffByDefault(category)) { + result.push(category); + } + } + return result; +} diff --git a/src/bin/chrome-devtools-cli-options.ts b/src/config/cli-options.ts similarity index 80% rename from src/bin/chrome-devtools-cli-options.ts rename to src/config/cli-options.ts index 18e60e52..9f61b5e7 100644 --- a/src/bin/chrome-devtools-cli-options.ts +++ b/src/config/cli-options.ts @@ -6,7 +6,11 @@ * Modified by Opera Software AS. */ -// NOTE: do not edit manually. Auto-generated by 'npm run cli:generate'. +/** + * @fileoverview + * WARNING: This file is auto-generated by 'npm run cli:generate'. + * Do not edit this file manually. + */ export interface ArgDef { name: string; @@ -29,6 +33,12 @@ export const commands: Commands = { description: 'Clicks on the provided element', category: 'Input automation', args: { + pageId: { + name: 'pageId', + type: 'number', + description: 'Targets a specific page by ID.', + required: true, + }, uid: { name: 'uid', type: 'string', @@ -56,6 +66,12 @@ export const commands: Commands = { 'Clicks at the provided coordinates (requires flag: --experimentalVision=true)', category: 'Input automation', args: { + pageId: { + name: 'pageId', + type: 'number', + description: 'Targets a specific page by ID.', + required: true, + }, x: { name: 'x', type: 'number', @@ -142,6 +158,12 @@ export const commands: Commands = { description: 'Drag an element onto another element', category: 'Input automation', args: { + pageId: { + name: 'pageId', + type: 'number', + description: 'Targets a specific page by ID.', + required: true, + }, from_uid: { name: 'from_uid', type: 'string', @@ -164,9 +186,15 @@ export const commands: Commands = { }, }, emulate: { - description: 'Emulates various features on the selected page.', + description: 'Emulates various features on the target page.', category: 'Emulation', args: { + pageId: { + name: 'pageId', + type: 'number', + description: 'Targets a specific page by ID.', + required: true, + }, networkConditions: { name: 'networkConditions', type: 'string', @@ -221,14 +249,21 @@ export const commands: Commands = { }, evaluate_script: { description: - 'Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON, so returned values have to be JSON-serializable.', + 'Evaluate a JavaScript function inside the target page or service worker. Returns the response as JSON, so returned values have to be JSON-serializable.', category: 'Debugging', args: { + pageId: { + name: 'pageId', + type: 'number', + description: + 'Targets a specific page by ID. Required when not evaluating in a service worker.', + required: false, + }, function: { name: 'function', type: 'string', description: - 'A JavaScript function declaration to be executed by the tool in the currently selected page.\nExample without arguments: `() => document.title` or `async () => await fetch("example.com")`.\nExample with arguments: `(el) => el.innerText`\n', + 'A JavaScript function declaration to be executed by the tool in the target page.\nExample without arguments: `() => document.title` or `async () => await fetch("example.com")`.\nExample with arguments: `(el) => el.innerText`\n', required: true, }, args: { @@ -258,6 +293,13 @@ export const commands: Commands = { 'Whether to wait for the DOM to settle. Pass false if the script only reads data. Defaults to true.', required: false, }, + serviceWorkerId: { + name: 'serviceWorkerId', + type: 'string', + description: + "The optional service worker id to evaluate the script in. If provided, 'pageId' should be omitted. Note: 'args' (element UIDs) cannot be used when evaluating in a service worker.", + required: false, + }, }, }, execute_3p_developer_tool: { @@ -265,6 +307,12 @@ export const commands: Commands = { 'Executes a tool exposed by the page. (requires flag: --categoryExperimentalThirdParty=true)', category: 'Third-party', args: { + pageId: { + name: 'pageId', + type: 'number', + description: 'Targets a specific page by ID.', + required: true, + }, toolName: { name: 'toolName', type: 'string', @@ -284,6 +332,12 @@ export const commands: Commands = { 'Executes a WebMCP tool exposed by the page. (requires flag: --categoryExperimentalWebmcp=true)', category: 'WebMCP', args: { + pageId: { + name: 'pageId', + type: 'number', + description: 'Targets a specific page by ID.', + required: true, + }, toolName: { name: 'toolName', type: 'string', @@ -304,6 +358,12 @@ export const commands: Commands = { 'Type text into an input, text area or select an option from a