chrome-devtools-mcp 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -23
- package/build/src/McpContext.js +14 -5
- package/build/src/McpResponse.js +36 -17
- package/build/src/browser.js +3 -2
- package/build/src/cli.js +30 -0
- package/build/src/formatters/SnapshotFormatter.js +128 -0
- package/build/src/formatters/consoleFormatter.js +40 -5
- package/build/src/main.js +41 -7
- package/build/src/telemetry/clearcut-logger.js +28 -0
- package/build/src/telemetry/clearcut-sender.js +11 -0
- package/build/src/telemetry/flag-utils.js +45 -0
- package/build/src/telemetry/types.js +27 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +5 -5
- package/build/src/third_party/index.js +1213 -799
- package/build/src/tools/input.js +31 -4
- package/build/src/tools/pages.js +42 -7
- package/build/src/tools/performance.js +32 -7
- package/build/src/utils/string.js +36 -0
- package/package.json +13 -13
- package/build/src/formatters/snapshotFormatter.js +0 -73
- package/build/src/third_party/issue-descriptions/SameSiteInvalidSameParty.md +0 -8
- package/build/src/third_party/issue-descriptions/SameSiteSamePartyCrossPartyContextSet.md +0 -10
package/README.md
CHANGED
|
@@ -91,10 +91,10 @@ Chrome DevTools MCP will not start the browser instance automatically using this
|
|
|
91
91
|
|
|
92
92
|
<details>
|
|
93
93
|
<summary>Claude Code</summary>
|
|
94
|
-
Use the Claude Code CLI to add the Chrome DevTools MCP server (<a href="https://
|
|
94
|
+
Use the Claude Code CLI to add the Chrome DevTools MCP server (<a href="https://code.claude.com/docs/en/mcp">guide</a>):
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
|
-
claude mcp add chrome-devtools npx chrome-devtools-mcp@latest
|
|
97
|
+
claude mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
</details>
|
|
@@ -241,6 +241,25 @@ Or, from the IDE **Activity Bar** > `Kiro` > `MCP Servers` > `Click Open MCP Con
|
|
|
241
241
|
|
|
242
242
|
</details>
|
|
243
243
|
|
|
244
|
+
<details>
|
|
245
|
+
<summary>OpenCode</summary>
|
|
246
|
+
|
|
247
|
+
Add the following configuration to your `opencode.json` file. If you don't have one, create it at `~/.config/opencode/opencode.json` (<a href="https://opencode.ai/docs/mcp-servers">guide</a>):
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"$schema": "https://opencode.ai/config.json",
|
|
252
|
+
"mcp": {
|
|
253
|
+
"chrome-devtools": {
|
|
254
|
+
"type": "local",
|
|
255
|
+
"command": ["npx", "-y", "chrome-devtools-mcp@latest"]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
</details>
|
|
262
|
+
|
|
244
263
|
<details>
|
|
245
264
|
<summary>Qoder</summary>
|
|
246
265
|
|
|
@@ -350,20 +369,20 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
350
369
|
|
|
351
370
|
<!-- BEGIN AUTO GENERATED OPTIONS -->
|
|
352
371
|
|
|
353
|
-
- **`--autoConnect`**
|
|
372
|
+
- **`--autoConnect`/ `--auto-connect`**
|
|
354
373
|
If specified, automatically connects to a browser (Chrome 145+) running in the user data directory identified by the channel param. Requires remote debugging being enabled in Chrome here: chrome://inspect/#remote-debugging.
|
|
355
374
|
- **Type:** boolean
|
|
356
375
|
- **Default:** `false`
|
|
357
376
|
|
|
358
|
-
- **`--browserUrl`, `-u`**
|
|
377
|
+
- **`--browserUrl`/ `--browser-url`, `-u`**
|
|
359
378
|
Connect to a running, debuggable Chrome instance (e.g. `http://127.0.0.1:9222`). For more details see: https://github.com/ChromeDevTools/chrome-devtools-mcp#connecting-to-a-running-chrome-instance.
|
|
360
379
|
- **Type:** string
|
|
361
380
|
|
|
362
|
-
- **`--wsEndpoint`, `-w`**
|
|
381
|
+
- **`--wsEndpoint`/ `--ws-endpoint`, `-w`**
|
|
363
382
|
WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.
|
|
364
383
|
- **Type:** string
|
|
365
384
|
|
|
366
|
-
- **`--wsHeaders`**
|
|
385
|
+
- **`--wsHeaders`/ `--ws-headers`**
|
|
367
386
|
Custom headers for WebSocket connection in JSON format (e.g., '{"Authorization":"Bearer token"}'). Only works with --wsEndpoint.
|
|
368
387
|
- **Type:** string
|
|
369
388
|
|
|
@@ -372,7 +391,7 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
372
391
|
- **Type:** boolean
|
|
373
392
|
- **Default:** `false`
|
|
374
393
|
|
|
375
|
-
- **`--executablePath`, `-e`**
|
|
394
|
+
- **`--executablePath`/ `--executable-path`, `-e`**
|
|
376
395
|
Path to custom Chrome executable.
|
|
377
396
|
- **Type:** string
|
|
378
397
|
|
|
@@ -380,7 +399,7 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
380
399
|
If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to false.
|
|
381
400
|
- **Type:** boolean
|
|
382
401
|
|
|
383
|
-
- **`--userDataDir`**
|
|
402
|
+
- **`--userDataDir`/ `--user-data-dir`**
|
|
384
403
|
Path to the user data directory for Chrome. Default is $HOME/.cache/chrome-devtools-mcp/chrome-profile$CHANNEL_SUFFIX_IF_NON_STABLE
|
|
385
404
|
- **Type:** string
|
|
386
405
|
|
|
@@ -389,7 +408,7 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
389
408
|
- **Type:** string
|
|
390
409
|
- **Choices:** `stable`, `canary`, `beta`, `dev`
|
|
391
410
|
|
|
392
|
-
- **`--logFile`**
|
|
411
|
+
- **`--logFile`/ `--log-file`**
|
|
393
412
|
Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.
|
|
394
413
|
- **Type:** string
|
|
395
414
|
|
|
@@ -397,29 +416,33 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
397
416
|
Initial viewport size for the Chrome instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.
|
|
398
417
|
- **Type:** string
|
|
399
418
|
|
|
400
|
-
- **`--proxyServer`**
|
|
419
|
+
- **`--proxyServer`/ `--proxy-server`**
|
|
401
420
|
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.
|
|
402
421
|
- **Type:** string
|
|
403
422
|
|
|
404
|
-
- **`--acceptInsecureCerts`**
|
|
423
|
+
- **`--acceptInsecureCerts`/ `--accept-insecure-certs`**
|
|
405
424
|
If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
|
|
406
425
|
- **Type:** boolean
|
|
407
426
|
|
|
408
|
-
- **`--chromeArg`**
|
|
427
|
+
- **`--chromeArg`/ `--chrome-arg`**
|
|
409
428
|
Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
|
|
410
429
|
- **Type:** array
|
|
411
430
|
|
|
412
|
-
- **`--
|
|
431
|
+
- **`--ignoreDefaultChromeArg`/ `--ignore-default-chrome-arg`**
|
|
432
|
+
Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
|
|
433
|
+
- **Type:** array
|
|
434
|
+
|
|
435
|
+
- **`--categoryEmulation`/ `--category-emulation`**
|
|
413
436
|
Set to false to exclude tools related to emulation.
|
|
414
437
|
- **Type:** boolean
|
|
415
438
|
- **Default:** `true`
|
|
416
439
|
|
|
417
|
-
- **`--categoryPerformance`**
|
|
440
|
+
- **`--categoryPerformance`/ `--category-performance`**
|
|
418
441
|
Set to false to exclude tools related to performance.
|
|
419
442
|
- **Type:** boolean
|
|
420
443
|
- **Default:** `true`
|
|
421
444
|
|
|
422
|
-
- **`--categoryNetwork`**
|
|
445
|
+
- **`--categoryNetwork`/ `--category-network`**
|
|
423
446
|
Set to false to exclude tools related to network.
|
|
424
447
|
- **Type:** boolean
|
|
425
448
|
- **Default:** `true`
|
|
@@ -499,7 +522,7 @@ In these cases, start Chrome first and let the Chrome DevTools MCP server connec
|
|
|
499
522
|
|
|
500
523
|
**Step 1:** Set up remote debugging in Chrome
|
|
501
524
|
|
|
502
|
-
In Chrome, do the following to set up remote debugging:
|
|
525
|
+
In Chrome (\>= M144), do the following to set up remote debugging:
|
|
503
526
|
|
|
504
527
|
1. Navigate to `chrome://inspect/#remote-debugging` to enable remote debugging.
|
|
505
528
|
2. Follow the dialog UI to allow or disallow incoming debugging connections.
|
|
@@ -516,17 +539,13 @@ The following code snippet is an example configuration for gemini-cli:
|
|
|
516
539
|
"mcpServers": {
|
|
517
540
|
"chrome-devtools": {
|
|
518
541
|
"command": "npx",
|
|
519
|
-
"args": [
|
|
520
|
-
"chrome-devtools-mcp@latest",
|
|
521
|
-
"--autoConnect",
|
|
522
|
-
"--channel=canary"
|
|
523
|
-
]
|
|
542
|
+
"args": ["chrome-devtools-mcp@latest", "--autoConnect", "--channel=beta"]
|
|
524
543
|
}
|
|
525
544
|
}
|
|
526
545
|
}
|
|
527
546
|
```
|
|
528
547
|
|
|
529
|
-
Note: you have to specify `--channel=
|
|
548
|
+
Note: you have to specify `--channel=beta` until Chrome M144 has reached the
|
|
530
549
|
stable channel.
|
|
531
550
|
|
|
532
551
|
**Step 3:** Test your setup
|
|
@@ -537,7 +556,8 @@ Make sure your browser is running. Open gemini-cli and run the following prompt:
|
|
|
537
556
|
Check the performance of https://developers.chrome.com
|
|
538
557
|
```
|
|
539
558
|
|
|
540
|
-
|
|
559
|
+
> [!NOTE]
|
|
560
|
+
> The <code>autoConnect</code> 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.
|
|
541
561
|
|
|
542
562
|
The Chrome DevTools MCP server will try to connect to your running Chrome
|
|
543
563
|
instance. It shows a dialog asking for user permission.
|
package/build/src/McpContext.js
CHANGED
|
@@ -56,6 +56,8 @@ export class McpContext {
|
|
|
56
56
|
#cpuThrottlingRateMap = new WeakMap();
|
|
57
57
|
#geolocationMap = new WeakMap();
|
|
58
58
|
#dialog;
|
|
59
|
+
#pageIdMap = new WeakMap();
|
|
60
|
+
#nextPageId = 1;
|
|
59
61
|
#nextSnapshotId = 1;
|
|
60
62
|
#traceResults = [];
|
|
61
63
|
#locatorClass;
|
|
@@ -163,11 +165,11 @@ export class McpContext {
|
|
|
163
165
|
this.#consoleCollector.addPage(page);
|
|
164
166
|
return page;
|
|
165
167
|
}
|
|
166
|
-
async closePage(
|
|
168
|
+
async closePage(pageId) {
|
|
167
169
|
if (this.#pages.length === 1) {
|
|
168
170
|
throw new Error(CLOSE_PAGE_ERROR);
|
|
169
171
|
}
|
|
170
|
-
const page = this.
|
|
172
|
+
const page = this.getPageById(pageId);
|
|
171
173
|
await page.close({ runBeforeUnload: false });
|
|
172
174
|
}
|
|
173
175
|
getNetworkRequestById(reqid) {
|
|
@@ -231,14 +233,16 @@ export class McpContext {
|
|
|
231
233
|
}
|
|
232
234
|
return page;
|
|
233
235
|
}
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const page = pages[idx];
|
|
236
|
+
getPageById(pageId) {
|
|
237
|
+
const page = this.#pages.find(p => this.#pageIdMap.get(p) === pageId);
|
|
237
238
|
if (!page) {
|
|
238
239
|
throw new Error('No page found');
|
|
239
240
|
}
|
|
240
241
|
return page;
|
|
241
242
|
}
|
|
243
|
+
getPageId(page) {
|
|
244
|
+
return this.#pageIdMap.get(page);
|
|
245
|
+
}
|
|
242
246
|
#dialogHandler = (dialog) => {
|
|
243
247
|
this.#dialog = dialog;
|
|
244
248
|
};
|
|
@@ -302,6 +306,11 @@ export class McpContext {
|
|
|
302
306
|
*/
|
|
303
307
|
async createPagesSnapshot() {
|
|
304
308
|
const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
|
|
309
|
+
for (const page of allPages) {
|
|
310
|
+
if (!this.#pageIdMap.has(page)) {
|
|
311
|
+
this.#pageIdMap.set(page, this.#nextPageId++);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
305
314
|
this.#pages = allPages.filter(page => {
|
|
306
315
|
// If we allow debugging DevTools windows, return all pages.
|
|
307
316
|
// If we are in regular mode, the user should only see non-DevTools page.
|
package/build/src/McpResponse.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { mapIssueToMessageObject } from './DevtoolsUtils.js';
|
|
7
7
|
import { formatConsoleEventShort, formatConsoleEventVerbose, } from './formatters/consoleFormatter.js';
|
|
8
8
|
import { getFormattedHeaderValue, getFormattedResponseBody, getFormattedRequestBody, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
|
|
9
|
-
import {
|
|
9
|
+
import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
|
|
10
10
|
import { DevTools } from './third_party/index.js';
|
|
11
11
|
import { handleDialog } from './tools/pages.js';
|
|
12
12
|
import { paginate } from './utils/pagination.js';
|
|
@@ -20,9 +20,13 @@ export class McpResponse {
|
|
|
20
20
|
#networkRequestsOptions;
|
|
21
21
|
#consoleDataOptions;
|
|
22
22
|
#devToolsData;
|
|
23
|
+
#tabId;
|
|
23
24
|
attachDevToolsData(data) {
|
|
24
25
|
this.#devToolsData = data;
|
|
25
26
|
}
|
|
27
|
+
setTabId(tabId) {
|
|
28
|
+
this.#tabId = tabId;
|
|
29
|
+
}
|
|
26
30
|
setIncludePages(value) {
|
|
27
31
|
this.#includePages = value;
|
|
28
32
|
}
|
|
@@ -112,17 +116,18 @@ export class McpResponse {
|
|
|
112
116
|
if (this.#includePages) {
|
|
113
117
|
await context.createPagesSnapshot();
|
|
114
118
|
}
|
|
115
|
-
let
|
|
119
|
+
let snapshot;
|
|
116
120
|
if (this.#snapshotParams) {
|
|
117
121
|
await context.createTextSnapshot(this.#snapshotParams.verbose, this.#devToolsData);
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
122
|
+
const textSnapshot = context.getTextSnapshot();
|
|
123
|
+
if (textSnapshot) {
|
|
124
|
+
const formatter = new SnapshotFormatter(textSnapshot);
|
|
120
125
|
if (this.#snapshotParams.filePath) {
|
|
121
|
-
await context.saveFile(new TextEncoder().encode(
|
|
122
|
-
|
|
126
|
+
await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath);
|
|
127
|
+
snapshot = this.#snapshotParams.filePath;
|
|
123
128
|
}
|
|
124
129
|
else {
|
|
125
|
-
|
|
130
|
+
snapshot = formatter;
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
133
|
}
|
|
@@ -157,8 +162,9 @@ export class McpResponse {
|
|
|
157
162
|
}
|
|
158
163
|
else if (message instanceof DevTools.AggregatedIssue) {
|
|
159
164
|
const mappedIssueMessage = mapIssueToMessageObject(message);
|
|
160
|
-
if (!mappedIssueMessage)
|
|
165
|
+
if (!mappedIssueMessage) {
|
|
161
166
|
throw new Error("Can't provide detals for the msgid " + consoleMessageStableId);
|
|
167
|
+
}
|
|
162
168
|
consoleData = {
|
|
163
169
|
consoleMessageStableId,
|
|
164
170
|
...mappedIssueMessage,
|
|
@@ -208,8 +214,9 @@ export class McpResponse {
|
|
|
208
214
|
}
|
|
209
215
|
if (item instanceof DevTools.AggregatedIssue) {
|
|
210
216
|
const mappedIssueMessage = mapIssueToMessageObject(item);
|
|
211
|
-
if (!mappedIssueMessage)
|
|
217
|
+
if (!mappedIssueMessage) {
|
|
212
218
|
return null;
|
|
219
|
+
}
|
|
213
220
|
return {
|
|
214
221
|
consoleMessageStableId,
|
|
215
222
|
...mappedIssueMessage,
|
|
@@ -227,7 +234,7 @@ export class McpResponse {
|
|
|
227
234
|
bodies,
|
|
228
235
|
consoleData,
|
|
229
236
|
consoleListData,
|
|
230
|
-
|
|
237
|
+
snapshot,
|
|
231
238
|
});
|
|
232
239
|
}
|
|
233
240
|
format(toolName, context, data) {
|
|
@@ -257,16 +264,25 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
257
264
|
}
|
|
258
265
|
if (this.#includePages) {
|
|
259
266
|
const parts = [`## Pages`];
|
|
260
|
-
let idx = 0;
|
|
261
267
|
for (const page of context.getPages()) {
|
|
262
|
-
parts.push(`${
|
|
263
|
-
idx++;
|
|
268
|
+
parts.push(`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}`);
|
|
264
269
|
}
|
|
265
270
|
response.push(...parts);
|
|
266
271
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
272
|
+
const structuredContent = {};
|
|
273
|
+
if (this.#tabId) {
|
|
274
|
+
structuredContent.tabId = this.#tabId;
|
|
275
|
+
}
|
|
276
|
+
if (data.snapshot) {
|
|
277
|
+
if (typeof data.snapshot === 'string') {
|
|
278
|
+
response.push(`Saved snapshot to ${data.snapshot}.`);
|
|
279
|
+
structuredContent.snapshotFilePath = data.snapshot;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
response.push('## Latest page snapshot');
|
|
283
|
+
response.push(data.snapshot.toString());
|
|
284
|
+
structuredContent.snapshot = data.snapshot.toJSON();
|
|
285
|
+
}
|
|
270
286
|
}
|
|
271
287
|
response.push(...this.#formatNetworkRequestData(context, data.bodies));
|
|
272
288
|
response.push(...this.#formatConsoleData(context, data.consoleData));
|
|
@@ -315,7 +331,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
315
331
|
...imageData,
|
|
316
332
|
};
|
|
317
333
|
});
|
|
318
|
-
return
|
|
334
|
+
return {
|
|
335
|
+
content: [text, ...images],
|
|
336
|
+
structuredContent,
|
|
337
|
+
};
|
|
319
338
|
}
|
|
320
339
|
#dataWithPagination(data, pagination) {
|
|
321
340
|
const response = [];
|
package/build/src/browser.js
CHANGED
|
@@ -116,9 +116,10 @@ export async function launch(options) {
|
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
const args = [
|
|
119
|
-
...(options.
|
|
119
|
+
...(options.chromeArgs ?? []),
|
|
120
120
|
'--hide-crash-restore-bubble',
|
|
121
121
|
];
|
|
122
|
+
const ignoreDefaultArgs = options.ignoreDefaultChromeArgs ?? false;
|
|
122
123
|
if (headless) {
|
|
123
124
|
args.push('--screen-info={3840x2160}');
|
|
124
125
|
}
|
|
@@ -142,6 +143,7 @@ export async function launch(options) {
|
|
|
142
143
|
pipe: true,
|
|
143
144
|
headless,
|
|
144
145
|
args,
|
|
146
|
+
ignoreDefaultArgs: ignoreDefaultArgs,
|
|
145
147
|
acceptInsecureCerts: options.acceptInsecureCerts,
|
|
146
148
|
handleDevToolsAsPage: true,
|
|
147
149
|
});
|
|
@@ -153,7 +155,6 @@ export async function launch(options) {
|
|
|
153
155
|
}
|
|
154
156
|
if (options.viewport) {
|
|
155
157
|
const [page] = await browser.pages();
|
|
156
|
-
// @ts-expect-error internal API for now.
|
|
157
158
|
await page?.resize({
|
|
158
159
|
contentWidth: options.viewport.width,
|
|
159
160
|
contentHeight: options.viewport.height,
|
package/build/src/cli.js
CHANGED
|
@@ -139,15 +139,34 @@ export const cliOptions = {
|
|
|
139
139
|
describe: 'Whether to enable automation over DevTools targets',
|
|
140
140
|
hidden: true,
|
|
141
141
|
},
|
|
142
|
+
experimentalVision: {
|
|
143
|
+
type: 'boolean',
|
|
144
|
+
describe: 'Whether to enable vision tools',
|
|
145
|
+
hidden: true,
|
|
146
|
+
},
|
|
147
|
+
experimentalStructuredContent: {
|
|
148
|
+
type: 'boolean',
|
|
149
|
+
describe: 'Whether to output structured formatted content.',
|
|
150
|
+
hidden: true,
|
|
151
|
+
},
|
|
142
152
|
experimentalIncludeAllPages: {
|
|
143
153
|
type: 'boolean',
|
|
144
154
|
describe: 'Whether to include all kinds of pages such as webviews or background pages as pages.',
|
|
145
155
|
hidden: true,
|
|
146
156
|
},
|
|
157
|
+
experimentalInteropTools: {
|
|
158
|
+
type: 'boolean',
|
|
159
|
+
describe: 'Whether to enable interoperability tools',
|
|
160
|
+
hidden: true,
|
|
161
|
+
},
|
|
147
162
|
chromeArg: {
|
|
148
163
|
type: 'array',
|
|
149
164
|
describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
|
|
150
165
|
},
|
|
166
|
+
ignoreDefaultChromeArg: {
|
|
167
|
+
type: 'array',
|
|
168
|
+
describe: 'Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
|
|
169
|
+
},
|
|
151
170
|
categoryEmulation: {
|
|
152
171
|
type: 'boolean',
|
|
153
172
|
default: true,
|
|
@@ -163,6 +182,13 @@ export const cliOptions = {
|
|
|
163
182
|
default: true,
|
|
164
183
|
describe: 'Set to false to exclude tools related to network.',
|
|
165
184
|
},
|
|
185
|
+
usageStatistics: {
|
|
186
|
+
type: 'boolean',
|
|
187
|
+
// Marked as `false` until the feature is ready to be enabled by default.
|
|
188
|
+
default: false,
|
|
189
|
+
hidden: true,
|
|
190
|
+
describe: 'Set to false to opt-out of usage statistics collection.',
|
|
191
|
+
},
|
|
166
192
|
};
|
|
167
193
|
export function parseArguments(version, argv = process.argv) {
|
|
168
194
|
const yargsInstance = yargs(hideBin(argv))
|
|
@@ -206,6 +232,10 @@ export function parseArguments(version, argv = process.argv) {
|
|
|
206
232
|
`$0 --chrome-arg='--no-sandbox' --chrome-arg='--disable-setuid-sandbox'`,
|
|
207
233
|
'Launch Chrome without sandboxes. Use with caution.',
|
|
208
234
|
],
|
|
235
|
+
[
|
|
236
|
+
`$0 --ignore-default-chrome-arg='--disable-extensions'`,
|
|
237
|
+
'Disable the default arguments provided by Puppeteer. Use with caution.',
|
|
238
|
+
],
|
|
209
239
|
['$0 --no-category-emulation', 'Disable tools in the emulation category'],
|
|
210
240
|
[
|
|
211
241
|
'$0 --no-category-performance',
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export class SnapshotFormatter {
|
|
2
|
+
#snapshot;
|
|
3
|
+
constructor(snapshot) {
|
|
4
|
+
this.#snapshot = snapshot;
|
|
5
|
+
}
|
|
6
|
+
toString() {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
const root = this.#snapshot.root;
|
|
9
|
+
// Top-level content of the snapshot.
|
|
10
|
+
if (this.#snapshot.verbose &&
|
|
11
|
+
this.#snapshot.hasSelectedElement &&
|
|
12
|
+
!this.#snapshot.selectedElementUid) {
|
|
13
|
+
chunks.push(`Note: there is a selected element in the DevTools Elements panel but it is not included into the current a11y tree snapshot.
|
|
14
|
+
Get a verbose snapshot to include all elements if you are interested in the selected element.\n\n`);
|
|
15
|
+
}
|
|
16
|
+
chunks.push(this.#formatNode(root, 0));
|
|
17
|
+
return chunks.join('');
|
|
18
|
+
}
|
|
19
|
+
toJSON() {
|
|
20
|
+
return this.#nodeToJSON(this.#snapshot.root);
|
|
21
|
+
}
|
|
22
|
+
#formatNode(node, depth = 0) {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
const attributes = this.#getAttributes(node);
|
|
25
|
+
const line = ' '.repeat(depth * 2) +
|
|
26
|
+
attributes.join(' ') +
|
|
27
|
+
(node.id === this.#snapshot.selectedElementUid
|
|
28
|
+
? ' [selected in the DevTools Elements panel]'
|
|
29
|
+
: '') +
|
|
30
|
+
'\n';
|
|
31
|
+
chunks.push(line);
|
|
32
|
+
for (const child of node.children) {
|
|
33
|
+
chunks.push(this.#formatNode(child, depth + 1));
|
|
34
|
+
}
|
|
35
|
+
return chunks.join('');
|
|
36
|
+
}
|
|
37
|
+
#nodeToJSON(node) {
|
|
38
|
+
const rawAttrs = this.#getAttributesMap(node);
|
|
39
|
+
const children = node.children.map(child => this.#nodeToJSON(child));
|
|
40
|
+
const result = structuredClone(rawAttrs);
|
|
41
|
+
if (children.length > 0) {
|
|
42
|
+
result.children = children;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
#getAttributes(serializedAXNodeRoot) {
|
|
47
|
+
const attributes = [`uid=${serializedAXNodeRoot.id}`];
|
|
48
|
+
if (serializedAXNodeRoot.role) {
|
|
49
|
+
attributes.push(serializedAXNodeRoot.role === 'none'
|
|
50
|
+
? 'ignored'
|
|
51
|
+
: serializedAXNodeRoot.role);
|
|
52
|
+
}
|
|
53
|
+
if (serializedAXNodeRoot.name) {
|
|
54
|
+
attributes.push(`"${serializedAXNodeRoot.name}"`);
|
|
55
|
+
}
|
|
56
|
+
const simpleAttrs = this.#getAttributesMap(serializedAXNodeRoot,
|
|
57
|
+
/* excludeSpecial */ true);
|
|
58
|
+
for (const attr of Object.keys(serializedAXNodeRoot).sort()) {
|
|
59
|
+
if (excludedAttributes.has(attr)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const mapped = booleanPropertyMap[attr];
|
|
63
|
+
if (mapped && simpleAttrs[mapped]) {
|
|
64
|
+
attributes.push(mapped);
|
|
65
|
+
}
|
|
66
|
+
const val = simpleAttrs[attr];
|
|
67
|
+
if (val === true) {
|
|
68
|
+
attributes.push(attr);
|
|
69
|
+
}
|
|
70
|
+
else if (typeof val === 'string' || typeof val === 'number') {
|
|
71
|
+
attributes.push(`${attr}="${val}"`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return attributes;
|
|
75
|
+
}
|
|
76
|
+
#getAttributesMap(node, excludeSpecial = false) {
|
|
77
|
+
const result = {};
|
|
78
|
+
if (!excludeSpecial) {
|
|
79
|
+
result.id = node.id;
|
|
80
|
+
if (node.role) {
|
|
81
|
+
result.role = node.role;
|
|
82
|
+
}
|
|
83
|
+
if (node.name) {
|
|
84
|
+
result.name = node.name;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Re-implementing the exact logic from original function for #getAttributes to be safe:
|
|
88
|
+
return {
|
|
89
|
+
...result,
|
|
90
|
+
...this.#extractedAttributes(node),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
#extractedAttributes(node) {
|
|
94
|
+
const result = {};
|
|
95
|
+
for (const attr of Object.keys(node).sort()) {
|
|
96
|
+
if (excludedAttributes.has(attr)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const value = node[attr];
|
|
100
|
+
if (typeof value === 'boolean') {
|
|
101
|
+
if (booleanPropertyMap[attr]) {
|
|
102
|
+
result[booleanPropertyMap[attr]] = true;
|
|
103
|
+
}
|
|
104
|
+
if (value) {
|
|
105
|
+
result[attr] = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (typeof value === 'string' || typeof value === 'number') {
|
|
109
|
+
result[attr] = value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const booleanPropertyMap = {
|
|
116
|
+
disabled: 'disableable',
|
|
117
|
+
expanded: 'expandable',
|
|
118
|
+
focused: 'focusable',
|
|
119
|
+
selected: 'selectable',
|
|
120
|
+
};
|
|
121
|
+
const excludedAttributes = new Set([
|
|
122
|
+
'id',
|
|
123
|
+
'role',
|
|
124
|
+
'name',
|
|
125
|
+
'elementHandle',
|
|
126
|
+
'children',
|
|
127
|
+
'backendNodeId',
|
|
128
|
+
]);
|
|
@@ -25,6 +25,7 @@ export function formatConsoleEventVerbose(msg, context) {
|
|
|
25
25
|
`ID: ${msg.consoleMessageStableId}`,
|
|
26
26
|
`Message: ${msg.type}> ${aggregatedIssue ? formatIssue(aggregatedIssue, msg.description, context) : msg.message}`,
|
|
27
27
|
aggregatedIssue ? undefined : formatArgs(msg),
|
|
28
|
+
formatStackTrace(msg.stackTrace),
|
|
28
29
|
].filter(line => !!line);
|
|
29
30
|
return result.join('\n');
|
|
30
31
|
}
|
|
@@ -49,8 +50,9 @@ export function formatIssue(issue, description, context) {
|
|
|
49
50
|
if (processedMarkdown?.startsWith('# ')) {
|
|
50
51
|
processedMarkdown = processedMarkdown.substring(2).trimStart();
|
|
51
52
|
}
|
|
52
|
-
if (processedMarkdown)
|
|
53
|
+
if (processedMarkdown) {
|
|
53
54
|
result.push(processedMarkdown);
|
|
55
|
+
}
|
|
54
56
|
const links = issue.getDescription()?.links;
|
|
55
57
|
if (links && links.length > 0) {
|
|
56
58
|
result.push('Learn more:');
|
|
@@ -62,8 +64,9 @@ export function formatIssue(issue, description, context) {
|
|
|
62
64
|
const affectedResources = [];
|
|
63
65
|
for (const singleIssue of issues) {
|
|
64
66
|
const details = singleIssue.details();
|
|
65
|
-
if (!details)
|
|
67
|
+
if (!details) {
|
|
66
68
|
continue;
|
|
69
|
+
}
|
|
67
70
|
// We send the remaining details as untyped JSON because the DevTools
|
|
68
71
|
// frontend code is currently not re-usable.
|
|
69
72
|
// eslint-disable-next-line
|
|
@@ -106,16 +109,48 @@ export function formatIssue(issue, description, context) {
|
|
|
106
109
|
}
|
|
107
110
|
result.push(...affectedResources.map(item => {
|
|
108
111
|
const details = [];
|
|
109
|
-
if (item.uid)
|
|
112
|
+
if (item.uid) {
|
|
110
113
|
details.push(`uid=${item.uid}`);
|
|
114
|
+
}
|
|
111
115
|
if (item.request) {
|
|
112
116
|
details.push((typeof item.request === 'number' ? `reqid=` : 'url=') + item.request);
|
|
113
117
|
}
|
|
114
|
-
if (item.data)
|
|
118
|
+
if (item.data) {
|
|
115
119
|
details.push(`data=${JSON.stringify(item.data)}`);
|
|
120
|
+
}
|
|
116
121
|
return details.join(' ');
|
|
117
122
|
}));
|
|
118
|
-
if (result.length === 0)
|
|
123
|
+
if (result.length === 0) {
|
|
119
124
|
return 'No affected resources found';
|
|
125
|
+
}
|
|
120
126
|
return result.join('\n');
|
|
121
127
|
}
|
|
128
|
+
function formatStackTrace(stackTrace) {
|
|
129
|
+
if (!stackTrace) {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
return [
|
|
133
|
+
'### Stack trace',
|
|
134
|
+
formatFragment(stackTrace.syncFragment),
|
|
135
|
+
...stackTrace.asyncFragments.map(formatAsyncFragment),
|
|
136
|
+
].join('\n');
|
|
137
|
+
}
|
|
138
|
+
function formatFragment(fragment) {
|
|
139
|
+
return fragment.frames.map(formatFrame).join('\n');
|
|
140
|
+
}
|
|
141
|
+
function formatAsyncFragment(fragment) {
|
|
142
|
+
const separatorLineLength = 40;
|
|
143
|
+
const prefix = `--- ${fragment.description || 'async'} `;
|
|
144
|
+
const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
|
|
145
|
+
return separator + '\n' + formatFragment(fragment);
|
|
146
|
+
}
|
|
147
|
+
function formatFrame(frame) {
|
|
148
|
+
let result = `at ${frame.name ?? '<anonymous>'}`;
|
|
149
|
+
if (frame.uiSourceCode) {
|
|
150
|
+
result += ` (${frame.uiSourceCode.displayName()}:${frame.line}:${frame.column})`;
|
|
151
|
+
}
|
|
152
|
+
else if (frame.url) {
|
|
153
|
+
result += ` (${frame.url}:${frame.line}:${frame.column})`;
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|