chrome-devtools-mcp 0.20.3 → 0.22.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.
Files changed (55) hide show
  1. package/README.md +97 -20
  2. package/build/src/HeapSnapshotManager.js +94 -0
  3. package/build/src/McpContext.js +26 -49
  4. package/build/src/McpPage.js +16 -0
  5. package/build/src/McpResponse.js +220 -12
  6. package/build/src/PageCollector.js +14 -28
  7. package/build/src/WaitForHelper.js +31 -0
  8. package/build/src/bin/check-latest-version.js +25 -0
  9. package/build/src/bin/chrome-devtools-mcp-cli-options.js +28 -9
  10. package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
  11. package/build/src/bin/chrome-devtools-mcp.js +1 -0
  12. package/build/src/bin/chrome-devtools.js +9 -3
  13. package/build/src/bin/cliDefinitions.js +15 -9
  14. package/build/src/daemon/client.js +1 -1
  15. package/build/src/daemon/daemon.js +2 -6
  16. package/build/src/daemon/utils.js +1 -0
  17. package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
  18. package/build/src/formatters/NetworkFormatter.js +24 -7
  19. package/build/src/index.js +22 -1
  20. package/build/src/telemetry/ClearcutLogger.js +145 -6
  21. package/build/src/telemetry/flagUtils.js +46 -4
  22. package/build/src/telemetry/toolMetricsUtils.js +88 -0
  23. package/build/src/telemetry/types.js +5 -0
  24. package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
  25. package/build/src/third_party/THIRD_PARTY_NOTICES +1400 -483
  26. package/build/src/third_party/bundled-packages.json +6 -5
  27. package/build/src/third_party/devtools-formatter-worker.js +61 -66
  28. package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
  29. package/build/src/third_party/index.js +61622 -52803
  30. package/build/src/third_party/issue-descriptions/sharedDictionaryUseErrorCrossOriginNoCorsRequest.md +1 -0
  31. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +10589 -4647
  32. package/build/src/tools/categories.js +5 -0
  33. package/build/src/tools/console.js +42 -39
  34. package/build/src/tools/emulation.js +1 -1
  35. package/build/src/tools/extensions.js +5 -11
  36. package/build/src/tools/inPage.js +105 -0
  37. package/build/src/tools/input.js +18 -16
  38. package/build/src/tools/lighthouse.js +3 -3
  39. package/build/src/tools/memory.js +50 -5
  40. package/build/src/tools/network.js +2 -2
  41. package/build/src/tools/pages.js +14 -6
  42. package/build/src/tools/performance.js +1 -1
  43. package/build/src/tools/screencast.js +2 -1
  44. package/build/src/tools/screenshot.js +3 -3
  45. package/build/src/tools/script.js +22 -16
  46. package/build/src/tools/tools.js +4 -0
  47. package/build/src/tools/webmcp.js +63 -0
  48. package/build/src/utils/check-for-updates.js +73 -0
  49. package/build/src/utils/files.js +4 -0
  50. package/build/src/utils/id.js +15 -0
  51. package/build/src/version.js +1 -1
  52. package/package.json +13 -9
  53. package/build/src/third_party/issue-descriptions/sharedDictionaryUseErrorNoCorpCrossOriginNoCorsRequest.md +0 -3
  54. package/build/src/third_party/issue-descriptions/sharedDictionaryWriteErrorNoCorpCossOriginNoCorsRequest.md +0 -3
  55. package/build/src/utils/ExtensionRegistry.js +0 -35
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { ConsoleFormatter } from './formatters/ConsoleFormatter.js';
7
+ import { HeapSnapshotFormatter } from './formatters/HeapSnapshotFormatter.js';
7
8
  import { IssueFormatter } from './formatters/IssueFormatter.js';
8
9
  import { NetworkFormatter } from './formatters/NetworkFormatter.js';
9
10
  import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
@@ -12,6 +13,99 @@ import { DevTools } from './third_party/index.js';
12
13
  import { handleDialog } from './tools/pages.js';
13
14
  import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
14
15
  import { paginate } from './utils/pagination.js';
16
+ export function replaceHtmlElementsWithUids(schema) {
17
+ if (typeof schema === 'boolean') {
18
+ return;
19
+ }
20
+ let isHtmlElement = false;
21
+ for (const [key, value] of Object.entries(schema)) {
22
+ if (key === 'x-mcp-type' && value === 'HTMLElement') {
23
+ isHtmlElement = true;
24
+ break;
25
+ }
26
+ }
27
+ if (isHtmlElement) {
28
+ schema.properties = { uid: { type: 'string' } };
29
+ schema.required = ['uid'];
30
+ }
31
+ if (schema.properties) {
32
+ for (const key of Object.keys(schema.properties)) {
33
+ replaceHtmlElementsWithUids(schema.properties[key]);
34
+ }
35
+ }
36
+ if (schema.items) {
37
+ if (Array.isArray(schema.items)) {
38
+ for (const item of schema.items) {
39
+ replaceHtmlElementsWithUids(item);
40
+ }
41
+ }
42
+ else {
43
+ replaceHtmlElementsWithUids(schema.items);
44
+ }
45
+ }
46
+ if (schema.anyOf) {
47
+ for (const s of schema.anyOf) {
48
+ replaceHtmlElementsWithUids(s);
49
+ }
50
+ }
51
+ if (schema.allOf) {
52
+ for (const s of schema.allOf) {
53
+ replaceHtmlElementsWithUids(s);
54
+ }
55
+ }
56
+ if (schema.oneOf) {
57
+ for (const s of schema.oneOf) {
58
+ replaceHtmlElementsWithUids(s);
59
+ }
60
+ }
61
+ }
62
+ async function getToolGroup(page) {
63
+ // Check if there is a `devtoolstooldiscovery` event listener
64
+ const windowHandle = await page.pptrPage.evaluateHandle(() => window);
65
+ // @ts-expect-error internal API
66
+ const client = page.pptrPage._client();
67
+ const { listeners } = await client.send('DOMDebugger.getEventListeners', {
68
+ objectId: windowHandle.remoteObject().objectId,
69
+ });
70
+ if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
71
+ return;
72
+ }
73
+ const toolGroup = await page.pptrPage.evaluate(() => {
74
+ return new Promise(resolve => {
75
+ const event = new CustomEvent('devtoolstooldiscovery');
76
+ // @ts-expect-error Adding custom property
77
+ event.respondWith = (toolGroup) => {
78
+ if (!window.__dtmcp) {
79
+ window.__dtmcp = {};
80
+ }
81
+ window.__dtmcp.toolGroup = toolGroup;
82
+ // When receiving a toolGroup for the first time, expose a simple execution helper
83
+ if (!window.__dtmcp.executeTool) {
84
+ window.__dtmcp.executeTool = async (toolName, args) => {
85
+ if (!window.__dtmcp?.toolGroup) {
86
+ throw new Error('No tools found on the page');
87
+ }
88
+ const tool = window.__dtmcp.toolGroup.tools.find(t => t.name === toolName);
89
+ if (!tool) {
90
+ throw new Error(`Tool ${toolName} not found`);
91
+ }
92
+ return await tool.execute(args);
93
+ };
94
+ }
95
+ resolve(toolGroup);
96
+ };
97
+ window.dispatchEvent(event);
98
+ // If the page does not synchronously call `event.respondWith`, return instead of timing out
99
+ setTimeout(() => {
100
+ resolve(undefined);
101
+ }, 0);
102
+ });
103
+ });
104
+ for (const tool of toolGroup?.tools ?? []) {
105
+ replaceHtmlElementsWithUids(tool.inputSchema);
106
+ }
107
+ return toolGroup;
108
+ }
15
109
  export class McpResponse {
16
110
  #includePages = false;
17
111
  #includeExtensionServiceWorkers = false;
@@ -25,19 +119,26 @@ export class McpResponse {
25
119
  #attachedLighthouseResult;
26
120
  #textResponseLines = [];
27
121
  #images = [];
122
+ #heapSnapshotOptions;
28
123
  #networkRequestsOptions;
29
124
  #consoleDataOptions;
30
125
  #listExtensions;
126
+ #listInPageTools;
127
+ #listWebMcpTools;
31
128
  #devToolsData;
32
129
  #tabId;
33
130
  #args;
34
131
  #page;
132
+ #redactNetworkHeaders = true;
35
133
  constructor(args) {
36
134
  this.#args = args;
37
135
  }
38
136
  setPage(page) {
39
137
  this.#page = page;
40
138
  }
139
+ setRedactNetworkHeaders(value) {
140
+ this.#redactNetworkHeaders = value;
141
+ }
41
142
  attachDevToolsData(data) {
42
143
  this.#devToolsData = data;
43
144
  }
@@ -59,6 +160,14 @@ export class McpResponse {
59
160
  setListExtensions() {
60
161
  this.#listExtensions = true;
61
162
  }
163
+ setListInPageTools() {
164
+ if (this.#args.categoryInPageTools) {
165
+ this.#listInPageTools = true;
166
+ }
167
+ }
168
+ setListWebMcpTools() {
169
+ this.#listWebMcpTools = true;
170
+ }
62
171
  setIncludeNetworkRequests(value, options) {
63
172
  if (!value) {
64
173
  this.#networkRequestsOptions = undefined;
@@ -94,8 +203,8 @@ export class McpResponse {
94
203
  includePreservedMessages: options?.includePreservedMessages,
95
204
  };
96
205
  }
97
- attachNetworkRequest(reqid, options) {
98
- this.#attachedNetworkRequestId = reqid;
206
+ attachNetworkRequest(reqId, options) {
207
+ this.#attachedNetworkRequestId = reqId;
99
208
  this.#attachedNetworkRequestOptions = options;
100
209
  }
101
210
  attachConsoleMessage(msgid) {
@@ -147,6 +256,22 @@ export class McpResponse {
147
256
  appendResponseLine(value) {
148
257
  this.#textResponseLines.push(value);
149
258
  }
259
+ setHeapSnapshotAggregates(aggregates, options) {
260
+ this.#heapSnapshotOptions = {
261
+ ...this.#heapSnapshotOptions,
262
+ include: true,
263
+ aggregates,
264
+ pagination: options,
265
+ };
266
+ }
267
+ setHeapSnapshotStats(stats, staticData) {
268
+ this.#heapSnapshotOptions = {
269
+ ...this.#heapSnapshotOptions,
270
+ include: true,
271
+ stats,
272
+ staticData,
273
+ };
274
+ }
150
275
  attachImage(value) {
151
276
  this.#images.push(value);
152
277
  }
@@ -159,6 +284,9 @@ export class McpResponse {
159
284
  get snapshotParams() {
160
285
  return this.#snapshotParams;
161
286
  }
287
+ get listWebMcpTools() {
288
+ return this.#listWebMcpTools;
289
+ }
162
290
  async handle(toolName, context) {
163
291
  if (this.#includePages) {
164
292
  await context.createPagesSnapshot();
@@ -176,8 +304,8 @@ export class McpResponse {
176
304
  if (textSnapshot) {
177
305
  const formatter = new SnapshotFormatter(textSnapshot);
178
306
  if (this.#snapshotParams.filePath) {
179
- await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath);
180
- snapshot = this.#snapshotParams.filePath;
307
+ const result = await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath, '.txt');
308
+ snapshot = result.filename;
181
309
  }
182
310
  else {
183
311
  snapshot = formatter;
@@ -196,7 +324,8 @@ export class McpResponse {
196
324
  fetchData: true,
197
325
  requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
198
326
  responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
199
- saveFile: (data, filename) => context.saveFile(data, filename),
327
+ saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
328
+ redactNetworkHeaders: this.#redactNetworkHeaders,
200
329
  });
201
330
  detailedNetworkRequest = formatter;
202
331
  }
@@ -223,14 +352,25 @@ export class McpResponse {
223
352
  elementIdResolver: context.resolveCdpElementId.bind(context, this.#page),
224
353
  });
225
354
  if (!formatter.isValid()) {
226
- throw new Error("Can't provide detals for the msgid " + consoleMessageStableId);
355
+ throw new Error("Can't provide details for the msgid " + consoleMessageStableId);
227
356
  }
228
357
  detailedConsoleMessage = formatter;
229
358
  }
230
359
  }
231
360
  let extensions;
232
361
  if (this.#listExtensions) {
233
- extensions = context.listExtensions();
362
+ extensions = await context.listExtensions();
363
+ }
364
+ let inPageTools;
365
+ if (this.#listInPageTools) {
366
+ const page = this.#page ?? context.getSelectedMcpPage();
367
+ inPageTools = await getToolGroup(page);
368
+ page.inPageTools = inPageTools;
369
+ }
370
+ let webmcpTools;
371
+ if (this.#listWebMcpTools && this.#args.experimentalWebmcp) {
372
+ const page = this.#page ?? context.getSelectedMcpPage();
373
+ webmcpTools = page.getWebMcpTools();
234
374
  }
235
375
  let consoleMessages;
236
376
  if (this.#consoleDataOptions?.include) {
@@ -294,7 +434,8 @@ export class McpResponse {
294
434
  selectedInDevToolsUI: context.getNetworkRequestStableId(request) ===
295
435
  this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
296
436
  fetchData: false,
297
- saveFile: (data, filename) => context.saveFile(data, filename),
437
+ saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
438
+ redactNetworkHeaders: this.#redactNetworkHeaders,
298
439
  })));
299
440
  }
300
441
  }
@@ -308,6 +449,8 @@ export class McpResponse {
308
449
  traceSummary: this.#attachedTraceSummary,
309
450
  extensions,
310
451
  lighthouseResult: this.#attachedLighthouseResult,
452
+ inPageTools,
453
+ webmcpTools,
311
454
  });
312
455
  }
313
456
  format(toolName, context, data) {
@@ -473,6 +616,32 @@ Call ${handleDialog.name} to handle it before continuing.`);
473
616
  structuredContent.snapshot = data.snapshot.toJSON();
474
617
  }
475
618
  }
619
+ if (this.#heapSnapshotOptions?.include) {
620
+ response.push('## Heap Snapshot Data');
621
+ const stats = this.#heapSnapshotOptions.stats;
622
+ const staticData = this.#heapSnapshotOptions.staticData;
623
+ if (stats) {
624
+ response.push(`Statistics: ${JSON.stringify(stats, null, 2)}`);
625
+ structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
626
+ structuredContent.heapSnapshot.stats = stats;
627
+ }
628
+ if (staticData) {
629
+ response.push(`Static Data: ${JSON.stringify(staticData, null, 2)}`);
630
+ structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
631
+ structuredContent.heapSnapshot.staticData = staticData;
632
+ }
633
+ const aggregates = this.#heapSnapshotOptions.aggregates;
634
+ if (aggregates) {
635
+ const sortedEntries = HeapSnapshotFormatter.sort(aggregates);
636
+ const paginationData = this.#dataWithPagination(sortedEntries, this.#heapSnapshotOptions.pagination);
637
+ structuredContent.pagination = paginationData.pagination;
638
+ response.push(...paginationData.info);
639
+ const paginatedRecord = Object.fromEntries(paginationData.items);
640
+ const formatter = new HeapSnapshotFormatter(paginatedRecord);
641
+ response.push(formatter.toString());
642
+ structuredContent.heapSnapshotData = formatter.toJSON();
643
+ }
644
+ }
476
645
  if (data.detailedNetworkRequest) {
477
646
  response.push(data.detailedNetworkRequest.toStringDetailed());
478
647
  structuredContent.networkRequest =
@@ -484,20 +653,59 @@ Call ${handleDialog.name} to handle it before continuing.`);
484
653
  data.detailedConsoleMessage.toJSONDetailed();
485
654
  }
486
655
  if (data.extensions) {
487
- structuredContent.extensions = data.extensions;
656
+ const extensionArray = Array.from(data.extensions.values());
657
+ structuredContent.extensions = extensionArray;
488
658
  response.push('## Extensions');
489
- if (data.extensions.length === 0) {
659
+ if (extensionArray.length === 0) {
490
660
  response.push('No extensions installed.');
491
661
  }
492
662
  else {
493
- const extensionsMessage = data.extensions
663
+ const extensionsMessage = extensionArray
494
664
  .map(extension => {
495
- return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.isEnabled ? 'Enabled' : 'Disabled'}`;
665
+ return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.enabled ? 'Enabled' : 'Disabled'}`;
496
666
  })
497
667
  .join('\n');
498
668
  response.push(extensionsMessage);
499
669
  }
500
670
  }
671
+ if (this.#listInPageTools) {
672
+ structuredContent.inPageTools = data.inPageTools ?? undefined;
673
+ response.push('## In-page tools');
674
+ if (!data.inPageTools || !data.inPageTools.tools) {
675
+ response.push('No in-page tools available.');
676
+ }
677
+ else {
678
+ const toolGroup = data.inPageTools;
679
+ response.push(`${toolGroup.name}: ${toolGroup.description}`);
680
+ response.push('Available tools:');
681
+ const toolDefinitionsMessage = toolGroup.tools
682
+ .map(tool => {
683
+ return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(tool.inputSchema)}`;
684
+ })
685
+ .join('\n');
686
+ response.push(toolDefinitionsMessage);
687
+ }
688
+ }
689
+ if (this.#listWebMcpTools && data.webmcpTools) {
690
+ structuredContent.webmcpTools = data.webmcpTools.map(({ name, description, inputSchema, annotations }) => ({
691
+ name,
692
+ description,
693
+ inputSchema,
694
+ annotations,
695
+ }));
696
+ response.push('## WebMCP tools');
697
+ if (data.webmcpTools.length === 0) {
698
+ response.push('No WebMCP tools available.');
699
+ }
700
+ else {
701
+ const webmcpToolsMessage = data.webmcpTools
702
+ .map(tool => {
703
+ return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(tool.inputSchema)}, annotations=${JSON.stringify(tool.annotations)}`;
704
+ })
705
+ .join('\n');
706
+ response.push(webmcpToolsMessage);
707
+ }
708
+ }
501
709
  if (this.#networkRequestsOptions?.include && data.networkRequests) {
502
710
  const requests = data.networkRequests;
503
711
  response.push('## Network requests');
@@ -6,6 +6,7 @@
6
6
  import { FakeIssuesManager } from './DevtoolsUtils.js';
7
7
  import { logger } from './logger.js';
8
8
  import { DevTools } from './third_party/index.js';
9
+ import { createIdGenerator, stableIdSymbol, } from './utils/id.js';
9
10
  export class UncaughtError {
10
11
  details;
11
12
  targetId;
@@ -14,16 +15,6 @@ export class UncaughtError {
14
15
  this.targetId = targetId;
15
16
  }
16
17
  }
17
- function createIdGenerator() {
18
- let i = 1;
19
- return () => {
20
- if (i === Number.MAX_SAFE_INTEGER) {
21
- i = 0;
22
- }
23
- return i++;
24
- };
25
- }
26
- export const stableIdSymbol = Symbol('stableIdSymbol');
27
18
  export class PageCollector {
28
19
  #browser;
29
20
  #listenersInitializer;
@@ -198,42 +189,33 @@ class PageEventSubscriber {
198
189
  #resetIssueAggregator() {
199
190
  this.#issueManager = new FakeIssuesManager();
200
191
  if (this.#issueAggregator) {
201
- this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedissue);
192
+ this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedIssue);
202
193
  }
203
194
  this.#issueAggregator = new DevTools.IssueAggregator(this.#issueManager);
204
- this.#issueAggregator.addEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedissue);
195
+ this.#issueAggregator.addEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedIssue);
205
196
  }
206
197
  async subscribe() {
207
198
  this.#resetIssueAggregator();
208
199
  this.#page.on('framenavigated', this.#onFrameNavigated);
209
- this.#session.on('Audits.issueAdded', this.#onIssueAdded);
200
+ this.#page.on('issue', this.#onIssueAdded);
210
201
  this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
211
- try {
212
- await this.#session.send('Audits.enable');
213
- }
214
- catch (error) {
215
- logger('Error subscribing to issues', error);
216
- }
217
202
  }
218
203
  unsubscribe() {
219
204
  this.#seenKeys.clear();
220
205
  this.#seenIssues.clear();
221
206
  this.#page.off('framenavigated', this.#onFrameNavigated);
222
- this.#session.off('Audits.issueAdded', this.#onIssueAdded);
207
+ this.#page.off('issue', this.#onIssueAdded);
223
208
  this.#session.off('Runtime.exceptionThrown', this.#onExceptionThrown);
224
209
  if (this.#issueAggregator) {
225
- this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedissue);
210
+ this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedIssue);
226
211
  }
227
- void this.#session.send('Audits.disable').catch(() => {
228
- // might fail.
229
- });
230
212
  }
231
- #onAggregatedissue = (event) => {
213
+ #onAggregatedIssue = (event) => {
232
214
  if (this.#seenIssues.has(event.data)) {
233
215
  return;
234
216
  }
235
217
  this.#seenIssues.add(event.data);
236
- this.#page.emit('issue', event.data);
218
+ this.#page.emit('devtoolsAggregatedIssue', event.data);
237
219
  };
238
220
  #onExceptionThrown = (event) => {
239
221
  this.#page.emit('uncaughtError', new UncaughtError(event.exceptionDetails, this.#targetId));
@@ -248,9 +230,13 @@ class PageEventSubscriber {
248
230
  this.#seenIssues.clear();
249
231
  this.#resetIssueAggregator();
250
232
  };
251
- #onIssueAdded = (data) => {
233
+ #onIssueAdded = (inspectorIssue) => {
252
234
  try {
253
- const inspectorIssue = data.issue;
235
+ // DevTools currently defines this protocol issue code but has no
236
+ // IssuesManager handler for it, so calling into the mapper only warns.
237
+ if (String(inspectorIssue.code) === 'PerformanceIssue') {
238
+ return;
239
+ }
254
240
  const issue = DevTools.createIssuesFromProtocolIssue(null,
255
241
  // @ts-expect-error Protocol types diverge.
256
242
  inspectorIssue)[0];
@@ -104,6 +104,23 @@ export class WaitForHelper {
104
104
  });
105
105
  }
106
106
  async waitForEventsAfterAction(action, options) {
107
+ if (options?.handleDialog) {
108
+ const dialogHandler = (dialog) => {
109
+ if (options.handleDialog === 'dismiss') {
110
+ void dialog.dismiss();
111
+ }
112
+ else if (options.handleDialog === 'accept') {
113
+ void dialog.accept();
114
+ }
115
+ else {
116
+ void dialog.accept(options.handleDialog);
117
+ }
118
+ };
119
+ this.#page.on('dialog', dialogHandler);
120
+ this.#abortController.signal.addEventListener('abort', () => {
121
+ this.#page.off('dialog', dialogHandler);
122
+ });
123
+ }
107
124
  const navigationFinished = this.waitForNavigationStarted()
108
125
  .then(navigationStated => {
109
126
  if (navigationStated) {
@@ -137,3 +154,17 @@ export class WaitForHelper {
137
154
  }
138
155
  }
139
156
  }
157
+ export function getNetworkMultiplierFromString(condition) {
158
+ const puppeteerCondition = condition;
159
+ switch (puppeteerCondition) {
160
+ case 'Fast 4G':
161
+ return 1;
162
+ case 'Slow 4G':
163
+ return 2.5;
164
+ case 'Fast 3G':
165
+ return 5;
166
+ case 'Slow 3G':
167
+ return 10;
168
+ }
169
+ return 1;
170
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import process from 'node:process';
9
+ const cachePath = process.argv[2];
10
+ if (cachePath) {
11
+ try {
12
+ const response = await fetch('https://registry.npmjs.org/chrome-devtools-mcp/latest');
13
+ const data = response.ok ? await response.json() : null;
14
+ if (data &&
15
+ typeof data === 'object' &&
16
+ 'version' in data &&
17
+ typeof data.version === 'string') {
18
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
19
+ await fs.writeFile(cachePath, JSON.stringify({ version: data.version }));
20
+ }
21
+ }
22
+ catch {
23
+ // Ignore errors.
24
+ }
25
+ }
@@ -7,8 +7,8 @@ import { yargs, hideBin } from '../third_party/index.js';
7
7
  export const cliOptions = {
8
8
  autoConnect: {
9
9
  type: 'boolean',
10
- description: 'If specified, automatically connects to a browser (Chrome 144+) running locally from the user data directory identified by the channel param (default channel is stable). Requires the remoted debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.',
11
- conflicts: ['isolated', 'executablePath', 'categoryExtensions'],
10
+ description: 'If specified, automatically connects to a browser (Chrome 144+) running locally from the user data directory identified by the channel param (default channel is stable). Requires the remote debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.',
11
+ conflicts: ['isolated', 'executablePath'],
12
12
  default: false,
13
13
  coerce: (value) => {
14
14
  if (!value) {
@@ -21,7 +21,7 @@ export const cliOptions = {
21
21
  type: 'string',
22
22
  description: '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.',
23
23
  alias: 'u',
24
- conflicts: ['wsEndpoint', 'categoryExtensions'],
24
+ conflicts: ['wsEndpoint'],
25
25
  coerce: (url) => {
26
26
  if (!url) {
27
27
  return;
@@ -39,7 +39,7 @@ export const cliOptions = {
39
39
  type: 'string',
40
40
  description: 'WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.',
41
41
  alias: 'w',
42
- conflicts: ['browserUrl', 'categoryExtensions'],
42
+ conflicts: ['browserUrl'],
43
43
  coerce: (url) => {
44
44
  if (!url) {
45
45
  return;
@@ -102,7 +102,7 @@ export const cliOptions = {
102
102
  channel: {
103
103
  type: 'string',
104
104
  description: 'Specify a different Chrome channel that should be used. The default is the stable channel version.',
105
- choices: ['stable', 'canary', 'beta', 'dev'],
105
+ choices: ['canary', 'dev', 'beta', 'stable'],
106
106
  conflicts: ['browserUrl', 'wsEndpoint', 'executablePath'],
107
107
  },
108
108
  logFile: {
@@ -146,7 +146,12 @@ export const cliOptions = {
146
146
  },
147
147
  experimentalVision: {
148
148
  type: 'boolean',
149
- describe: 'Whether to enable vision tools',
149
+ describe: 'Whether to enable coordinate-based tools such as click_at(x,y). Usually requires a computer-use model able to produce accurate coordinates by looking at screenshots.',
150
+ hidden: false,
151
+ },
152
+ experimentalMemory: {
153
+ type: 'boolean',
154
+ describe: 'Whether to enable experimental memory tools.',
150
155
  hidden: true,
151
156
  },
152
157
  experimentalStructuredContent: {
@@ -168,6 +173,10 @@ export const cliOptions = {
168
173
  type: 'boolean',
169
174
  describe: 'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
170
175
  },
176
+ experimentalWebmcp: {
177
+ type: 'boolean',
178
+ describe: 'Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`',
179
+ },
171
180
  chromeArg: {
172
181
  type: 'array',
173
182
  describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
@@ -192,10 +201,15 @@ export const cliOptions = {
192
201
  describe: 'Set to false to exclude tools related to network.',
193
202
  },
194
203
  categoryExtensions: {
204
+ type: 'boolean',
205
+ hidden: false,
206
+ default: false,
207
+ describe: 'Set to true to include tools related to extensions. Note: This feature is currently only supported with a pipe connection. autoConnect, browserUrl, and wsEndpoint are not supported with this feature until 149 will be released.',
208
+ },
209
+ categoryInPageTools: {
195
210
  type: 'boolean',
196
211
  hidden: true,
197
- conflicts: ['browserUrl', 'autoConnect', 'wsEndpoint'],
198
- describe: 'Set to true to include tools related to extensions. Note: This feature is only supported with a pipe connection. autoConnect is not supported.',
212
+ describe: 'Set to true to enable tools exposed by the inspected page itself',
199
213
  },
200
214
  performanceCrux: {
201
215
  type: 'boolean',
@@ -205,7 +219,7 @@ export const cliOptions = {
205
219
  usageStatistics: {
206
220
  type: 'boolean',
207
221
  default: true,
208
- describe: 'Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.',
222
+ describe: 'Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if `CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS` or `CI` env variables are set.',
209
223
  },
210
224
  clearcutEndpoint: {
211
225
  type: 'string',
@@ -231,6 +245,11 @@ export const cliOptions = {
231
245
  describe: 'Set by Chrome DevTools CLI if the MCP server is started via the CLI client (this arg exists for usage stats)',
232
246
  hidden: true,
233
247
  },
248
+ redactNetworkHeaders: {
249
+ type: 'boolean',
250
+ describe: 'If true, redacts some of the network headers considered senstive before returning to the client.',
251
+ default: false,
252
+ },
234
253
  };
235
254
  export function parseArguments(version, argv = process.argv) {
236
255
  const yargsInstance = yargs(hideBin(argv))
@@ -9,8 +9,10 @@ import { createMcpServer, logDisclaimers } from '../index.js';
9
9
  import { logger, saveLogsToFile } from '../logger.js';
10
10
  import { computeFlagUsage } from '../telemetry/flagUtils.js';
11
11
  import { StdioServerTransport } from '../third_party/index.js';
12
+ import { checkForUpdates } from '../utils/check-for-updates.js';
12
13
  import { VERSION } from '../version.js';
13
14
  import { cliOptions, parseArguments } from './chrome-devtools-mcp-cli-options.js';
15
+ await checkForUpdates('Run `npm install chrome-devtools-mcp@latest` to update.');
14
16
  export const args = parseArguments(VERSION);
15
17
  const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
16
18
  if (process.env['CI'] ||
@@ -4,6 +4,7 @@
4
4
  * Copyright 2025 Google LLC
5
5
  * SPDX-License-Identifier: Apache-2.0
6
6
  */
7
+ process.title = 'chrome-devtools-mcp';
7
8
  import { version } from 'node:process';
8
9
  const [major, minor] = version.substring(1).split('.').map(Number);
9
10
  if (major === 20 && minor < 19) {