feat: make pageId required for page-scoped tools by default (#1777)

Removes `--experimental-page-id-routing` making it the default behavior.
To go back to the previous behavior, pass `--pageIdRouting=false` when
starting the server.

---------

Co-authored-by: Alex Rudenko <alexrudenko@chromium.org>
Co-authored-by: Samiya Caur <samiyac@google.com>
This commit is contained in:
Tolgahan Demirbaş
2026-08-24 16:54:06 +00:00
committed by GitHub
parent ebf58f2f4a
commit 50a16fae69
36 changed files with 724 additions and 197 deletions
+16 -13
View File
@@ -663,10 +663,10 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Type:** boolean
- **Default:** `false`
- **`--experimentalPageIdRouting`/ `--experimental-page-id-routing`**
Whether to expose pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions).
- **`--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:** `false`
- **Default:** `true`
- **`--experimentalDevtools`/ `--experimental-devtools`**
Whether to enable automation over DevTools targets
@@ -851,22 +851,25 @@ You can also run `npx chrome-devtools-mcp@latest --help` to see all available co
### 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.
Most MCP clients start one Chrome DevTools MCP server per conversation.
By default, the server runs with `--pageIdRouting` enabled, making `pageId` a
required parameter on page-scoped tools (such as `click`, `fill`, `navigate_page`,
`take_snapshot`, etc.) so multiple agents or subagents sharing a server instance can
route tool calls directly to the specific tab they are working with.
For `evaluate_script`, `pageId` is required by default for targeting pages, but
becomes optional when `--categoryExtensions` is enabled so that `serviceWorkerId`
can be specified instead to evaluate inside an extension background service worker.
To disable this behavior and default to the currently selected page, pass
`--no-page-id-routing`.
```json
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": [
"-y",
"chrome-devtools-mcp@latest",
"--experimentalPageIdRouting"
]
"args": ["-y", "chrome-devtools-mcp@latest"]
}
}
}
+26 -12
View File
@@ -23,11 +23,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 +42,7 @@ Thus, `--categoryExtensions` tools are currently not available in the CLI.
chrome-devtools <tool> [arguments] [flags]
```
- **Required Arguments**: Passed as positional arguments.
- **Required Arguments**: Passed as positional arguments. Page-scoped tools require `<pageId>` as their first positional argument.
- **Optional Arguments**: Passed as flags (e.g., `--filePath`, `--fullPage`).
### Examples
@@ -51,24 +51,38 @@ chrome-devtools <tool> [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
chrome-devtools fill "input-uid-456" "search query"
# 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 <number>`: `chrome-devtools evaluate_script "() => document.title" --pageId 1`
- Target an extension service worker using `--serviceWorkerId <string>`: `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
# 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
+51 -16
View File
@@ -79,6 +79,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.
@@ -92,6 +93,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.
@@ -103,6 +105,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.
@@ -116,6 +119,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.
---
@@ -127,6 +131,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.
---
@@ -137,6 +142,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.
@@ -149,6 +155,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.
---
@@ -159,6 +166,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"
@@ -171,6 +179,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.
@@ -182,6 +191,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.
@@ -215,6 +225,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.
@@ -254,6 +265,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.
@@ -263,10 +275,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.
@@ -279,11 +292,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
---
@@ -298,27 +312,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).
---
@@ -331,6 +348,7 @@
**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.
@@ -339,10 +357,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.
@@ -354,14 +373,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.
@@ -376,6 +396,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.
---
@@ -385,6 +406,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 &amp; audits. "snapshot" analyzes current state.
- **outputDirPath** (string) _(optional)_: Directory for reports. If omitted, uses temporary files.
@@ -393,10 +415,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.
@@ -412,6 +435,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.
@@ -422,12 +446,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.
@@ -435,19 +460,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.
---
@@ -455,11 +483,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.
---
@@ -682,6 +711,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
@@ -697,7 +727,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.
---
@@ -711,6 +743,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
@@ -720,7 +753,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.
---
+2 -2
View File
@@ -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[] {
@@ -102,6 +102,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[];
}
@@ -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,<textarea id="ta"></textarea>
- Page 2 at data:text/html,<h1>Other</h1>
@@ -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,<form id="login-form"><input type="text" id="username" placeholder="Username" /><input type="password" id="password" placeholder="Password" /><button type="submit" id="submit-login">Log In</button></form>';
const PAGE_B_URL =
'data:text/html,<form id="feedback-form"><input type="text" id="email" placeholder="Email" /><textarea id="comments" placeholder="Comments"></textarea><button type="submit" id="submit-feedback">Send</button></form>';
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',
);
},
};
@@ -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,<h1>Counter Page</h1><button id="inc-btn" onclick="document.getElementById(\'count\').innerText = parseInt(document.getElementById(\'count\').innerText) + 1">Increment</button><div id="count">0</div>';
const PAGE_INPUT =
'data:text/html,<h1>Input Page</h1><label for="val-input">Enter Counter Value:</label><input type="text" id="val-input" /><button id="submit-btn">Submit</button>';
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',
);
},
};
@@ -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,<button>Click A</button>
- Page B (isolatedContext "contextB") at data:text/html,<button>Click B</button>
+14 -4
View File
@@ -18,6 +18,7 @@ import {
OFF_BY_DEFAULT_CATEGORIES,
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';
@@ -434,7 +435,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 => {
@@ -455,8 +456,12 @@ function getToolsAndCategories(tools: any) {
const properties: Record<string, TypeInfo> = {};
const required: string[] = [];
const toolSchema = {
...tool.schema,
...(tool.pageScoped && !slim ? pageIdSchema : {}),
};
for (const [key, schema] of Object.entries(
tool.schema as unknown as Record<string, ZodSchema>,
toolSchema as unknown as Record<string, ZodSchema>,
)) {
const info = getZodTypeInfo(schema);
properties[key] = info;
@@ -520,7 +525,9 @@ async function generateToolDocumentation(): Promise<void> {
{
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,
@@ -536,7 +543,10 @@ async function generateToolDocumentation(): Promise<void> {
{
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,
+2
View File
@@ -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!
+65 -65
View File
@@ -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 `<uid>`.
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 <pageId>` to get an element `<uid>`.
3. **Act**: Use `chrome-devtools click <pageId> <uid>`, `chrome-devtools fill <pageId> <uid> <value>`, etc. State persists across commands.
Snapshot example:
@@ -44,23 +44,23 @@ chrome-devtools <tool> [arguments] [flags]
## Input Automation (<uid> 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 +68,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 +83,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 +125,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,13 +178,13 @@ 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
+5 -4
View File
@@ -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
+9 -9
View File
@@ -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.
+2 -2
View File
@@ -20,10 +20,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
+1
View File
@@ -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.
+2 -2
View File
@@ -242,7 +242,7 @@ export class ToolHandler {
this.inputSchema =
'pageScoped' in tool &&
tool.pageScoped &&
serverArgs.experimentalPageIdRouting &&
serverArgs.pageIdRouting &&
!serverArgs.slim
? {...pageIdSchema, ...tool.schema}
: tool.schema;
@@ -311,7 +311,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)
+3 -6
View File
@@ -60,7 +60,6 @@ function getCliOptions() {
// Change the defaults for the CLI.
delete options.experimentalStructuredContent;
delete options.experimentalInteropTools;
delete options.experimentalPageIdRouting;
return options;
}
@@ -103,13 +102,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"',
' - INCORRECT: chrome-devtools click --pageId 1 --uid "1_2"',
);
console.error(' - CORRECT: chrome-devtools click 1 "1_2"');
console.error(
' - CORRECT: chrome-devtools evaluate_script "() => document.title"',
);
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.',
+200 -16
View File
@@ -31,6 +31,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',
@@ -58,6 +64,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',
@@ -144,6 +156,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',
@@ -166,9 +184,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',
@@ -223,14 +247,21 @@ export const commands: Commands = {
},
evaluate_script: {
description:
'Evaluate a JavaScript function inside the currently selected page or service worker. 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: {
@@ -274,6 +305,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',
@@ -293,6 +330,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',
@@ -313,6 +356,12 @@ export const commands: Commands = {
'Type text into an input, text area or select an option from a <select> element.',
category: 'Input automation',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
uid: {
name: 'uid',
type: 'string',
@@ -341,6 +390,12 @@ export const commands: Commands = {
'Gets a console message by its ID. You can get all messages by calling list_console_messages.',
category: 'Debugging',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
msgid: {
name: 'msgid',
type: 'number',
@@ -651,6 +706,12 @@ export const commands: Commands = {
'Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel.',
category: 'Network',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
reqid: {
name: 'reqid',
type: 'number',
@@ -693,6 +754,12 @@ export const commands: Commands = {
'If a browser dialog was opened, use this command to handle it',
category: 'Input automation',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
action: {
name: 'action',
type: 'string',
@@ -712,6 +779,12 @@ export const commands: Commands = {
description: 'Hover over 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',
@@ -796,6 +869,12 @@ export const commands: Commands = {
'Get Lighthouse score and reports for accessibility, SEO, best practices, and agentic browsing. This excludes performance. For performance audits, run performance_start_trace',
category: 'Debugging',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
mode: {
name: 'mode',
type: 'string',
@@ -825,13 +904,26 @@ export const commands: Commands = {
description:
"Lists all third-party developer tools the page exposes for providing runtime information.\nThird-party developer tools can be called via the 'execute_3p_developer_tool()' MCP tool.\nAlternatively, third-party developer tools can be executed by calling 'evaluate_script' and adding the\nfollowing command to the script:\n`window.__dtmcp.executeTool(toolName, params)`\nThis might be helpful when the third-party developer tools return non-serializable values or when composing\nthird-party developer tools with additional functionality. (requires flag: --categoryExperimentalThirdParty=true)",
category: 'Third-party',
args: {},
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
},
},
list_console_messages: {
description:
'List all console messages for the currently selected page since the last navigation. This includes console messages originating from extensions content scripts.',
'List all console messages for the target page since the last navigation. This includes console messages originating from extensions content scripts.',
category: 'Debugging',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
pageSize: {
name: 'pageSize',
type: 'integer',
@@ -886,9 +978,15 @@ export const commands: Commands = {
},
list_network_requests: {
description:
'Lists the most recent requests for the currently selected page since the last navigation.',
'Lists the most recent requests for the target page since the last navigation.',
category: 'Network',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
pageSize: {
name: 'pageSize',
type: 'integer',
@@ -930,13 +1028,26 @@ export const commands: Commands = {
description:
'Lists all WebMCP tools the page exposes. (requires flag: --categoryExperimentalWebmcp=true)',
category: 'WebMCP',
args: {},
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
},
},
navigate_page: {
description:
'Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.',
category: 'Navigation automation',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
type: {
name: 'type',
type: 'string',
@@ -1020,6 +1131,12 @@ export const commands: Commands = {
'Provides more detailed information on a specific Performance Insight of an insight set that was highlighted in the results of a trace recording.',
category: 'Performance',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
insightSetId: {
name: 'insightSetId',
type: 'string',
@@ -1038,14 +1155,20 @@ export const commands: Commands = {
},
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.',
'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.',
category: 'Performance',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
reload: {
name: 'reload',
type: 'boolean',
description:
'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 tool BEFORE starting the trace if reload or autoStop is set to true.',
'Determines if, once tracing has started, the target page should be automatically reloaded. Navigate the page to the right URL using the navigate_page tool BEFORE starting the trace if reload or autoStop is set to true.',
required: false,
default: true,
},
@@ -1068,9 +1191,15 @@ export const commands: Commands = {
},
performance_stop_trace: {
description:
'Stop the active performance trace recording on the selected webpage.',
'Stop the active performance trace recording on the target webpage.',
category: 'Performance',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
filePath: {
name: 'filePath',
type: 'string',
@@ -1085,6 +1214,12 @@ export const commands: Commands = {
'Press a key or key combination. Use this when other input methods like fill() cannot be used (e.g., keyboard shortcuts, navigation keys, or special key combinations).',
category: 'Input automation',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
key: {
name: 'key',
type: 'string',
@@ -1188,9 +1323,15 @@ export const commands: Commands = {
},
resize_page: {
description:
"Resizes the selected page's window so that the page has specified dimension",
"Resizes the page's window so that the page has specified dimension",
category: 'Emulation',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
width: {
name: 'width',
type: 'number',
@@ -1207,9 +1348,15 @@ export const commands: Commands = {
},
screencast_start: {
description:
'Starts recording a screencast (video) of the selected page in specified format. (requires flag: --experimentalScreencast=true)',
'Starts recording a screencast (video) of the target page in specified format. (requires flag: --experimentalScreencast=true)',
category: 'Debugging',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
filePath: {
name: 'filePath',
type: 'string',
@@ -1221,9 +1368,16 @@ export const commands: Commands = {
},
screencast_stop: {
description:
'Stops the active screencast recording on the selected page. (requires flag: --experimentalScreencast=true)',
'Stops the active screencast recording on the target page. (requires flag: --experimentalScreencast=true)',
category: 'Debugging',
args: {},
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
},
},
select_page: {
description: 'Select a page as a context for future tool calls.',
@@ -1246,9 +1400,15 @@ export const commands: Commands = {
},
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.',
'Capture a heap snapshot of the target page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.',
category: 'Memory',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
filePath: {
name: 'filePath',
type: 'string',
@@ -1262,6 +1422,12 @@ export const commands: Commands = {
description: 'Take a screenshot of the page or element.',
category: 'Debugging',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
format: {
name: 'format',
type: 'string',
@@ -1303,9 +1469,15 @@ export const commands: Commands = {
},
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\nidentifier (uid). Always use the latest snapshot. Prefer taking a snapshot over taking a screenshot. The snapshot indicates the element selected\nin the DevTools Elements panel (if any).',
'Take a text snapshot of the target page based on the a11y tree. The snapshot lists page elements along with a unique\nidentifier (uid). Always use the latest snapshot. Prefer taking a snapshot over taking a screenshot. The snapshot indicates the element selected\nin the DevTools Elements panel (if any).',
category: 'Debugging',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
verbose: {
name: 'verbose',
type: 'boolean',
@@ -1339,6 +1511,12 @@ export const commands: Commands = {
description: 'Type text using keyboard into a previously focused input',
category: 'Input automation',
args: {
pageId: {
name: 'pageId',
type: 'number',
description: 'Targets a specific page by ID.',
required: true,
},
text: {
name: 'text',
type: 'string',
@@ -1385,6 +1563,12 @@ export const commands: Commands = {
description: 'Upload a file through a 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',
+3 -2
View File
@@ -147,10 +147,11 @@ export const mcpOptions = {
type: 'boolean',
description: `If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.`,
},
experimentalPageIdRouting: {
pageIdRouting: {
type: 'boolean',
describe:
'Whether to expose pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions).',
'Require pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions). Use --no-page-id-routing to disable.',
default: true,
},
experimentalDevtools: {
type: 'boolean',
+12 -2
View File
@@ -242,11 +242,13 @@
},
{
"name": "experimental_page_id_routing",
"flagType": "boolean"
"flagType": "boolean",
"isDeprecated": true
},
{
"name": "experimental_page_id_routing_present",
"flagType": "boolean"
"flagType": "boolean",
"isDeprecated": true
},
{
"name": "experimental_webmcp",
@@ -300,6 +302,14 @@
"name": "category_experimental_third_party",
"flagType": "boolean"
},
{
"name": "page_id_routing_present",
"flagType": "boolean"
},
{
"name": "page_id_routing",
"flagType": "boolean"
},
{
"name": "memory_debugging_present",
"flagType": "boolean"
+6 -1
View File
@@ -123,6 +123,10 @@
{
"name": "wait_for_stable_dom",
"argType": "boolean"
},
{
"name": "page_id",
"argType": "number"
}
]
},
@@ -188,7 +192,8 @@
"args": [
{
"name": "page_id",
"argType": "number"
"argType": "number",
"isDeprecated": true
}
]
},
+1 -1
View File
@@ -42,7 +42,7 @@ const LIST_CONSOLE_MESSAGES_TOOL_NAME = 'list_console_messages';
export const listConsoleMessages = definePageTool(cliArgs => {
return {
name: LIST_CONSOLE_MESSAGES_TOOL_NAME,
description: `List all console messages for the currently selected page since the last navigation.${cliArgs?.categoryExtensions ? ' This includes console messages originating from extensions content scripts.' : ''}`,
description: `List all console messages for the target page since the last navigation.${cliArgs?.categoryExtensions ? ' This includes console messages originating from extensions content scripts.' : ''}`,
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: true,
+1 -1
View File
@@ -47,7 +47,7 @@ const throttlingOptions: [string, ...string[]] = [
export const emulate = definePageTool({
name: 'emulate',
description: `Emulates various features on the selected page.`,
description: `Emulates various features on the target page.`,
annotations: {
category: ToolCategory.EMULATION,
readOnlyHint: false,
+1 -1
View File
@@ -22,7 +22,7 @@ const HEAP_SNAPSHOT_FILTERS: readonly [string, ...string[]] = [
export const takeHeapSnapshot = definePageTool({
name: '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.`,
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: false,
+1 -1
View File
@@ -34,7 +34,7 @@ const FILTERABLE_RESOURCE_TYPES: readonly [ResourceType, ...ResourceType[]] = [
export const listNetworkRequests = definePageTool({
name: '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.`,
annotations: {
category: ToolCategory.NETWORK,
readOnlyHint: true,
+4 -10
View File
@@ -296,7 +296,7 @@ export const navigatePage = definePageTool(() => {
export const resizePage = definePageTool({
name: '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`,
annotations: {
category: ToolCategory.EMULATION,
readOnlyHint: false,
@@ -396,17 +396,11 @@ export const getTabId = definePageTool({
readOnlyHint: true,
conditions: ['experimentalInteropTools'],
},
schema: {
pageId: zod
.number()
.describe(
`The ID of the page to get the tab ID for. Call ${listPages().name} to get available pages.`,
),
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response, context) => {
const page = context.getPageById(request.params.pageId);
handler: async (request, response) => {
const page = request.page;
const tabId = (page.pptrPage as unknown as CdpPage)._tabId;
response.setTabId(tabId);
response.appendResponseLine(`Tab ID: ${tabId}`);
+3 -3
View File
@@ -27,7 +27,7 @@ const filePathSchema = zod
export const startTrace = definePageTool({
name: '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.`,
annotations: {
category: ToolCategory.PERFORMANCE,
readOnlyHint: false,
@@ -37,7 +37,7 @@ export const startTrace = definePageTool({
.boolean()
.default(true)
.describe(
'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 tool BEFORE starting the trace if reload or autoStop is set to true.',
'Determines if, once tracing has started, the target page should be automatically reloaded. Navigate the page to the right URL using the navigate_page tool BEFORE starting the trace if reload or autoStop is set to true.',
),
autoStop: zod
.boolean()
@@ -125,7 +125,7 @@ export const startTrace = definePageTool({
export const stopTrace = definePageTool({
name: 'performance_stop_trace',
description:
'Stop the active performance trace recording on the selected webpage.',
'Stop the active performance trace recording on the target webpage.',
annotations: {
category: ToolCategory.PERFORMANCE,
readOnlyHint: false,
+2 -2
View File
@@ -25,7 +25,7 @@ const supportedExtensions: SupportedVideoExtension[] = ['.webm', '.mp4'];
export const startScreencast = definePageTool(args => ({
name: 'screencast_start',
description: `Starts recording a screencast (video) of the selected page in specified format.`,
description: `Starts recording a screencast (video) of the target page in specified format.`,
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
@@ -123,7 +123,7 @@ export const startScreencast = definePageTool(args => ({
export const stopScreencast = definePageTool({
name: 'screencast_stop',
description: 'Stops the active screencast recording on the selected page.',
description: 'Stops the active screencast recording on the target page.',
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
+22 -6
View File
@@ -17,15 +17,26 @@ export type Evaluatable = Page | Frame | WebWorker;
export const evaluateScript = defineTool(cliArgs => {
return {
name: 'evaluate_script',
description: `Evaluate a JavaScript function inside the currently selected page${cliArgs?.categoryExtensions ? ' or service worker' : ''}. Returns the response as JSON, so returned values have to be JSON-serializable.`,
description: `Evaluate a JavaScript function inside the target page${cliArgs?.categoryExtensions ? ' or service worker' : ''}. Returns the response as JSON, so returned values have to be JSON-serializable.`,
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
},
schema: {
...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}),
...(cliArgs?.pageIdRouting
? cliArgs.categoryExtensions
? {
pageId: zod
.number()
.optional()
.describe(
'Targets a specific page by ID. Required when not evaluating in a service worker.',
),
}
: pageIdSchema
: {}),
function: zod.string().describe(
`A JavaScript function declaration to be executed by the tool in the currently selected page.
`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\`
`,
@@ -114,9 +125,14 @@ Example with arguments: \`(el) => el.innerText\`
return;
}
const mcpPage = cliArgs?.experimentalPageIdRouting
? context.getPageById(request.params.pageId)
: context.getSelectedMcpPage();
if (cliArgs?.categoryExtensions && cliArgs?.pageIdRouting && !pageId) {
throw new Error('specify either a pageId or a serviceWorkerId.');
}
const mcpPage =
cliArgs?.pageIdRouting && request.params.pageId
? context.getPageById(request.params.pageId)
: context.getSelectedMcpPage();
const page: Page = mcpPage.pptrPage;
const args: Array<JSHandle<unknown>> = [];
+1 -1
View File
@@ -11,7 +11,7 @@ import {definePageTool, timeoutSchema} from './ToolDefinition.js';
export const takeSnapshot = definePageTool({
name: '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).`,
annotations: {
+50 -4
View File
@@ -32,7 +32,51 @@ describe('ToolHandler', () => {
ClearcutLogger.resetForTesting();
});
it('calls page getter for page scoped tools', async () => {
it('calls getPageById for page scoped tools when pageId is provided', async () => {
let handlerCalled = false;
const tool: DefinedPageTool = {
name: 'page_tool',
description: 'A page scoped tool',
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: {},
pageScoped: true,
handler: async () => {
handlerCalled = true;
},
};
const mockContext = sinon.createStubInstance(McpContext);
const mockProcess = sinon.createStubInstance(ChildProcess);
mockContext.browser = getMockBrowser({process: mockProcess});
const mockPage = sinon.createStubInstance(McpPage);
mockContext.getPageById.returns(mockPage);
const toolMutex = new Mutex();
const serverArgs = parseArguments('1.0.0', ['node', 'script.js'], {
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
});
const toolHandler = new ToolHandler(
tool,
serverArgs,
async () => mockContext,
toolMutex,
);
assert.strictEqual(toolHandler.shouldRegister, true);
await toolHandler.handle({pageId: 1});
assert.strictEqual(mockContext.getPageById.calledOnce, true);
assert.strictEqual(mockContext.getPageById.calledWith(1), true);
assert.strictEqual(handlerCalled, true);
});
it('calls getSelectedMcpPage for page scoped tools when pageIdRouting is disabled', async () => {
let handlerCalled = false;
const tool: DefinedPageTool = {
name: 'page_tool',
@@ -57,9 +101,11 @@ describe('ToolHandler', () => {
mockContext.getSelectedMcpPage.returns(mockPage);
const toolMutex = new Mutex();
const serverArgs = parseArguments('1.0.0', ['node', 'script.js'], {
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
});
const serverArgs = parseArguments(
'1.0.0',
['node', 'script.js', '--no-page-id-routing'],
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
);
const toolHandler = new ToolHandler(
tool,
+1
View File
@@ -29,6 +29,7 @@ describe('cli args parsing', () => {
allowUnrestrictedPaths: false,
memoryDebugging: false,
experimentalStructuredContent: false,
pageIdRouting: true,
};
it('parses with default args', async () => {
+5 -5
View File
@@ -60,7 +60,7 @@ describe('chrome-devtools', () => {
`start command failed: ${startResult.stderr}`,
);
const result = await runCli(['take_screenshot'], sessionId);
const result = await runCli(['take_screenshot', '1'], sessionId);
assert.strictEqual(
result.status,
0,
@@ -75,7 +75,7 @@ describe('chrome-devtools', () => {
it('fails to invoke list_network_requests when categoryNetwork is disabled', async () => {
await runCli(['start', '--categoryNetwork=false'], sessionId);
const result = await runCli(['list_network_requests'], sessionId);
const result = await runCli(['list_network_requests', '1'], sessionId);
assert.strictEqual(result.status, 0);
assert(
@@ -93,7 +93,7 @@ describe('chrome-devtools', () => {
it('fails to invoke click_at when experimentalVision is disabled (default)', async () => {
await runCli(['start'], sessionId);
const result = await runCli(['click_at', '100', '100'], sessionId);
const result = await runCli(['click_at', '1', '100', '100'], sessionId);
assert.strictEqual(result.status, 0);
assert(
result.stdout.includes(
@@ -119,7 +119,7 @@ describe('chrome-devtools', () => {
);
const emulateResult = await runCli(
['emulate', '--cpuThrottlingRate', '2'],
['emulate', '1', '--cpuThrottlingRate', '2'],
sessionId,
);
assert.strictEqual(
@@ -128,7 +128,7 @@ describe('chrome-devtools', () => {
`emulate command failed: ${emulateResult.stderr}`,
);
const result = await runCli(['performance_start_trace'], sessionId);
const result = await runCli(['performance_start_trace', '1'], sessionId);
assert.strictEqual(
result.status,
0,
+11 -2
View File
@@ -244,6 +244,7 @@ describe('e2e', () => {
const result = await client.callTool({
name: 'take_screenshot',
arguments: {
pageId: 1,
filePath: path.resolve(os.homedir(), 'test.png'),
},
});
@@ -271,6 +272,7 @@ describe('e2e', () => {
const result = await client.callTool({
name: 'take_screenshot',
arguments: {
pageId: 1,
filePath: path.join(os.tmpdir(), 'test.png'),
},
});
@@ -349,7 +351,10 @@ describe('e2e', () => {
const result = await client.callTool({
name: 'take_screenshot',
arguments: {filePath: path.join(workspace, 'shot.png')},
arguments: {
pageId: 1,
filePath: path.join(workspace, 'shot.png'),
},
});
// Asserted before isError so a denial reports the path it rejected
@@ -381,7 +386,9 @@ describe('e2e', () => {
const snapshotResult = await client.callTool({
name: 'take_snapshot',
arguments: {},
arguments: {
pageId: 2,
},
});
const snapshotText = (snapshotResult.content as TextContent[])[0].text;
@@ -392,6 +399,7 @@ describe('e2e', () => {
const result = await client.callTool({
name: 'click',
arguments: {
pageId: 2,
uid,
},
});
@@ -412,6 +420,7 @@ describe('e2e', () => {
const result = await client.callTool({
name: 'take_screenshot',
arguments: {
pageId: 2,
// Use os.tmpdir() so validatePath passes on macOS/Windows before
// reaching the dialog-blocked check.
filePath: path.join(os.tmpdir(), 'test.png'),
+1 -1
View File
@@ -1338,7 +1338,7 @@ describe('pages', () => {
// @ts-expect-error _tabId is internal.
page._tabId = 'test-tab-id';
await getTabId.handler(
{params: {pageId: 1}, page: context.getSelectedMcpPage()},
{params: {}, page: context.getSelectedMcpPage()},
response,
context,
);
+38 -1
View File
@@ -12,6 +12,7 @@ import sinon from 'sinon';
import type {ParsedArguments} from '../../src/config/mcp-options.js';
import {TextSnapshot} from '../../src/TextSnapshot.js';
import {zod} from '../../src/third_party/index.js';
import {installExtension} from '../../src/tools/extensions.js';
import {evaluateScript} from '../../src/tools/script.js';
import {WaitForHelper} from '../../src/utils/WaitForHelper.js';
@@ -426,7 +427,7 @@ describe('script', () => {
params: {
function: String(() => 'test'),
serviceWorkerId: 'example_service_worker',
pageId: '1',
pageId: 1,
},
},
response,
@@ -469,5 +470,41 @@ describe('script', () => {
{categoryExtensions: true},
);
});
it('makes pageId optional in schema when categoryExtensions is true and pageIdRouting is true', () => {
const tool = evaluateScript({
categoryExtensions: true,
pageIdRouting: true,
} as ParsedArguments);
const schema = zod.object(tool.schema);
const validSw = schema.safeParse({
function: '() => 1',
serviceWorkerId: 'sw_1',
});
assert.strictEqual(validSw.success, true);
const validPage = schema.safeParse({
function: '() => 1',
pageId: 1,
});
assert.strictEqual(validPage.success, true);
});
it('makes pageId required in schema when categoryExtensions is false and pageIdRouting is true', () => {
const tool = evaluateScript({
pageIdRouting: true,
} as ParsedArguments);
const schema = zod.object(tool.schema);
const resultWithoutPageId = schema.safeParse({
function: '() => 1',
});
assert.strictEqual(resultWithoutPageId.success, false);
const resultWithPageId = schema.safeParse({
function: '() => 1',
pageId: 1,
});
assert.strictEqual(resultWithPageId.success, true);
});
});
});