chrome-devtools-mcp 0.22.0 → 0.24.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 (78) hide show
  1. package/README.md +4 -0
  2. package/build/src/DevToolsConnectionAdapter.js +1 -0
  3. package/build/src/DevtoolsUtils.js +1 -0
  4. package/build/src/HeapSnapshotManager.js +16 -0
  5. package/build/src/McpContext.js +54 -126
  6. package/build/src/McpPage.js +204 -0
  7. package/build/src/McpResponse.js +44 -6
  8. package/build/src/Mutex.js +1 -0
  9. package/build/src/PageCollector.js +1 -0
  10. package/build/src/SlimMcpResponse.js +1 -0
  11. package/build/src/TextSnapshot.js +236 -0
  12. package/build/src/WaitForHelper.js +6 -0
  13. package/build/src/bin/check-latest-version.js +1 -0
  14. package/build/src/bin/chrome-devtools-cli-options.js +206 -46
  15. package/build/src/bin/chrome-devtools-mcp-cli-options.js +13 -1
  16. package/build/src/bin/chrome-devtools-mcp-main.js +1 -0
  17. package/build/src/bin/chrome-devtools-mcp.js +1 -0
  18. package/build/src/bin/chrome-devtools.js +27 -27
  19. package/build/src/browser.js +1 -0
  20. package/build/src/daemon/client.js +14 -12
  21. package/build/src/daemon/daemon.js +7 -5
  22. package/build/src/daemon/types.js +1 -0
  23. package/build/src/daemon/utils.js +20 -14
  24. package/build/src/formatters/ConsoleFormatter.js +48 -1
  25. package/build/src/formatters/HeapSnapshotFormatter.js +18 -2
  26. package/build/src/formatters/IssueFormatter.js +1 -0
  27. package/build/src/formatters/NetworkFormatter.js +1 -0
  28. package/build/src/formatters/SnapshotFormatter.js +2 -1
  29. package/build/src/index.js +114 -51
  30. package/build/src/issue-descriptions.js +1 -0
  31. package/build/src/logger.js +1 -0
  32. package/build/src/polyfill.js +1 -0
  33. package/build/src/telemetry/ClearcutLogger.js +13 -1
  34. package/build/src/telemetry/WatchdogClient.js +1 -0
  35. package/build/src/telemetry/flagUtils.js +1 -0
  36. package/build/src/telemetry/metricUtils.js +1 -0
  37. package/build/src/telemetry/persistence.js +1 -0
  38. package/build/src/telemetry/toolMetricsUtils.js +2 -1
  39. package/build/src/telemetry/types.js +1 -0
  40. package/build/src/telemetry/watchdog/ClearcutSender.js +1 -0
  41. package/build/src/telemetry/watchdog/main.js +1 -0
  42. package/build/src/third_party/THIRD_PARTY_NOTICES +32 -5
  43. package/build/src/third_party/bundled-packages.json +3 -2
  44. package/build/src/third_party/devtools-formatter-worker.js +2451 -2933
  45. package/build/src/third_party/devtools-heap-snapshot-worker.js +32 -26
  46. package/build/src/third_party/index.js +1942 -1536
  47. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +21717 -20261
  48. package/build/src/tools/ToolDefinition.js +1 -0
  49. package/build/src/tools/categories.js +6 -2
  50. package/build/src/tools/console.js +3 -0
  51. package/build/src/tools/emulation.js +2 -0
  52. package/build/src/tools/extensions.js +6 -0
  53. package/build/src/tools/inPage.js +5 -35
  54. package/build/src/tools/input.js +13 -2
  55. package/build/src/tools/lighthouse.js +17 -9
  56. package/build/src/tools/memory.js +34 -1
  57. package/build/src/tools/network.js +7 -2
  58. package/build/src/tools/pages.js +218 -146
  59. package/build/src/tools/performance.js +6 -0
  60. package/build/src/tools/screencast.js +25 -10
  61. package/build/src/tools/screenshot.js +3 -0
  62. package/build/src/tools/script.js +2 -0
  63. package/build/src/tools/slim/tools.js +4 -0
  64. package/build/src/tools/snapshot.js +5 -1
  65. package/build/src/tools/tools.js +1 -0
  66. package/build/src/tools/webmcp.js +3 -0
  67. package/build/src/trace-processing/parse.js +1 -0
  68. package/build/src/types.js +1 -0
  69. package/build/src/utils/check-for-updates.js +1 -0
  70. package/build/src/utils/files.js +5 -10
  71. package/build/src/utils/id.js +1 -0
  72. package/build/src/utils/keyboard.js +1 -0
  73. package/build/src/utils/pagination.js +1 -0
  74. package/build/src/utils/string.js +1 -0
  75. package/build/src/utils/types.js +1 -0
  76. package/build/src/version.js +2 -1
  77. package/package.json +10 -9
  78. package/build/src/bin/cliDefinitions.js +0 -621
package/README.md CHANGED
@@ -591,6 +591,10 @@ The Chrome DevTools MCP server supports the following configuration option:
591
591
  Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
592
592
  - **Type:** boolean
593
593
 
594
+ - **`--experimentalFfmpegPath`/ `--experimental-ffmpeg-path`**
595
+ Path to ffmpeg executable for screencast recording.
596
+ - **Type:** string
597
+
594
598
  - **`--experimentalWebmcp`/ `--experimental-webmcp`**
595
599
  Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`
596
600
  - **Type:** boolean
@@ -67,3 +67,4 @@ export class PuppeteerDevToolsConnection {
67
67
  }
68
68
  }
69
69
  }
70
+ //# sourceMappingURL=DevToolsConnectionAdapter.js.map
@@ -292,3 +292,4 @@ async function waitForScript(model, scriptId, signal) {
292
292
  });
293
293
  }
294
294
  }
295
+ //# sourceMappingURL=DevtoolsUtils.js.map
@@ -56,6 +56,17 @@ export class HeapSnapshotManager {
56
56
  }
57
57
  return uid;
58
58
  }
59
+ async getNodesByUid(filePath, uid) {
60
+ const snapshot = await this.getSnapshot(filePath);
61
+ const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
62
+ const className = await this.resolveClassKeyFromUid(filePath, uid);
63
+ if (!className) {
64
+ throw new Error(`Class with UID ${uid} not found in heap snapshot`);
65
+ }
66
+ const provider = snapshot.createNodesProviderForClass(className, filter);
67
+ const range = await provider.serializeItemsRange(0, 1);
68
+ return await provider.serializeItemsRange(0, range.totalLength);
69
+ }
59
70
  #getCachedSnapshot(filePath) {
60
71
  const absolutePath = path.resolve(filePath);
61
72
  const cached = this.#snapshots.get(absolutePath);
@@ -64,6 +75,10 @@ export class HeapSnapshotManager {
64
75
  }
65
76
  return cached;
66
77
  }
78
+ async resolveClassKeyFromUid(filePath, uid) {
79
+ const cached = this.#getCachedSnapshot(filePath);
80
+ return cached.uidToClassKey.get(uid);
81
+ }
67
82
  async #loadSnapshot(absolutePath) {
68
83
  const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
69
84
  /* noop */
@@ -92,3 +107,4 @@ export class HeapSnapshotManager {
92
107
  }
93
108
  }
94
109
  }
110
+ //# sourceMappingURL=HeapSnapshotManager.js.map
@@ -4,16 +4,17 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import fs from 'node:fs/promises';
7
+ import os from 'node:os';
7
8
  import path from 'node:path';
9
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
10
  import { UniverseManager } from './DevtoolsUtils.js';
9
11
  import { HeapSnapshotManager } from './HeapSnapshotManager.js';
10
12
  import { McpPage } from './McpPage.js';
11
13
  import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
12
- import { Locator } from './third_party/index.js';
13
- import { PredefinedNetworkConditions } from './third_party/index.js';
14
+ import { Locator, PredefinedNetworkConditions, } from './third_party/index.js';
14
15
  import { listPages } from './tools/pages.js';
15
16
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
16
- import { ensureExtension, saveTemporaryFile } from './utils/files.js';
17
+ import { ensureExtension, getTempFilePath } from './utils/files.js';
17
18
  import { getNetworkMultiplierFromString } from './WaitForHelper.js';
18
19
  const DEFAULT_TIMEOUT = 5_000;
19
20
  const NAVIGATION_TIMEOUT = 10_000;
@@ -37,11 +38,11 @@ export class McpContext {
37
38
  #extensionPages = new WeakMap();
38
39
  #extensionServiceWorkerMap = new WeakMap();
39
40
  #nextExtensionServiceWorkerId = 1;
40
- #nextSnapshotId = 1;
41
41
  #traceResults = [];
42
42
  #locatorClass;
43
43
  #options;
44
44
  #heapSnapshotManager = new HeapSnapshotManager();
45
+ #roots = undefined;
45
46
  constructor(browser, logger, options, locatorClass) {
46
47
  this.browser = browser;
47
48
  this.logger = logger;
@@ -90,6 +91,39 @@ export class McpContext {
90
91
  await context.#init();
91
92
  return context;
92
93
  }
94
+ roots() {
95
+ if (this.#roots === undefined) {
96
+ return undefined;
97
+ }
98
+ return [
99
+ ...this.#roots,
100
+ {
101
+ uri: pathToFileURL(os.tmpdir()).href,
102
+ name: 'temp',
103
+ },
104
+ ];
105
+ }
106
+ setRoots(roots) {
107
+ this.#roots = roots;
108
+ }
109
+ validatePath(filePath) {
110
+ if (filePath === undefined) {
111
+ return;
112
+ }
113
+ const roots = this.roots();
114
+ if (roots === undefined) {
115
+ return;
116
+ }
117
+ const absolutePath = path.resolve(filePath);
118
+ for (const root of roots) {
119
+ const rootPath = path.resolve(fileURLToPath(root.uri));
120
+ if (absolutePath === rootPath ||
121
+ absolutePath.startsWith(rootPath + path.sep)) {
122
+ return;
123
+ }
124
+ }
125
+ throw new Error(`Access denied: path ${filePath} is not within any of the workspace roots ${JSON.stringify(roots)}.`);
126
+ }
93
127
  resolveCdpRequestId(page, cdpRequestId) {
94
128
  if (!cdpRequestId) {
95
129
  this.logger('no network request');
@@ -105,29 +139,6 @@ export class McpContext {
105
139
  }
106
140
  return this.#networkCollector.getIdForResource(request);
107
141
  }
108
- resolveCdpElementId(page, cdpBackendNodeId) {
109
- if (!cdpBackendNodeId) {
110
- this.logger('no cdpBackendNodeId');
111
- return;
112
- }
113
- const snapshot = page.textSnapshot;
114
- if (!snapshot) {
115
- this.logger('no text snapshot');
116
- return;
117
- }
118
- // TODO: index by backendNodeId instead.
119
- const queue = [snapshot.root];
120
- while (queue.length) {
121
- const current = queue.pop();
122
- if (current.backendNodeId === cdpBackendNodeId) {
123
- return current.id;
124
- }
125
- for (const child of current.children) {
126
- queue.push(child);
127
- }
128
- }
129
- return;
130
- }
131
142
  getNetworkRequests(page, includePreservedRequests) {
132
143
  return this.#networkCollector.getData(page.pptrPage, includePreservedRequests);
133
144
  }
@@ -486,111 +497,19 @@ export class McpContext {
486
497
  getIsolatedContextName(page) {
487
498
  return this.#mcpPages.get(page)?.isolatedContextName;
488
499
  }
489
- getDevToolsPage(page) {
490
- return this.#mcpPages.get(page)?.devToolsPage;
491
- }
492
- async getDevToolsData(page) {
500
+ async saveTemporaryFile(data, filename) {
501
+ const filepath = await getTempFilePath(filename);
502
+ this.validatePath(filepath);
493
503
  try {
494
- this.logger('Getting DevTools UI data');
495
- const devtoolsPage = this.getDevToolsPage(page.pptrPage);
496
- if (!devtoolsPage) {
497
- this.logger('No DevTools page detected');
498
- return {};
499
- }
500
- const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
501
- // @ts-expect-error no types
502
- const UI = await import('/bundled/ui/legacy/legacy.js');
503
- // @ts-expect-error no types
504
- const SDK = await import('/bundled/core/sdk/sdk.js');
505
- const request = UI.Context.Context.instance().flavor(SDK.NetworkRequest.NetworkRequest);
506
- const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
507
- return {
508
- cdpRequestId: request?.requestId(),
509
- cdpBackendNodeId: node?.backendNodeId(),
510
- };
511
- });
512
- return { cdpBackendNodeId, cdpRequestId };
504
+ await fs.writeFile(filepath, data);
513
505
  }
514
506
  catch (err) {
515
- this.logger('error getting devtools data', err);
516
- }
517
- return {};
518
- }
519
- /**
520
- * Creates a text snapshot of a page.
521
- */
522
- async createTextSnapshot(page, verbose = false, devtoolsData = undefined) {
523
- const rootNode = await page.pptrPage.accessibility.snapshot({
524
- includeIframes: true,
525
- interestingOnly: !verbose,
526
- });
527
- if (!rootNode) {
528
- return;
529
- }
530
- const { uniqueBackendNodeIdToMcpId } = page;
531
- const snapshotId = this.#nextSnapshotId++;
532
- // Iterate through the whole accessibility node tree and assign node ids that
533
- // will be used for the tree serialization and mapping ids back to nodes.
534
- let idCounter = 0;
535
- const idToNode = new Map();
536
- const seenUniqueIds = new Set();
537
- const assignIds = (node) => {
538
- let id = '';
539
- // @ts-expect-error untyped loaderId & backendNodeId.
540
- const uniqueBackendId = `${node.loaderId}_${node.backendNodeId}`;
541
- if (uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
542
- // Re-use MCP exposed ID if the uniqueId is the same.
543
- id = uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
544
- }
545
- else {
546
- // Only generate a new ID if we have not seen the node before.
547
- id = `${snapshotId}_${idCounter++}`;
548
- uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
549
- }
550
- seenUniqueIds.add(uniqueBackendId);
551
- const nodeWithId = {
552
- ...node,
553
- id,
554
- children: node.children
555
- ? node.children.map(child => assignIds(child))
556
- : [],
557
- };
558
- // The AXNode for an option doesn't contain its `value`.
559
- // Therefore, set text content of the option as value.
560
- if (node.role === 'option') {
561
- const optionText = node.name;
562
- if (optionText) {
563
- nodeWithId.value = optionText.toString();
564
- }
565
- }
566
- idToNode.set(nodeWithId.id, nodeWithId);
567
- return nodeWithId;
568
- };
569
- const rootNodeWithId = assignIds(rootNode);
570
- const snapshot = {
571
- root: rootNodeWithId,
572
- snapshotId: String(snapshotId),
573
- idToNode,
574
- hasSelectedElement: false,
575
- verbose,
576
- };
577
- page.textSnapshot = snapshot;
578
- const data = devtoolsData ?? (await this.getDevToolsData(page));
579
- if (data?.cdpBackendNodeId) {
580
- snapshot.hasSelectedElement = true;
581
- snapshot.selectedElementUid = this.resolveCdpElementId(page, data?.cdpBackendNodeId);
582
- }
583
- // Clean up unique IDs that we did not see anymore.
584
- for (const key of uniqueBackendNodeIdToMcpId.keys()) {
585
- if (!seenUniqueIds.has(key)) {
586
- uniqueBackendNodeIdToMcpId.delete(key);
587
- }
507
+ throw new Error('Could not save a file', { cause: err });
588
508
  }
589
- }
590
- async saveTemporaryFile(data, filename) {
591
- return await saveTemporaryFile(data, filename);
509
+ return { filepath };
592
510
  }
593
511
  async saveFile(data, clientProvidedFilePath, extension) {
512
+ this.validatePath(clientProvidedFilePath);
594
513
  try {
595
514
  const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
596
515
  await fs.mkdir(path.dirname(filePath), { recursive: true });
@@ -643,6 +562,7 @@ export class McpContext {
643
562
  await this.#networkCollector.init(pages);
644
563
  }
645
564
  async installExtension(extensionPath) {
565
+ this.validatePath(extensionPath);
646
566
  const id = await this.browser.installExtension(extensionPath);
647
567
  return id;
648
568
  }
@@ -666,12 +586,20 @@ export class McpContext {
666
586
  return pptrExtensions.get(id);
667
587
  }
668
588
  async getHeapSnapshotAggregates(filePath) {
589
+ this.validatePath(filePath);
669
590
  return await this.#heapSnapshotManager.getAggregates(filePath);
670
591
  }
671
592
  async getHeapSnapshotStats(filePath) {
593
+ this.validatePath(filePath);
672
594
  return await this.#heapSnapshotManager.getStats(filePath);
673
595
  }
674
596
  async getHeapSnapshotStaticData(filePath) {
597
+ this.validatePath(filePath);
675
598
  return await this.#heapSnapshotManager.getStaticData(filePath);
676
599
  }
600
+ async getHeapSnapshotNodesByUid(filePath, uid) {
601
+ this.validatePath(filePath);
602
+ return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
603
+ }
677
604
  }
605
+ //# sourceMappingURL=McpContext.js.map
@@ -3,6 +3,8 @@
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';
7
9
  import { getNetworkMultiplierFromString, WaitForHelper, } from './WaitForHelper.js';
8
10
  /**
@@ -19,6 +21,7 @@ export class McpPage {
19
21
  // Snapshot
20
22
  textSnapshot = null;
21
23
  uniqueBackendNodeIdToMcpId = new Map();
24
+ extraHandles = [];
22
25
  // Emulation
23
26
  emulationSettings = {};
24
27
  // Metadata
@@ -45,6 +48,11 @@ export class McpPage {
45
48
  clearDialog() {
46
49
  this.#dialog = undefined;
47
50
  }
51
+ throwIfDialogOpen() {
52
+ if (this.#dialog) {
53
+ throw new Error(`A dialog is open (${this.#dialog.type()}: ${this.#dialog.message()}).`);
54
+ }
55
+ }
48
56
  getInPageTools() {
49
57
  return this.inPageTools;
50
58
  }
@@ -80,6 +88,151 @@ export class McpPage {
80
88
  dispose() {
81
89
  this.pptrPage.off('dialog', this.#dialogHandler);
82
90
  }
91
+ async executeInPageTool(toolName, params, response) {
92
+ // Creates array of ElementHandles from the UIDs in the params.
93
+ // We do not replace the uids with the ElementsHandles yet, because
94
+ // the `evaluate` function only turns them into DOM elements if they
95
+ // are passed as non-nested arguments.
96
+ const handles = [];
97
+ for (const value of Object.values(params)) {
98
+ if (value instanceof Object &&
99
+ 'uid' in value &&
100
+ typeof value.uid === 'string' &&
101
+ Object.keys(value).length === 1) {
102
+ handles.push(await this.getElementByUid(value.uid));
103
+ }
104
+ }
105
+ const result = await this.pptrPage.evaluate(async (name, args, ...elements) => {
106
+ // Replace the UIDs with DOM elements.
107
+ for (const [key, value] of Object.entries(args)) {
108
+ if (value instanceof Object &&
109
+ 'uid' in value &&
110
+ typeof value.uid === 'string' &&
111
+ Object.keys(value).length === 1) {
112
+ args[key] = elements.shift();
113
+ }
114
+ }
115
+ if (!window.__dtmcp?.executeTool) {
116
+ throw new Error('No tools found on the page');
117
+ }
118
+ const toolResult = await window.__dtmcp.executeTool(name, args);
119
+ const stashDOMElement = (el) => {
120
+ if (!window.__dtmcp) {
121
+ window.__dtmcp = {};
122
+ }
123
+ if (window.__dtmcp.stashedElements === undefined) {
124
+ window.__dtmcp.stashedElements = [];
125
+ }
126
+ window.__dtmcp.stashedElements.push(el);
127
+ return {
128
+ stashedId: `stashed-${window.__dtmcp.stashedElements.length - 1}`,
129
+ };
130
+ };
131
+ const ancestors = [];
132
+ // Recursively walks the tool result:
133
+ // - Replaces DOM elements with an ID and stashes the DOM element on the window object
134
+ // - Replaces non-plain objects with a string representation of the object
135
+ // - Replaces circular references with the string '<Circular reference>'
136
+ // - Replaces functions with the string '<Function object>'
137
+ const processToolResult = (data, parentEl) => {
138
+ // 1. Handle DOM Elements
139
+ if (data instanceof Element) {
140
+ return stashDOMElement(data);
141
+ }
142
+ // 2. Handle Arrays
143
+ if (Array.isArray(data)) {
144
+ return data.map((item) => processToolResult(item, parentEl));
145
+ }
146
+ // 3. Handle Objects
147
+ if (data !== null && typeof data === 'object') {
148
+ while (ancestors.length > 0 && ancestors.at(-1) !== parentEl) {
149
+ ancestors.pop();
150
+ }
151
+ if (ancestors.includes(data)) {
152
+ return '<Circular reference>';
153
+ }
154
+ ancestors.push(data);
155
+ // If not a plain object, return a string representation of the object
156
+ if (Object.getPrototypeOf(data) !== Object.prototype) {
157
+ return `<${data.constructor.name} instance>`;
158
+ }
159
+ const processedObj = {};
160
+ for (const [key, value] of Object.entries(data)) {
161
+ processedObj[key] = processToolResult(value, data);
162
+ }
163
+ return processedObj;
164
+ }
165
+ // 4. Handle Functions
166
+ if (typeof data === 'function') {
167
+ return '<Function object>';
168
+ }
169
+ // 5. Return primitives (strings, numbers, booleans) as-is
170
+ return data;
171
+ };
172
+ return {
173
+ result: processToolResult(toolResult),
174
+ stashed: window.__dtmcp?.stashedElements?.length ?? 0,
175
+ };
176
+ }, toolName, params, ...handles);
177
+ const elementHandles = [];
178
+ for (let i = 0; i < (result.stashed ?? 0); i++) {
179
+ const elementHandle = await this.pptrPage.evaluateHandle(index => {
180
+ const el = window.__dtmcp?.stashedElements?.[index];
181
+ if (!el) {
182
+ throw new Error(`Stashed element at index ${index} not found`);
183
+ }
184
+ return el;
185
+ }, i);
186
+ elementHandles.push(elementHandle);
187
+ }
188
+ if (elementHandles.length) {
189
+ const oldHandles = [...this.extraHandles];
190
+ this.textSnapshot = await TextSnapshot.create(this, {
191
+ extraHandles: elementHandles,
192
+ });
193
+ response.includeSnapshot();
194
+ for (const handle of oldHandles) {
195
+ await handle
196
+ .dispose()
197
+ .catch(e => logger('Failed to dispose old handle', e));
198
+ }
199
+ }
200
+ const cdpElementIds = await Promise.all(elementHandles.map(async (elementHandle, index) => {
201
+ const backendNodeId = await elementHandle.backendNodeId();
202
+ if (!backendNodeId) {
203
+ logger(`No backendNodeId for stashed DOM element with index ${index}`);
204
+ return `stashed-${index}`;
205
+ }
206
+ const cdpElementId = this.resolveCdpElementId(backendNodeId);
207
+ if (!cdpElementId) {
208
+ logger(`Could not get cdpElementId for backend node ${backendNodeId}`);
209
+ return `stashed-${index}`;
210
+ }
211
+ return cdpElementId;
212
+ }));
213
+ const recursivelyReplaceStashedElements = (node) => {
214
+ if (Array.isArray(node)) {
215
+ return node.map(x => recursivelyReplaceStashedElements(x));
216
+ }
217
+ if (node !== null && typeof node === 'object') {
218
+ if ('stashedId' in node &&
219
+ typeof node.stashedId === 'string' &&
220
+ node.stashedId.startsWith('stashed-') &&
221
+ Object.keys(node).length === 1) {
222
+ const index = parseInt(node.stashedId.split('-')[1]);
223
+ return { uid: cdpElementIds[index] };
224
+ }
225
+ const resultObj = {};
226
+ for (const [key, value] of Object.entries(node)) {
227
+ resultObj[key] = recursivelyReplaceStashedElements(value);
228
+ }
229
+ return resultObj;
230
+ }
231
+ return node;
232
+ };
233
+ const resultWithUids = recursivelyReplaceStashedElements(result.result);
234
+ response.appendResponseLine(JSON.stringify(resultWithUids, null, 2));
235
+ }
83
236
  async getElementByUid(uid) {
84
237
  if (!this.textSnapshot) {
85
238
  throw new Error(`No snapshot found for page ${this.id ?? '?'}. Use ${takeSnapshot.name} to capture one.`);
@@ -108,4 +261,55 @@ export class McpPage {
108
261
  getAXNodeByUid(uid) {
109
262
  return this.textSnapshot?.idToNode.get(uid);
110
263
  }
264
+ resolveCdpElementId(cdpBackendNodeId) {
265
+ if (!cdpBackendNodeId) {
266
+ logger('no cdpBackendNodeId');
267
+ return;
268
+ }
269
+ const snapshot = this.textSnapshot;
270
+ if (!snapshot) {
271
+ logger('no text snapshot');
272
+ return;
273
+ }
274
+ // TODO: index by backendNodeId instead.
275
+ const queue = [snapshot.root];
276
+ while (queue.length) {
277
+ const current = queue.pop();
278
+ if (current.backendNodeId === cdpBackendNodeId) {
279
+ return current.id;
280
+ }
281
+ for (const child of current.children) {
282
+ queue.push(child);
283
+ }
284
+ }
285
+ return;
286
+ }
287
+ async getDevToolsData() {
288
+ try {
289
+ logger('Getting DevTools UI data');
290
+ const devtoolsPage = this.devToolsPage;
291
+ if (!devtoolsPage) {
292
+ logger('No DevTools page detected');
293
+ return {};
294
+ }
295
+ const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
296
+ // @ts-expect-error no types
297
+ const UI = await import('/bundled/ui/legacy/legacy.js');
298
+ // @ts-expect-error no types
299
+ const SDK = await import('/bundled/core/sdk/sdk.js');
300
+ const request = UI.Context.Context.instance().flavor(SDK.NetworkRequest.NetworkRequest);
301
+ const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
302
+ return {
303
+ cdpRequestId: request?.requestId(),
304
+ cdpBackendNodeId: node?.backendNodeId(),
305
+ };
306
+ });
307
+ return { cdpBackendNodeId, cdpRequestId };
308
+ }
309
+ catch (err) {
310
+ logger('error getting devtools data', err);
311
+ }
312
+ return {};
313
+ }
111
314
  }
315
+ //# sourceMappingURL=McpPage.js.map
@@ -5,10 +5,12 @@
5
5
  */
6
6
  import { ConsoleFormatter } from './formatters/ConsoleFormatter.js';
7
7
  import { HeapSnapshotFormatter } from './formatters/HeapSnapshotFormatter.js';
8
+ import { isNodeLike } from './formatters/HeapSnapshotFormatter.js';
8
9
  import { IssueFormatter } from './formatters/IssueFormatter.js';
9
10
  import { NetworkFormatter } from './formatters/NetworkFormatter.js';
10
11
  import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
11
12
  import { UncaughtError } from './PageCollector.js';
13
+ import { TextSnapshot } from './TextSnapshot.js';
12
14
  import { DevTools } from './third_party/index.js';
13
15
  import { handleDialog } from './tools/pages.js';
14
16
  import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
@@ -130,6 +132,7 @@ export class McpResponse {
130
132
  #args;
131
133
  #page;
132
134
  #redactNetworkHeaders = true;
135
+ #error;
133
136
  constructor(args) {
134
137
  this.#args = args;
135
138
  }
@@ -161,7 +164,7 @@ export class McpResponse {
161
164
  this.#listExtensions = true;
162
165
  }
163
166
  setListInPageTools() {
164
- if (this.#args.categoryInPageTools) {
167
+ if (this.#args.categoryExperimentalInPage) {
165
168
  this.#listInPageTools = true;
166
169
  }
167
170
  }
@@ -203,6 +206,9 @@ export class McpResponse {
203
206
  includePreservedMessages: options?.includePreservedMessages,
204
207
  };
205
208
  }
209
+ setError(error) {
210
+ this.#error = error;
211
+ }
206
212
  attachNetworkRequest(reqId, options) {
207
213
  this.#attachedNetworkRequestId = reqId;
208
214
  this.#attachedNetworkRequestOptions = options;
@@ -253,6 +259,9 @@ export class McpResponse {
253
259
  get consoleMessagesTypes() {
254
260
  return this.#consoleDataOptions?.types;
255
261
  }
262
+ get error() {
263
+ return this.#error;
264
+ }
256
265
  appendResponseLine(value) {
257
266
  this.#textResponseLines.push(value);
258
267
  }
@@ -272,6 +281,14 @@ export class McpResponse {
272
281
  staticData,
273
282
  };
274
283
  }
284
+ setHeapSnapshotNodes(nodes, options) {
285
+ this.#heapSnapshotOptions = {
286
+ ...this.#heapSnapshotOptions,
287
+ include: true,
288
+ nodes,
289
+ pagination: options,
290
+ };
291
+ }
275
292
  attachImage(value) {
276
293
  this.#images.push(value);
277
294
  }
@@ -299,7 +316,10 @@ export class McpResponse {
299
316
  if (!this.#page) {
300
317
  throw new Error('Response must have a page');
301
318
  }
302
- await context.createTextSnapshot(this.#page, this.#snapshotParams.verbose, this.#devToolsData);
319
+ this.#page.textSnapshot = await TextSnapshot.create(this.#page, {
320
+ verbose: this.#snapshotParams.verbose,
321
+ devtoolsData: this.#devToolsData,
322
+ });
303
323
  const textSnapshot = this.#page.textSnapshot;
304
324
  if (textSnapshot) {
305
325
  const formatter = new SnapshotFormatter(textSnapshot);
@@ -349,7 +369,7 @@ export class McpResponse {
349
369
  const formatter = new IssueFormatter(message, {
350
370
  id: consoleMessageStableId,
351
371
  requestIdResolver: context.resolveCdpRequestId.bind(context, this.#page),
352
- elementIdResolver: context.resolveCdpElementId.bind(context, this.#page),
372
+ elementIdResolver: this.#page.resolveCdpElementId.bind(this.#page),
353
373
  });
354
374
  if (!formatter.isValid()) {
355
375
  throw new Error("Can't provide details for the msgid " + consoleMessageStableId);
@@ -451,6 +471,7 @@ export class McpResponse {
451
471
  lighthouseResult: this.#attachedLighthouseResult,
452
472
  inPageTools,
453
473
  webmcpTools,
474
+ errorMessage: this.#error?.message,
454
475
  });
455
476
  }
456
477
  format(toolName, context, data) {
@@ -641,6 +662,17 @@ Call ${handleDialog.name} to handle it before continuing.`);
641
662
  response.push(formatter.toString());
642
663
  structuredContent.heapSnapshotData = formatter.toJSON();
643
664
  }
665
+ const nodes = this.#heapSnapshotOptions.nodes;
666
+ if (nodes) {
667
+ const sortedItems = nodes.items
668
+ .filter(isNodeLike)
669
+ .sort((a, b) => b.retainedSize - a.retainedSize);
670
+ const paginationData = this.#dataWithPagination(sortedItems, this.#heapSnapshotOptions.pagination);
671
+ response.push(HeapSnapshotFormatter.formatNodes(paginationData.items));
672
+ structuredContent.pagination = paginationData.pagination;
673
+ response.push(...paginationData.info);
674
+ structuredContent.heapSnapshotNodes = paginationData.items;
675
+ }
644
676
  }
645
677
  if (data.detailedNetworkRequest) {
646
678
  response.push(data.detailedNetworkRequest.toStringDetailed());
@@ -729,16 +761,21 @@ Call ${handleDialog.name} to handle it before continuing.`);
729
761
  const messages = data.consoleMessages ?? [];
730
762
  response.push('## Console messages');
731
763
  if (messages.length) {
732
- const paginationData = this.#dataWithPagination(messages, this.#consoleDataOptions.pagination);
764
+ const grouped = ConsoleFormatter.groupConsecutive(messages);
765
+ const paginationData = this.#dataWithPagination(grouped, this.#consoleDataOptions.pagination);
733
766
  structuredContent.pagination = paginationData.pagination;
734
767
  response.push(...paginationData.info);
735
- response.push(...paginationData.items.map(message => message.toString()));
736
- structuredContent.consoleMessages = paginationData.items.map(message => message.toJSON());
768
+ response.push(...paginationData.items.map(item => item.toString()));
769
+ structuredContent.consoleMessages = paginationData.items.map(item => item.toJSON());
737
770
  }
738
771
  else {
739
772
  response.push('<no console messages found>');
740
773
  }
741
774
  }
775
+ if (data.errorMessage) {
776
+ response.push(`Error: ${data.errorMessage}`);
777
+ structuredContent.errorMessage = data.errorMessage;
778
+ }
742
779
  const text = {
743
780
  type: 'text',
744
781
  text: response.join('\n'),
@@ -800,3 +837,4 @@ function createStructuredPage(page, context) {
800
837
  }
801
838
  return entry;
802
839
  }
840
+ //# sourceMappingURL=McpResponse.js.map
@@ -35,3 +35,4 @@ export class Mutex {
35
35
  resolve();
36
36
  }
37
37
  }
38
+ //# sourceMappingURL=Mutex.js.map