@nimbus21.ai/chrome-devtools-mcp 0.12.3 → 0.17.4

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 (53) hide show
  1. package/README.md +119 -24
  2. package/build/src/DevtoolsUtils.js +174 -42
  3. package/build/src/McpContext.js +127 -29
  4. package/build/src/McpResponse.js +245 -159
  5. package/build/src/PageCollector.js +18 -2
  6. package/build/src/WaitForHelper.js +2 -2
  7. package/build/src/browser.js +8 -13
  8. package/build/src/cli.js +65 -3
  9. package/build/src/formatters/ConsoleFormatter.js +240 -0
  10. package/build/src/formatters/IssueFormatter.js +190 -0
  11. package/build/src/formatters/NetworkFormatter.js +218 -0
  12. package/build/src/formatters/SnapshotFormatter.js +134 -0
  13. package/build/src/logger.js +9 -0
  14. package/build/src/main.js +66 -8
  15. package/build/src/telemetry/ClearcutLogger.js +102 -0
  16. package/build/src/telemetry/WatchdogClient.js +60 -0
  17. package/build/src/telemetry/flagUtils.js +45 -0
  18. package/build/src/telemetry/metricUtils.js +14 -0
  19. package/build/src/telemetry/persistence.js +53 -0
  20. package/build/src/telemetry/types.js +33 -0
  21. package/build/src/telemetry/watchdog/ClearcutSender.js +201 -0
  22. package/build/src/telemetry/watchdog/main.js +127 -0
  23. package/build/src/third_party/THIRD_PARTY_NOTICES +10 -9
  24. package/build/src/third_party/bundled-packages.json +8 -0
  25. package/build/src/third_party/devtools-formatter-worker.js +15449 -0
  26. package/build/src/third_party/index.js +4093 -2367
  27. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidAllowlistItemType.md +12 -0
  28. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidHeader.md +12 -0
  29. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidUrlPattern.md +8 -0
  30. package/build/src/third_party/issue-descriptions/connectionAllowlistItemNotInnerList.md +12 -0
  31. package/build/src/third_party/issue-descriptions/connectionAllowlistMoreThanOneList.md +7 -0
  32. package/build/src/third_party/issue-descriptions/connectionAllowlistReportingEndpointNotToken.md +10 -0
  33. package/build/src/tools/categories.js +2 -0
  34. package/build/src/tools/emulation.js +83 -1
  35. package/build/src/tools/extensions.js +79 -0
  36. package/build/src/tools/input.js +93 -13
  37. package/build/src/tools/network.js +17 -3
  38. package/build/src/tools/pages.js +134 -54
  39. package/build/src/tools/performance.js +68 -27
  40. package/build/src/tools/script.js +2 -2
  41. package/build/src/tools/tools.js +2 -0
  42. package/build/src/utils/ExtensionRegistry.js +35 -0
  43. package/build/src/utils/string.js +36 -0
  44. package/package.json +17 -16
  45. package/build/src/formatters/consoleFormatter.js +0 -121
  46. package/build/src/formatters/networkFormatter.js +0 -77
  47. package/build/src/formatters/snapshotFormatter.js +0 -73
  48. package/build/src/third_party/devtools.js +0 -6
  49. package/build/src/third_party/issue-descriptions/SameSiteInvalidSameParty.md +0 -8
  50. package/build/src/third_party/issue-descriptions/SameSiteSamePartyCrossPartyContextSet.md +0 -10
  51. package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataHttpNotFound.md +0 -1
  52. package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataInvalidResponse.md +0 -1
  53. package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataNoResponse.md +0 -1
@@ -6,12 +6,13 @@
6
6
  import fs from 'node:fs/promises';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
- import { extractUrlLikeFromDevToolsTitle, urlsEqual } from './DevtoolsUtils.js';
9
+ import { extractUrlLikeFromDevToolsTitle, UniverseManager, urlsEqual, } from './DevtoolsUtils.js';
10
10
  import { NetworkCollector, ConsoleCollector } from './PageCollector.js';
11
11
  import { Locator } from './third_party/index.js';
12
12
  import { listPages } from './tools/pages.js';
13
13
  import { takeSnapshot } from './tools/snapshot.js';
14
14
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
15
+ import { ExtensionRegistry, } from './utils/ExtensionRegistry.js';
15
16
  import { WaitForHelper } from './WaitForHelper.js';
16
17
  const DEFAULT_TIMEOUT = 5_000;
17
18
  const NAVIGATION_TIMEOUT = 10_000;
@@ -51,15 +52,23 @@ export class McpContext {
51
52
  #textSnapshot = null;
52
53
  #networkCollector;
53
54
  #consoleCollector;
55
+ #devtoolsUniverseManager;
56
+ #extensionRegistry = new ExtensionRegistry();
54
57
  #isRunningTrace = false;
55
58
  #networkConditionsMap = new WeakMap();
56
59
  #cpuThrottlingRateMap = new WeakMap();
57
60
  #geolocationMap = new WeakMap();
61
+ #viewportMap = new WeakMap();
62
+ #userAgentMap = new WeakMap();
63
+ #colorSchemeMap = new WeakMap();
58
64
  #dialog;
65
+ #pageIdMap = new WeakMap();
66
+ #nextPageId = 1;
59
67
  #nextSnapshotId = 1;
60
68
  #traceResults = [];
61
69
  #locatorClass;
62
70
  #options;
71
+ #uniqueBackendNodeIdToMcpId = new Map();
63
72
  constructor(browser, logger, options, locatorClass) {
64
73
  this.browser = browser;
65
74
  this.logger = logger;
@@ -71,30 +80,26 @@ export class McpContext {
71
80
  console: event => {
72
81
  collect(event);
73
82
  },
74
- pageerror: event => {
75
- if (event instanceof Error) {
76
- collect(event);
77
- }
78
- else {
79
- const error = new Error(`${event}`);
80
- error.stack = undefined;
81
- collect(error);
82
- }
83
+ uncaughtError: event => {
84
+ collect(event);
83
85
  },
84
86
  issue: event => {
85
87
  collect(event);
86
88
  },
87
89
  };
88
90
  });
91
+ this.#devtoolsUniverseManager = new UniverseManager(this.browser);
89
92
  }
90
93
  async #init() {
91
94
  const pages = await this.createPagesSnapshot();
92
95
  await this.#networkCollector.init(pages);
93
96
  await this.#consoleCollector.init(pages);
97
+ await this.#devtoolsUniverseManager.init(pages);
94
98
  }
95
99
  dispose() {
96
100
  this.#networkCollector.dispose();
97
101
  this.#consoleCollector.dispose();
102
+ this.#devtoolsUniverseManager.dispose();
98
103
  }
99
104
  static async from(browser, logger, opts,
100
105
  /* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */
@@ -149,25 +154,28 @@ export class McpContext {
149
154
  const page = this.getSelectedPage();
150
155
  return this.#consoleCollector.getData(page, includePreservedMessages);
151
156
  }
157
+ getDevToolsUniverse() {
158
+ return this.#devtoolsUniverseManager.get(this.getSelectedPage());
159
+ }
152
160
  getConsoleMessageStableId(message) {
153
161
  return this.#consoleCollector.getIdForResource(message);
154
162
  }
155
163
  getConsoleMessageById(id) {
156
164
  return this.#consoleCollector.getById(this.getSelectedPage(), id);
157
165
  }
158
- async newPage() {
159
- const page = await this.browser.newPage();
166
+ async newPage(background) {
167
+ const page = await this.browser.newPage({ background });
160
168
  await this.createPagesSnapshot();
161
169
  this.selectPage(page);
162
170
  this.#networkCollector.addPage(page);
163
171
  this.#consoleCollector.addPage(page);
164
172
  return page;
165
173
  }
166
- async closePage(pageIdx) {
174
+ async closePage(pageId) {
167
175
  if (this.#pages.length === 1) {
168
176
  throw new Error(CLOSE_PAGE_ERROR);
169
177
  }
170
- const page = this.getPageByIdx(pageIdx);
178
+ const page = this.getPageById(pageId);
171
179
  await page.close({ runBeforeUnload: false });
172
180
  }
173
181
  getNetworkRequestById(reqid) {
@@ -209,12 +217,54 @@ export class McpContext {
209
217
  const page = this.getSelectedPage();
210
218
  return this.#geolocationMap.get(page) ?? null;
211
219
  }
220
+ setViewport(viewport) {
221
+ const page = this.getSelectedPage();
222
+ if (viewport === null) {
223
+ this.#viewportMap.delete(page);
224
+ }
225
+ else {
226
+ this.#viewportMap.set(page, viewport);
227
+ }
228
+ }
229
+ getViewport() {
230
+ const page = this.getSelectedPage();
231
+ return this.#viewportMap.get(page) ?? null;
232
+ }
233
+ setUserAgent(userAgent) {
234
+ const page = this.getSelectedPage();
235
+ if (userAgent === null) {
236
+ this.#userAgentMap.delete(page);
237
+ }
238
+ else {
239
+ this.#userAgentMap.set(page, userAgent);
240
+ }
241
+ }
242
+ getUserAgent() {
243
+ const page = this.getSelectedPage();
244
+ return this.#userAgentMap.get(page) ?? null;
245
+ }
246
+ setColorScheme(scheme) {
247
+ const page = this.getSelectedPage();
248
+ if (scheme === null) {
249
+ this.#colorSchemeMap.delete(page);
250
+ }
251
+ else {
252
+ this.#colorSchemeMap.set(page, scheme);
253
+ }
254
+ }
255
+ getColorScheme() {
256
+ const page = this.getSelectedPage();
257
+ return this.#colorSchemeMap.get(page) ?? null;
258
+ }
212
259
  setIsRunningPerformanceTrace(x) {
213
260
  this.#isRunningTrace = x;
214
261
  }
215
262
  isRunningPerformanceTrace() {
216
263
  return this.#isRunningTrace;
217
264
  }
265
+ isCruxEnabled() {
266
+ return this.#options.performanceCrux;
267
+ }
218
268
  getDialog() {
219
269
  return this.#dialog;
220
270
  }
@@ -231,14 +281,16 @@ export class McpContext {
231
281
  }
232
282
  return page;
233
283
  }
234
- getPageByIdx(idx) {
235
- const pages = this.#pages;
236
- const page = pages[idx];
284
+ getPageById(pageId) {
285
+ const page = this.#pages.find(p => this.#pageIdMap.get(p) === pageId);
237
286
  if (!page) {
238
287
  throw new Error('No page found');
239
288
  }
240
289
  return page;
241
290
  }
291
+ getPageId(page) {
292
+ return this.#pageIdMap.get(page);
293
+ }
242
294
  #dialogHandler = (dialog) => {
243
295
  this.#dialog = dialog;
244
296
  };
@@ -283,25 +335,34 @@ export class McpContext {
283
335
  if (!this.#textSnapshot?.idToNode.size) {
284
336
  throw new Error(`No snapshot found. Use ${takeSnapshot.name} to capture one.`);
285
337
  }
286
- const [snapshotId] = uid.split('_');
287
- if (this.#textSnapshot.snapshotId !== snapshotId) {
288
- throw new Error('This uid is coming from a stale snapshot. Call take_snapshot to get a fresh snapshot.');
289
- }
290
338
  const node = this.#textSnapshot?.idToNode.get(uid);
291
339
  if (!node) {
292
- throw new Error('No such element found in the snapshot');
340
+ throw new Error('No such element found in the snapshot.');
293
341
  }
294
- const handle = await node.elementHandle();
295
- if (!handle) {
296
- throw new Error('No such element found in the snapshot');
342
+ const message = `Element with uid ${uid} no longer exists on the page.`;
343
+ try {
344
+ const handle = await node.elementHandle();
345
+ if (!handle) {
346
+ throw new Error(message);
347
+ }
348
+ return handle;
349
+ }
350
+ catch (error) {
351
+ throw new Error(message, {
352
+ cause: error,
353
+ });
297
354
  }
298
- return handle;
299
355
  }
300
356
  /**
301
357
  * Creates a snapshot of the pages.
302
358
  */
303
359
  async createPagesSnapshot() {
304
360
  const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
361
+ for (const page of allPages) {
362
+ if (!this.#pageIdMap.has(page)) {
363
+ this.#pageIdMap.set(page, this.#nextPageId++);
364
+ }
365
+ }
305
366
  this.#pages = allPages.filter(page => {
306
367
  // If we allow debugging DevTools windows, return all pages.
307
368
  // If we are in regular mode, the user should only see non-DevTools page.
@@ -396,10 +457,24 @@ export class McpContext {
396
457
  // will be used for the tree serialization and mapping ids back to nodes.
397
458
  let idCounter = 0;
398
459
  const idToNode = new Map();
460
+ const seenUniqueIds = new Set();
399
461
  const assignIds = (node) => {
462
+ let id = '';
463
+ // @ts-expect-error untyped loaderId & backendNodeId.
464
+ const uniqueBackendId = `${node.loaderId}_${node.backendNodeId}`;
465
+ if (this.#uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
466
+ // Re-use MCP exposed ID if the uniqueId is the same.
467
+ id = this.#uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
468
+ }
469
+ else {
470
+ // Only generate a new ID if we have not seen the node before.
471
+ id = `${snapshotId}_${idCounter++}`;
472
+ this.#uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
473
+ }
474
+ seenUniqueIds.add(uniqueBackendId);
400
475
  const nodeWithId = {
401
476
  ...node,
402
- id: `${snapshotId}_${idCounter++}`,
477
+ id,
403
478
  children: node.children
404
479
  ? node.children.map(child => assignIds(child))
405
480
  : [],
@@ -428,6 +503,12 @@ export class McpContext {
428
503
  this.#textSnapshot.hasSelectedElement = true;
429
504
  this.#textSnapshot.selectedElementUid = this.resolveCdpElementId(data?.cdpBackendNodeId);
430
505
  }
506
+ // Clean up unique IDs that we did not see anymore.
507
+ for (const key of this.#uniqueBackendNodeIdToMcpId.keys()) {
508
+ if (!seenUniqueIds.has(key)) {
509
+ this.#uniqueBackendNodeIdToMcpId.delete(key);
510
+ }
511
+ }
431
512
  }
432
513
  getTextSnapshot() {
433
514
  return this.#textSnapshot;
@@ -456,6 +537,8 @@ export class McpContext {
456
537
  }
457
538
  }
458
539
  storeTraceRecording(result) {
540
+ // Clear the trace results because we only consume the latest trace currently.
541
+ this.#traceResults = [];
459
542
  this.#traceResults.push(result);
460
543
  }
461
544
  recordedTraces() {
@@ -464,12 +547,12 @@ export class McpContext {
464
547
  getWaitForHelper(page, cpuMultiplier, networkMultiplier) {
465
548
  return new WaitForHelper(page, cpuMultiplier, networkMultiplier);
466
549
  }
467
- waitForEventsAfterAction(action) {
550
+ waitForEventsAfterAction(action, options) {
468
551
  const page = this.getSelectedPage();
469
552
  const cpuMultiplier = this.getCpuThrottlingRate();
470
553
  const networkMultiplier = getNetworkMultiplierFromString(this.getNetworkConditions());
471
554
  const waitForHelper = this.getWaitForHelper(page, cpuMultiplier, networkMultiplier);
472
- return waitForHelper.waitForEventsAfterAction(action);
555
+ return waitForHelper.waitForEventsAfterAction(action, options);
473
556
  }
474
557
  getNetworkRequestStableId(request) {
475
558
  return this.#networkCollector.getIdForResource(request);
@@ -502,4 +585,19 @@ export class McpContext {
502
585
  });
503
586
  await this.#networkCollector.init(await this.browser.pages());
504
587
  }
588
+ async installExtension(extensionPath) {
589
+ const id = await this.browser.installExtension(extensionPath);
590
+ await this.#extensionRegistry.registerExtension(id, extensionPath);
591
+ return id;
592
+ }
593
+ async uninstallExtension(id) {
594
+ await this.browser.uninstallExtension(id);
595
+ this.#extensionRegistry.remove(id);
596
+ }
597
+ listExtensions() {
598
+ return this.#extensionRegistry.list();
599
+ }
600
+ getExtension(id) {
601
+ return this.#extensionRegistry.getById(id);
602
+ }
505
603
  }