chrome-devtools-mcp 0.21.0 → 0.23.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 (51) hide show
  1. package/README.md +87 -21
  2. package/build/src/HeapSnapshotManager.js +94 -0
  3. package/build/src/McpContext.js +26 -181
  4. package/build/src/McpPage.js +214 -0
  5. package/build/src/McpResponse.js +151 -13
  6. package/build/src/PageCollector.js +10 -24
  7. package/build/src/TextSnapshot.js +230 -0
  8. package/build/src/WaitForHelper.js +31 -0
  9. package/build/src/bin/check-latest-version.js +25 -0
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +34 -10
  11. package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
  12. package/build/src/bin/chrome-devtools.js +25 -14
  13. package/build/src/bin/cliDefinitions.js +14 -8
  14. package/build/src/daemon/client.js +11 -11
  15. package/build/src/daemon/daemon.js +6 -9
  16. package/build/src/daemon/utils.js +19 -14
  17. package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
  18. package/build/src/formatters/NetworkFormatter.js +24 -7
  19. package/build/src/index.js +12 -1
  20. package/build/src/telemetry/ClearcutLogger.js +34 -12
  21. package/build/src/telemetry/flagUtils.js +46 -4
  22. package/build/src/telemetry/toolMetricsUtils.js +88 -0
  23. package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
  24. package/build/src/third_party/THIRD_PARTY_NOTICES +59 -32
  25. package/build/src/third_party/bundled-packages.json +6 -4
  26. package/build/src/third_party/devtools-formatter-worker.js +61 -64
  27. package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
  28. package/build/src/third_party/index.js +62661 -60590
  29. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +3501 -2658
  30. package/build/src/tools/categories.js +3 -0
  31. package/build/src/tools/console.js +42 -39
  32. package/build/src/tools/emulation.js +1 -1
  33. package/build/src/tools/extensions.js +5 -11
  34. package/build/src/tools/inPage.js +3 -13
  35. package/build/src/tools/input.js +15 -16
  36. package/build/src/tools/lighthouse.js +2 -2
  37. package/build/src/tools/memory.js +48 -3
  38. package/build/src/tools/network.js +4 -4
  39. package/build/src/tools/pages.js +212 -146
  40. package/build/src/tools/performance.js +1 -1
  41. package/build/src/tools/screencast.js +20 -8
  42. package/build/src/tools/screenshot.js +3 -3
  43. package/build/src/tools/script.js +22 -16
  44. package/build/src/tools/tools.js +2 -0
  45. package/build/src/tools/webmcp.js +63 -0
  46. package/build/src/utils/check-for-updates.js +73 -0
  47. package/build/src/utils/files.js +4 -0
  48. package/build/src/utils/id.js +15 -0
  49. package/build/src/version.js +1 -1
  50. package/package.json +13 -8
  51. package/build/src/utils/ExtensionRegistry.js +0 -35
@@ -3,7 +3,10 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ import { logger } from './logger.js';
7
+ import { TextSnapshot } from './TextSnapshot.js';
6
8
  import { takeSnapshot } from './tools/snapshot.js';
9
+ import { getNetworkMultiplierFromString, WaitForHelper, } from './WaitForHelper.js';
7
10
  /**
8
11
  * Per-page state wrapper. Consolidates dialog, snapshot, emulation,
9
12
  * and metadata that were previously scattered across Maps in McpContext.
@@ -18,6 +21,7 @@ export class McpPage {
18
21
  // Snapshot
19
22
  textSnapshot = null;
20
23
  uniqueBackendNodeIdToMcpId = new Map();
24
+ extraHandles = [];
21
25
  // Emulation
22
26
  emulationSettings = {};
23
27
  // Metadata
@@ -26,6 +30,7 @@ export class McpPage {
26
30
  // Dialog
27
31
  #dialog;
28
32
  #dialogHandler;
33
+ inPageTools;
29
34
  constructor(page, id) {
30
35
  this.pptrPage = page;
31
36
  this.id = id;
@@ -43,6 +48,12 @@ export class McpPage {
43
48
  clearDialog() {
44
49
  this.#dialog = undefined;
45
50
  }
51
+ getInPageTools() {
52
+ return this.inPageTools;
53
+ }
54
+ getWebMcpTools() {
55
+ return this.pptrPage.webmcp.tools();
56
+ }
46
57
  get networkConditions() {
47
58
  return this.emulationSettings.networkConditions ?? null;
48
59
  }
@@ -61,9 +72,162 @@ export class McpPage {
61
72
  get colorScheme() {
62
73
  return this.emulationSettings.colorScheme ?? null;
63
74
  }
75
+ // Public for testability: tests spy on this method to verify throttle multipliers.
76
+ createWaitForHelper(cpuMultiplier, networkMultiplier) {
77
+ return new WaitForHelper(this.pptrPage, cpuMultiplier, networkMultiplier);
78
+ }
79
+ waitForEventsAfterAction(action, options) {
80
+ const helper = this.createWaitForHelper(this.cpuThrottlingRate, getNetworkMultiplierFromString(this.networkConditions));
81
+ return helper.waitForEventsAfterAction(action, options);
82
+ }
64
83
  dispose() {
65
84
  this.pptrPage.off('dialog', this.#dialogHandler);
66
85
  }
86
+ async executeInPageTool(toolName, params, response) {
87
+ // Creates array of ElementHandles from the UIDs in the params.
88
+ // We do not replace the uids with the ElementsHandles yet, because
89
+ // the `evaluate` function only turns them into DOM elements if they
90
+ // are passed as non-nested arguments.
91
+ const handles = [];
92
+ for (const value of Object.values(params)) {
93
+ if (value instanceof Object &&
94
+ 'uid' in value &&
95
+ typeof value.uid === 'string' &&
96
+ Object.keys(value).length === 1) {
97
+ handles.push(await this.getElementByUid(value.uid));
98
+ }
99
+ }
100
+ const result = await this.pptrPage.evaluate(async (name, args, ...elements) => {
101
+ // Replace the UIDs with DOM elements.
102
+ for (const [key, value] of Object.entries(args)) {
103
+ if (value instanceof Object &&
104
+ 'uid' in value &&
105
+ typeof value.uid === 'string' &&
106
+ Object.keys(value).length === 1) {
107
+ args[key] = elements.shift();
108
+ }
109
+ }
110
+ if (!window.__dtmcp?.executeTool) {
111
+ throw new Error('No tools found on the page');
112
+ }
113
+ const toolResult = await window.__dtmcp.executeTool(name, args);
114
+ const stashDOMElement = (el) => {
115
+ if (!window.__dtmcp) {
116
+ window.__dtmcp = {};
117
+ }
118
+ if (window.__dtmcp.stashedElements === undefined) {
119
+ window.__dtmcp.stashedElements = [];
120
+ }
121
+ window.__dtmcp.stashedElements.push(el);
122
+ return {
123
+ stashedId: `stashed-${window.__dtmcp.stashedElements.length - 1}`,
124
+ };
125
+ };
126
+ const ancestors = [];
127
+ // Recursively walks the tool result:
128
+ // - Replaces DOM elements with an ID and stashes the DOM element on the window object
129
+ // - Replaces non-plain objects with a string representation of the object
130
+ // - Replaces circular references with the string '<Circular reference>'
131
+ // - Replaces functions with the string '<Function object>'
132
+ const processToolResult = (data, parentEl) => {
133
+ // 1. Handle DOM Elements
134
+ if (data instanceof Element) {
135
+ return stashDOMElement(data);
136
+ }
137
+ // 2. Handle Arrays
138
+ if (Array.isArray(data)) {
139
+ return data.map((item) => processToolResult(item, parentEl));
140
+ }
141
+ // 3. Handle Objects
142
+ if (data !== null && typeof data === 'object') {
143
+ while (ancestors.length > 0 && ancestors.at(-1) !== parentEl) {
144
+ ancestors.pop();
145
+ }
146
+ if (ancestors.includes(data)) {
147
+ return '<Circular reference>';
148
+ }
149
+ ancestors.push(data);
150
+ // If not a plain object, return a string representation of the object
151
+ if (Object.getPrototypeOf(data) !== Object.prototype) {
152
+ return `<${data.constructor.name} instance>`;
153
+ }
154
+ const processedObj = {};
155
+ for (const [key, value] of Object.entries(data)) {
156
+ processedObj[key] = processToolResult(value, data);
157
+ }
158
+ return processedObj;
159
+ }
160
+ // 4. Handle Functions
161
+ if (typeof data === 'function') {
162
+ return '<Function object>';
163
+ }
164
+ // 5. Return primitives (strings, numbers, booleans) as-is
165
+ return data;
166
+ };
167
+ return {
168
+ result: processToolResult(toolResult),
169
+ stashed: window.__dtmcp?.stashedElements?.length ?? 0,
170
+ };
171
+ }, toolName, params, ...handles);
172
+ const elementHandles = [];
173
+ for (let i = 0; i < (result.stashed ?? 0); i++) {
174
+ const elementHandle = await this.pptrPage.evaluateHandle(index => {
175
+ const el = window.__dtmcp?.stashedElements?.[index];
176
+ if (!el) {
177
+ throw new Error(`Stashed element at index ${index} not found`);
178
+ }
179
+ return el;
180
+ }, i);
181
+ elementHandles.push(elementHandle);
182
+ }
183
+ if (elementHandles.length) {
184
+ const oldHandles = [...this.extraHandles];
185
+ this.textSnapshot = await TextSnapshot.create(this, {
186
+ extraHandles: elementHandles,
187
+ });
188
+ response.includeSnapshot();
189
+ for (const handle of oldHandles) {
190
+ await handle
191
+ .dispose()
192
+ .catch(e => logger('Failed to dispose old handle', e));
193
+ }
194
+ }
195
+ const cdpElementIds = await Promise.all(elementHandles.map(async (elementHandle, index) => {
196
+ const backendNodeId = await elementHandle.backendNodeId();
197
+ if (!backendNodeId) {
198
+ logger(`No backendNodeId for stashed DOM element with index ${index}`);
199
+ return `stashed-${index}`;
200
+ }
201
+ const cdpElementId = this.resolveCdpElementId(backendNodeId);
202
+ if (!cdpElementId) {
203
+ logger(`Could not get cdpElementId for backend node ${backendNodeId}`);
204
+ return `stashed-${index}`;
205
+ }
206
+ return cdpElementId;
207
+ }));
208
+ const recursivelyReplaceStashedElements = (node) => {
209
+ if (Array.isArray(node)) {
210
+ return node.map(x => recursivelyReplaceStashedElements(x));
211
+ }
212
+ if (node !== null && typeof node === 'object') {
213
+ if ('stashedId' in node &&
214
+ typeof node.stashedId === 'string' &&
215
+ node.stashedId.startsWith('stashed-') &&
216
+ Object.keys(node).length === 1) {
217
+ const index = parseInt(node.stashedId.split('-')[1]);
218
+ return { uid: cdpElementIds[index] };
219
+ }
220
+ const resultObj = {};
221
+ for (const [key, value] of Object.entries(node)) {
222
+ resultObj[key] = recursivelyReplaceStashedElements(value);
223
+ }
224
+ return resultObj;
225
+ }
226
+ return node;
227
+ };
228
+ const resultWithUids = recursivelyReplaceStashedElements(result.result);
229
+ response.appendResponseLine(JSON.stringify(resultWithUids, null, 2));
230
+ }
67
231
  async getElementByUid(uid) {
68
232
  if (!this.textSnapshot) {
69
233
  throw new Error(`No snapshot found for page ${this.id ?? '?'}. Use ${takeSnapshot.name} to capture one.`);
@@ -92,4 +256,54 @@ export class McpPage {
92
256
  getAXNodeByUid(uid) {
93
257
  return this.textSnapshot?.idToNode.get(uid);
94
258
  }
259
+ resolveCdpElementId(cdpBackendNodeId) {
260
+ if (!cdpBackendNodeId) {
261
+ logger('no cdpBackendNodeId');
262
+ return;
263
+ }
264
+ const snapshot = this.textSnapshot;
265
+ if (!snapshot) {
266
+ logger('no text snapshot');
267
+ return;
268
+ }
269
+ // TODO: index by backendNodeId instead.
270
+ const queue = [snapshot.root];
271
+ while (queue.length) {
272
+ const current = queue.pop();
273
+ if (current.backendNodeId === cdpBackendNodeId) {
274
+ return current.id;
275
+ }
276
+ for (const child of current.children) {
277
+ queue.push(child);
278
+ }
279
+ }
280
+ return;
281
+ }
282
+ async getDevToolsData() {
283
+ try {
284
+ logger('Getting DevTools UI data');
285
+ const devtoolsPage = this.devToolsPage;
286
+ if (!devtoolsPage) {
287
+ logger('No DevTools page detected');
288
+ return {};
289
+ }
290
+ const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
291
+ // @ts-expect-error no types
292
+ const UI = await import('/bundled/ui/legacy/legacy.js');
293
+ // @ts-expect-error no types
294
+ const SDK = await import('/bundled/core/sdk/sdk.js');
295
+ const request = UI.Context.Context.instance().flavor(SDK.NetworkRequest.NetworkRequest);
296
+ const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
297
+ return {
298
+ cdpRequestId: request?.requestId(),
299
+ cdpBackendNodeId: node?.backendNodeId(),
300
+ };
301
+ });
302
+ return { cdpBackendNodeId, cdpRequestId };
303
+ }
304
+ catch (err) {
305
+ logger('error getting devtools data', err);
306
+ }
307
+ return {};
308
+ }
95
309
  }
@@ -4,14 +4,62 @@
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';
10
11
  import { UncaughtError } from './PageCollector.js';
12
+ import { TextSnapshot } from './TextSnapshot.js';
11
13
  import { DevTools } from './third_party/index.js';
12
14
  import { handleDialog } from './tools/pages.js';
13
15
  import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
14
16
  import { paginate } from './utils/pagination.js';
17
+ export function replaceHtmlElementsWithUids(schema) {
18
+ if (typeof schema === 'boolean') {
19
+ return;
20
+ }
21
+ let isHtmlElement = false;
22
+ for (const [key, value] of Object.entries(schema)) {
23
+ if (key === 'x-mcp-type' && value === 'HTMLElement') {
24
+ isHtmlElement = true;
25
+ break;
26
+ }
27
+ }
28
+ if (isHtmlElement) {
29
+ schema.properties = { uid: { type: 'string' } };
30
+ schema.required = ['uid'];
31
+ }
32
+ if (schema.properties) {
33
+ for (const key of Object.keys(schema.properties)) {
34
+ replaceHtmlElementsWithUids(schema.properties[key]);
35
+ }
36
+ }
37
+ if (schema.items) {
38
+ if (Array.isArray(schema.items)) {
39
+ for (const item of schema.items) {
40
+ replaceHtmlElementsWithUids(item);
41
+ }
42
+ }
43
+ else {
44
+ replaceHtmlElementsWithUids(schema.items);
45
+ }
46
+ }
47
+ if (schema.anyOf) {
48
+ for (const s of schema.anyOf) {
49
+ replaceHtmlElementsWithUids(s);
50
+ }
51
+ }
52
+ if (schema.allOf) {
53
+ for (const s of schema.allOf) {
54
+ replaceHtmlElementsWithUids(s);
55
+ }
56
+ }
57
+ if (schema.oneOf) {
58
+ for (const s of schema.oneOf) {
59
+ replaceHtmlElementsWithUids(s);
60
+ }
61
+ }
62
+ }
15
63
  async function getToolGroup(page) {
16
64
  // Check if there is a `devtoolstooldiscovery` event listener
17
65
  const windowHandle = await page.pptrPage.evaluateHandle(() => window);
@@ -54,6 +102,9 @@ async function getToolGroup(page) {
54
102
  }, 0);
55
103
  });
56
104
  });
105
+ for (const tool of toolGroup?.tools ?? []) {
106
+ replaceHtmlElementsWithUids(tool.inputSchema);
107
+ }
57
108
  return toolGroup;
58
109
  }
59
110
  export class McpResponse {
@@ -69,20 +120,26 @@ export class McpResponse {
69
120
  #attachedLighthouseResult;
70
121
  #textResponseLines = [];
71
122
  #images = [];
123
+ #heapSnapshotOptions;
72
124
  #networkRequestsOptions;
73
125
  #consoleDataOptions;
74
126
  #listExtensions;
75
127
  #listInPageTools;
128
+ #listWebMcpTools;
76
129
  #devToolsData;
77
130
  #tabId;
78
131
  #args;
79
132
  #page;
133
+ #redactNetworkHeaders = true;
80
134
  constructor(args) {
81
135
  this.#args = args;
82
136
  }
83
137
  setPage(page) {
84
138
  this.#page = page;
85
139
  }
140
+ setRedactNetworkHeaders(value) {
141
+ this.#redactNetworkHeaders = value;
142
+ }
86
143
  attachDevToolsData(data) {
87
144
  this.#devToolsData = data;
88
145
  }
@@ -109,6 +166,9 @@ export class McpResponse {
109
166
  this.#listInPageTools = true;
110
167
  }
111
168
  }
169
+ setListWebMcpTools() {
170
+ this.#listWebMcpTools = true;
171
+ }
112
172
  setIncludeNetworkRequests(value, options) {
113
173
  if (!value) {
114
174
  this.#networkRequestsOptions = undefined;
@@ -197,6 +257,22 @@ export class McpResponse {
197
257
  appendResponseLine(value) {
198
258
  this.#textResponseLines.push(value);
199
259
  }
260
+ setHeapSnapshotAggregates(aggregates, options) {
261
+ this.#heapSnapshotOptions = {
262
+ ...this.#heapSnapshotOptions,
263
+ include: true,
264
+ aggregates,
265
+ pagination: options,
266
+ };
267
+ }
268
+ setHeapSnapshotStats(stats, staticData) {
269
+ this.#heapSnapshotOptions = {
270
+ ...this.#heapSnapshotOptions,
271
+ include: true,
272
+ stats,
273
+ staticData,
274
+ };
275
+ }
200
276
  attachImage(value) {
201
277
  this.#images.push(value);
202
278
  }
@@ -209,6 +285,9 @@ export class McpResponse {
209
285
  get snapshotParams() {
210
286
  return this.#snapshotParams;
211
287
  }
288
+ get listWebMcpTools() {
289
+ return this.#listWebMcpTools;
290
+ }
212
291
  async handle(toolName, context) {
213
292
  if (this.#includePages) {
214
293
  await context.createPagesSnapshot();
@@ -221,13 +300,16 @@ export class McpResponse {
221
300
  if (!this.#page) {
222
301
  throw new Error('Response must have a page');
223
302
  }
224
- await context.createTextSnapshot(this.#page, this.#snapshotParams.verbose, this.#devToolsData);
303
+ this.#page.textSnapshot = await TextSnapshot.create(this.#page, {
304
+ verbose: this.#snapshotParams.verbose,
305
+ devtoolsData: this.#devToolsData,
306
+ });
225
307
  const textSnapshot = this.#page.textSnapshot;
226
308
  if (textSnapshot) {
227
309
  const formatter = new SnapshotFormatter(textSnapshot);
228
310
  if (this.#snapshotParams.filePath) {
229
- await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath);
230
- snapshot = this.#snapshotParams.filePath;
311
+ const result = await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath, '.txt');
312
+ snapshot = result.filename;
231
313
  }
232
314
  else {
233
315
  snapshot = formatter;
@@ -246,7 +328,8 @@ export class McpResponse {
246
328
  fetchData: true,
247
329
  requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
248
330
  responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
249
- saveFile: (data, filename) => context.saveFile(data, filename),
331
+ saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
332
+ redactNetworkHeaders: this.#redactNetworkHeaders,
250
333
  });
251
334
  detailedNetworkRequest = formatter;
252
335
  }
@@ -270,7 +353,7 @@ export class McpResponse {
270
353
  const formatter = new IssueFormatter(message, {
271
354
  id: consoleMessageStableId,
272
355
  requestIdResolver: context.resolveCdpRequestId.bind(context, this.#page),
273
- elementIdResolver: context.resolveCdpElementId.bind(context, this.#page),
356
+ elementIdResolver: this.#page.resolveCdpElementId.bind(this.#page),
274
357
  });
275
358
  if (!formatter.isValid()) {
276
359
  throw new Error("Can't provide details for the msgid " + consoleMessageStableId);
@@ -280,12 +363,18 @@ export class McpResponse {
280
363
  }
281
364
  let extensions;
282
365
  if (this.#listExtensions) {
283
- extensions = context.listExtensions();
366
+ extensions = await context.listExtensions();
284
367
  }
285
368
  let inPageTools;
286
369
  if (this.#listInPageTools) {
287
- inPageTools = await getToolGroup(context.getSelectedMcpPage());
288
- context.setInPageTools(inPageTools);
370
+ const page = this.#page ?? context.getSelectedMcpPage();
371
+ inPageTools = await getToolGroup(page);
372
+ page.inPageTools = inPageTools;
373
+ }
374
+ let webmcpTools;
375
+ if (this.#listWebMcpTools && this.#args.experimentalWebmcp) {
376
+ const page = this.#page ?? context.getSelectedMcpPage();
377
+ webmcpTools = page.getWebMcpTools();
289
378
  }
290
379
  let consoleMessages;
291
380
  if (this.#consoleDataOptions?.include) {
@@ -349,7 +438,8 @@ export class McpResponse {
349
438
  selectedInDevToolsUI: context.getNetworkRequestStableId(request) ===
350
439
  this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
351
440
  fetchData: false,
352
- saveFile: (data, filename) => context.saveFile(data, filename),
441
+ saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
442
+ redactNetworkHeaders: this.#redactNetworkHeaders,
353
443
  })));
354
444
  }
355
445
  }
@@ -364,6 +454,7 @@ export class McpResponse {
364
454
  extensions,
365
455
  lighthouseResult: this.#attachedLighthouseResult,
366
456
  inPageTools,
457
+ webmcpTools,
367
458
  });
368
459
  }
369
460
  format(toolName, context, data) {
@@ -529,6 +620,32 @@ Call ${handleDialog.name} to handle it before continuing.`);
529
620
  structuredContent.snapshot = data.snapshot.toJSON();
530
621
  }
531
622
  }
623
+ if (this.#heapSnapshotOptions?.include) {
624
+ response.push('## Heap Snapshot Data');
625
+ const stats = this.#heapSnapshotOptions.stats;
626
+ const staticData = this.#heapSnapshotOptions.staticData;
627
+ if (stats) {
628
+ response.push(`Statistics: ${JSON.stringify(stats, null, 2)}`);
629
+ structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
630
+ structuredContent.heapSnapshot.stats = stats;
631
+ }
632
+ if (staticData) {
633
+ response.push(`Static Data: ${JSON.stringify(staticData, null, 2)}`);
634
+ structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
635
+ structuredContent.heapSnapshot.staticData = staticData;
636
+ }
637
+ const aggregates = this.#heapSnapshotOptions.aggregates;
638
+ if (aggregates) {
639
+ const sortedEntries = HeapSnapshotFormatter.sort(aggregates);
640
+ const paginationData = this.#dataWithPagination(sortedEntries, this.#heapSnapshotOptions.pagination);
641
+ structuredContent.pagination = paginationData.pagination;
642
+ response.push(...paginationData.info);
643
+ const paginatedRecord = Object.fromEntries(paginationData.items);
644
+ const formatter = new HeapSnapshotFormatter(paginatedRecord);
645
+ response.push(formatter.toString());
646
+ structuredContent.heapSnapshotData = formatter.toJSON();
647
+ }
648
+ }
532
649
  if (data.detailedNetworkRequest) {
533
650
  response.push(data.detailedNetworkRequest.toStringDetailed());
534
651
  structuredContent.networkRequest =
@@ -540,15 +657,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
540
657
  data.detailedConsoleMessage.toJSONDetailed();
541
658
  }
542
659
  if (data.extensions) {
543
- structuredContent.extensions = data.extensions;
660
+ const extensionArray = Array.from(data.extensions.values());
661
+ structuredContent.extensions = extensionArray;
544
662
  response.push('## Extensions');
545
- if (data.extensions.length === 0) {
663
+ if (extensionArray.length === 0) {
546
664
  response.push('No extensions installed.');
547
665
  }
548
666
  else {
549
- const extensionsMessage = data.extensions
667
+ const extensionsMessage = extensionArray
550
668
  .map(extension => {
551
- return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.isEnabled ? 'Enabled' : 'Disabled'}`;
669
+ return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.enabled ? 'Enabled' : 'Disabled'}`;
552
670
  })
553
671
  .join('\n');
554
672
  response.push(extensionsMessage);
@@ -572,6 +690,26 @@ Call ${handleDialog.name} to handle it before continuing.`);
572
690
  response.push(toolDefinitionsMessage);
573
691
  }
574
692
  }
693
+ if (this.#listWebMcpTools && data.webmcpTools) {
694
+ structuredContent.webmcpTools = data.webmcpTools.map(({ name, description, inputSchema, annotations }) => ({
695
+ name,
696
+ description,
697
+ inputSchema,
698
+ annotations,
699
+ }));
700
+ response.push('## WebMCP tools');
701
+ if (data.webmcpTools.length === 0) {
702
+ response.push('No WebMCP tools available.');
703
+ }
704
+ else {
705
+ const webmcpToolsMessage = data.webmcpTools
706
+ .map(tool => {
707
+ return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(tool.inputSchema)}, annotations=${JSON.stringify(tool.annotations)}`;
708
+ })
709
+ .join('\n');
710
+ response.push(webmcpToolsMessage);
711
+ }
712
+ }
575
713
  if (this.#networkRequestsOptions?.include && data.networkRequests) {
576
714
  const requests = data.networkRequests;
577
715
  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;
@@ -206,34 +197,25 @@ class PageEventSubscriber {
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
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
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];