chrome-devtools-mcp 1.1.1 → 1.2.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 (105) hide show
  1. package/README.md +12 -3
  2. package/build/src/DevtoolsUtils.js +57 -0
  3. package/build/src/HeapSnapshotManager.js +5 -0
  4. package/build/src/McpContext.js +28 -8
  5. package/build/src/McpPage.js +9 -9
  6. package/build/src/McpResponse.js +101 -53
  7. package/build/src/PageCollector.js +7 -7
  8. package/build/src/ServiceWorkerCollector.js +171 -0
  9. package/build/src/TextSnapshot.js +1 -1
  10. package/build/src/ToolHandler.js +10 -4
  11. package/build/src/WaitForHelper.js +2 -2
  12. package/build/src/bin/chrome-devtools-cli-options.js +22 -4
  13. package/build/src/bin/chrome-devtools-mcp-cli-options.js +19 -4
  14. package/build/src/bin/chrome-devtools-mcp-main.js +5 -5
  15. package/build/src/browser.js +8 -4
  16. package/build/src/daemon/client.js +7 -7
  17. package/build/src/daemon/daemon.js +12 -12
  18. package/build/src/daemon/utils.js +2 -2
  19. package/build/src/formatters/IssueFormatter.js +4 -4
  20. package/build/src/index.js +13 -1
  21. package/build/src/telemetry/ClearcutLogger.js +1 -1
  22. package/build/src/telemetry/WatchdogClient.js +4 -4
  23. package/build/src/telemetry/persistence.js +2 -2
  24. package/build/src/telemetry/watchdog/ClearcutSender.js +10 -10
  25. package/build/src/telemetry/watchdog/main.js +5 -5
  26. package/build/src/third_party/THIRD_PARTY_NOTICES +30 -0
  27. package/build/src/third_party/bundled-packages.json +2 -1
  28. package/build/src/third_party/devtools-formatter-worker.js +1 -0
  29. package/build/src/third_party/devtools-heap-snapshot-worker.js +107 -0
  30. package/build/src/third_party/index.js +5906 -4913
  31. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsEmptyList.md +1 -0
  32. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsHttpNotFound.md +1 -0
  33. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidContentType.md +1 -0
  34. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidResponse.md +1 -0
  35. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsNoResponse.md +1 -0
  36. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsFetchFailed.md +1 -0
  37. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsInvalidRecord.md +1 -0
  38. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownHttpNotFound.md +1 -0
  39. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidContentType.md +1 -0
  40. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidResponse.md +1 -0
  41. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownNoResponse.md +1 -0
  42. package/build/src/third_party/issue-descriptions/emailVerificationRequestInvalidEmail.md +1 -0
  43. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksHttpNotFound.md +1 -0
  44. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksInvalidResponse.md +1 -0
  45. package/build/src/third_party/issue-descriptions/emailVerificationRequestKeyBindingSigningFailed.md +1 -0
  46. package/build/src/third_party/issue-descriptions/emailVerificationRequestRpOriginIsOpaque.md +1 -0
  47. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenHttpNotFound.md +1 -0
  48. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidContentType.md +1 -0
  49. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidResponse.md +1 -0
  50. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidSdJwt.md +1 -0
  51. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenMalformedSdJwt.md +1 -0
  52. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenNoResponse.md +1 -0
  53. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidAudience.md +1 -0
  54. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidIssuedAt.md +1 -0
  55. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidNonce.md +1 -0
  56. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidSdHash.md +1 -0
  57. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidTyp.md +1 -0
  58. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingAud.md +1 -0
  59. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingCnf.md +1 -0
  60. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingIat.md +1 -0
  61. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingNonce.md +1 -0
  62. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingSdHash.md +1 -0
  63. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbSignatureFailed.md +1 -0
  64. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmail.md +1 -0
  65. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmailVerified.md +1 -0
  66. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidHolderKey.md +1 -0
  67. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuedAt.md +1 -0
  68. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuer.md +1 -0
  69. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtJwksMissingKeys.md +1 -0
  70. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingCnf.md +1 -0
  71. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingEmail.md +1 -0
  72. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIat.md +1 -0
  73. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIss.md +1 -0
  74. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtSignatureFailed.md +1 -0
  75. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtUnsupportedHeaderAlg.md +1 -0
  76. package/build/src/third_party/issue-descriptions/emailVerificationRequestUserLoggedOut.md +1 -0
  77. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownAccountsEndpointCrossOrigin.md +1 -0
  78. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownHttpNotFound.md +1 -0
  79. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidContentType.md +1 -0
  80. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidResponse.md +1 -0
  81. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownIssuanceEndpointCrossOrigin.md +1 -0
  82. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownListEmpty.md +1 -0
  83. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingAccountsEndpoint.md +1 -0
  84. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingIssuanceEndpoint.md +1 -0
  85. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownNoResponse.md +1 -0
  86. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownUnsupportedSigningAlgorithm.md +1 -0
  87. package/build/src/tools/console.js +7 -0
  88. package/build/src/tools/emulation.js +1 -0
  89. package/build/src/tools/extensions.js +5 -2
  90. package/build/src/tools/input.js +11 -3
  91. package/build/src/tools/lighthouse.js +11 -6
  92. package/build/src/tools/memory.js +33 -10
  93. package/build/src/tools/network.js +2 -2
  94. package/build/src/tools/pages.js +13 -5
  95. package/build/src/tools/performance.js +8 -7
  96. package/build/src/tools/screencast.js +2 -1
  97. package/build/src/tools/screenshot.js +1 -1
  98. package/build/src/tools/script.js +1 -1
  99. package/build/src/tools/slim/tools.js +3 -0
  100. package/build/src/tools/snapshot.js +3 -2
  101. package/build/src/tools/thirdPartyDeveloper.js +12 -2
  102. package/build/src/tools/webmcp.js +2 -0
  103. package/build/src/trace-processing/parse.js +5 -5
  104. package/build/src/version.js +1 -1
  105. package/package.json +4 -3
package/README.md CHANGED
@@ -514,8 +514,9 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
514
514
  - [`take_snapshot`](docs/tool-reference.md#take_snapshot)
515
515
  - [`screencast_start`](docs/tool-reference.md#screencast_start)
516
516
  - [`screencast_stop`](docs/tool-reference.md#screencast_stop)
517
- - **Memory** (5 tools)
517
+ - **Memory** (6 tools)
518
518
  - [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
519
+ - [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
519
520
  - [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
520
521
  - [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
521
522
  - [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
@@ -608,8 +609,8 @@ The Chrome DevTools MCP server supports the following configuration option:
608
609
  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.
609
610
  - **Type:** boolean
610
611
 
611
- - **`--experimentalMemory`/ `--experimental-memory`**
612
- Whether to enable experimental memory tools.
612
+ - **`--memoryDebugging`/ `--memory-debugging`, `-experimentalMemory`**
613
+ Whether to enable memory debugging tools.
613
614
  - **Type:** boolean
614
615
 
615
616
  - **`--experimentalStructuredContent`/ `--experimental-structured-content`**
@@ -636,6 +637,14 @@ The Chrome DevTools MCP server supports the following configuration option:
636
637
  Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
637
638
  - **Type:** array
638
639
 
640
+ - **`--blockedUrlPattern`/ `--blocked-url-pattern`**
641
+ Restricts network access by blocking specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.
642
+ - **Type:** array
643
+
644
+ - **`--allowedUrlPattern`/ `--allowed-url-pattern`**
645
+ Restricts network access by allowing only specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Requires Chrome 149+. Silently detaches from targets with unallowed URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.
646
+ - **Type:** array
647
+
639
648
  - **`--ignoreDefaultChromeArg`/ `--ignore-default-chrome-arg`**
640
649
  Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
641
650
  - **Type:** array
@@ -18,6 +18,63 @@ export class FakeIssuesManager extends DevTools.Common.ObjectWrapper
18
18
  }
19
19
  // DevTools CDP errors can get noisy.
20
20
  DevTools.ProtocolClient.InspectorBackend.test.suppressRequestErrors = true;
21
+ // Stub out Network emulation commands on the DevTools Agent prototype globally.
22
+ // This prevents the DevTools Frontend from ever resetting/clearing Puppeteer's
23
+ // active network blocking/throttling rules during target setup or session lifetime.
24
+ const networkAgentPrototype = DevTools.ProtocolClient.InspectorBackend.inspectorBackend.agentPrototypes.get('Network');
25
+ if (networkAgentPrototype) {
26
+ Object.defineProperty(networkAgentPrototype, 'invoke_emulateNetworkConditionsByRule', {
27
+ value: () => {
28
+ return Promise.resolve({
29
+ ruleIds: [],
30
+ getError: () => undefined,
31
+ });
32
+ },
33
+ writable: true,
34
+ configurable: true,
35
+ enumerable: true,
36
+ });
37
+ Object.defineProperty(networkAgentPrototype, 'invoke_overrideNetworkState', {
38
+ value: () => {
39
+ return Promise.resolve({
40
+ getError: () => undefined,
41
+ });
42
+ },
43
+ writable: true,
44
+ configurable: true,
45
+ enumerable: true,
46
+ });
47
+ Object.defineProperty(networkAgentPrototype, 'invoke_enable', {
48
+ value: () => {
49
+ return Promise.resolve({
50
+ getError: () => undefined,
51
+ });
52
+ },
53
+ writable: true,
54
+ configurable: true,
55
+ enumerable: true,
56
+ });
57
+ Object.defineProperty(networkAgentPrototype, 'invoke_disable', {
58
+ value: () => {
59
+ return Promise.resolve({
60
+ getError: () => undefined,
61
+ });
62
+ },
63
+ writable: true,
64
+ configurable: true,
65
+ enumerable: true,
66
+ });
67
+ Object.defineProperty(networkAgentPrototype, 'invoke_setBlockedURLs', {
68
+ value: () => {
69
+ return Promise.resolve({
70
+ getError: () => undefined,
71
+ });
72
+ },
73
+ writable: true,
74
+ configurable: true,
75
+ enumerable: true,
76
+ });
77
+ }
21
78
  DevTools.I18n.DevToolsLocale.DevToolsLocale.instance({
22
79
  create: true,
23
80
  data: {
@@ -122,13 +122,18 @@ export class HeapSnapshotManager {
122
122
  const snapshot = await snapshotPromise;
123
123
  return { snapshot, worker: workerProxy };
124
124
  }
125
+ hasSnapshots() {
126
+ return this.#snapshots.size > 0;
127
+ }
125
128
  dispose(filePath) {
126
129
  const absolutePath = path.resolve(filePath);
127
130
  const cached = this.#snapshots.get(absolutePath);
128
131
  if (cached) {
129
132
  cached.worker.dispose();
130
133
  this.#snapshots.delete(absolutePath);
134
+ return true;
131
135
  }
136
+ return false;
132
137
  }
133
138
  }
134
139
  //# sourceMappingURL=HeapSnapshotManager.js.map
@@ -12,6 +12,7 @@ import { UniverseManager } from './DevtoolsUtils.js';
12
12
  import { HeapSnapshotManager } from './HeapSnapshotManager.js';
13
13
  import { McpPage } from './McpPage.js';
14
14
  import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
15
+ import { ServiceWorkerConsoleCollector } from './ServiceWorkerCollector.js';
15
16
  import { Locator, PredefinedNetworkConditions, } from './third_party/index.js';
16
17
  import { listPages } from './tools/pages.js';
17
18
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
@@ -33,6 +34,7 @@ export class McpContext {
33
34
  #networkCollector;
34
35
  #consoleCollector;
35
36
  #devtoolsUniverseManager;
37
+ #serviceWorkerConsoleCollector;
36
38
  #isRunningTrace = false;
37
39
  #screenRecorderData = null;
38
40
  #nextPageId = 1;
@@ -63,19 +65,22 @@ export class McpContext {
63
65
  },
64
66
  };
65
67
  });
68
+ this.#serviceWorkerConsoleCollector = new ServiceWorkerConsoleCollector(this.browser);
66
69
  this.#devtoolsUniverseManager = new UniverseManager(this.browser);
67
70
  }
68
71
  async #init() {
69
72
  const pages = await this.createPagesSnapshot();
70
- await this.createExtensionServiceWorkersSnapshot();
73
+ const workers = await this.createExtensionServiceWorkersSnapshot();
71
74
  await this.#networkCollector.init(pages);
72
75
  await this.#consoleCollector.init(pages);
73
76
  await this.#devtoolsUniverseManager.init(pages);
77
+ await this.#serviceWorkerConsoleCollector.init(workers);
74
78
  }
75
79
  dispose() {
76
80
  this.#networkCollector.dispose();
77
81
  this.#consoleCollector.dispose();
78
82
  this.#devtoolsUniverseManager.dispose();
83
+ this.#serviceWorkerConsoleCollector.dispose();
79
84
  for (const mcpPage of this.#mcpPages.values()) {
80
85
  mcpPage.dispose();
81
86
  }
@@ -148,7 +153,7 @@ export class McpContext {
148
153
  }
149
154
  resolveCdpRequestId(page, cdpRequestId) {
150
155
  if (!cdpRequestId) {
151
- this.logger('no network request');
156
+ this.logger?.('no network request');
152
157
  return;
153
158
  }
154
159
  const request = this.#networkCollector.find(page.pptrPage, request => {
@@ -156,7 +161,7 @@ export class McpContext {
156
161
  return request.id === cdpRequestId;
157
162
  });
158
163
  if (!request) {
159
- this.logger('no network request for ' + cdpRequestId);
164
+ this.logger?.('no network request for ' + cdpRequestId);
160
165
  return;
161
166
  }
162
167
  return this.#networkCollector.getIdForResource(request);
@@ -217,7 +222,13 @@ export class McpContext {
217
222
  const page = targetPage ?? this.getSelectedPptrPage();
218
223
  const mcpPage = this.#getMcpPage(page);
219
224
  const newSettings = { ...mcpPage.emulationSettings };
220
- if (!options.networkConditions) {
225
+ // Skip network emulation if blocklist/allowlist is configured, as it conflicts with blocking rules in Puppeteer.
226
+ if (this.#options.hasNetworkBlockOrAllowlist) {
227
+ if (options.networkConditions !== undefined) {
228
+ throw new Error('Network throttling is not supported when network blocking (allowlist/blocklist) is configured.');
229
+ }
230
+ }
231
+ else if (!options.networkConditions) {
221
232
  await page.emulateNetworkConditions(null);
222
233
  delete newSettings.networkConditions;
223
234
  }
@@ -412,6 +423,9 @@ export class McpContext {
412
423
  });
413
424
  return this.#extensionServiceWorkers;
414
425
  }
426
+ getServiceWorkerConsoleData(extensionId) {
427
+ return this.#serviceWorkerConsoleCollector.getData(extensionId);
428
+ }
415
429
  async createPagesSnapshot() {
416
430
  const { pages: allPages, isolatedContextNames } = await this.#getAllPages();
417
431
  for (const page of allPages) {
@@ -421,7 +435,7 @@ export class McpContext {
421
435
  this.#mcpPages.set(page, mcpPage);
422
436
  // We emulate a focused page for all pages to support multi-agent workflows.
423
437
  void page.emulateFocusedPage(true).catch(error => {
424
- this.logger('Error turning on focused page emulation', error);
438
+ this.logger?.('Error turning on focused page emulation', error);
425
439
  });
426
440
  }
427
441
  mcpPage.isolatedContextName = isolatedContextNames.get(page);
@@ -467,7 +481,7 @@ export class McpContext {
467
481
  this.#extensionPages.set(target, page);
468
482
  }
469
483
  catch (e) {
470
- this.logger('Failed to get page for extension target', e);
484
+ this.logger?.('Failed to get page for extension target', e);
471
485
  }
472
486
  }
473
487
  }
@@ -502,7 +516,7 @@ export class McpContext {
502
516
  return { pages: allPages, isolatedContextNames };
503
517
  }
504
518
  async detectOpenDevToolsWindows() {
505
- this.logger('Detecting open DevTools windows');
519
+ this.logger?.('Detecting open DevTools windows');
506
520
  const { pages } = await this.#getAllPages();
507
521
  await Promise.all(pages.map(async (page) => {
508
522
  const mcpPage = this.#mcpPages.get(page);
@@ -557,7 +571,7 @@ export class McpContext {
557
571
  return { filename: filePath };
558
572
  }
559
573
  catch (err) {
560
- this.logger(err);
574
+ this.logger?.(err);
561
575
  throw new Error('Could not save a file', { cause: err });
562
576
  }
563
577
  }
@@ -639,5 +653,11 @@ export class McpContext {
639
653
  async getHeapSnapshotRetainers(filePath, nodeId) {
640
654
  return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
641
655
  }
656
+ async closeHeapSnapshot(filePath) {
657
+ return this.#heapSnapshotManager.dispose(filePath);
658
+ }
659
+ hasHeapSnapshots() {
660
+ return this.#heapSnapshotManager.hasSnapshots();
661
+ }
642
662
  }
643
663
  //# sourceMappingURL=McpContext.js.map
@@ -30,7 +30,7 @@ export class McpPage {
30
30
  // Dialog
31
31
  #dialog;
32
32
  #dialogHandler;
33
- thirdPartyDeveloperTools;
33
+ thirdPartyDeveloperTools = [];
34
34
  constructor(page, id) {
35
35
  this.pptrPage = page;
36
36
  this.id = id;
@@ -194,18 +194,18 @@ export class McpPage {
194
194
  for (const handle of oldHandles) {
195
195
  await handle
196
196
  .dispose()
197
- .catch(e => logger('Failed to dispose old handle', e));
197
+ .catch(e => logger?.('Failed to dispose old handle', e));
198
198
  }
199
199
  }
200
200
  const cdpElementIds = await Promise.all(elementHandles.map(async (elementHandle, index) => {
201
201
  const backendNodeId = await elementHandle.backendNodeId();
202
202
  if (!backendNodeId) {
203
- logger(`No backendNodeId for stashed DOM element with index ${index}`);
203
+ logger?.(`No backendNodeId for stashed DOM element with index ${index}`);
204
204
  return `stashed-${index}`;
205
205
  }
206
206
  const cdpElementId = this.resolveCdpElementId(backendNodeId);
207
207
  if (!cdpElementId) {
208
- logger(`Could not get cdpElementId for backend node ${backendNodeId}`);
208
+ logger?.(`Could not get cdpElementId for backend node ${backendNodeId}`);
209
209
  return `stashed-${index}`;
210
210
  }
211
211
  return cdpElementId;
@@ -263,12 +263,12 @@ export class McpPage {
263
263
  }
264
264
  resolveCdpElementId(cdpBackendNodeId) {
265
265
  if (!cdpBackendNodeId) {
266
- logger('no cdpBackendNodeId');
266
+ logger?.('no cdpBackendNodeId');
267
267
  return;
268
268
  }
269
269
  const snapshot = this.textSnapshot;
270
270
  if (!snapshot) {
271
- logger('no text snapshot');
271
+ logger?.('no text snapshot');
272
272
  return;
273
273
  }
274
274
  // TODO: index by backendNodeId instead.
@@ -286,10 +286,10 @@ export class McpPage {
286
286
  }
287
287
  async getDevToolsData() {
288
288
  try {
289
- logger('Getting DevTools UI data');
289
+ logger?.('Getting DevTools UI data');
290
290
  const devtoolsPage = this.devToolsPage;
291
291
  if (!devtoolsPage) {
292
- logger('No DevTools page detected');
292
+ logger?.('No DevTools page detected');
293
293
  return {};
294
294
  }
295
295
  const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
@@ -307,7 +307,7 @@ export class McpPage {
307
307
  return { cdpBackendNodeId, cdpRequestId };
308
308
  }
309
309
  catch (err) {
310
- logger('error getting devtools data', err);
310
+ logger?.('error getting devtools data', err);
311
311
  }
312
312
  return {};
313
313
  }
@@ -11,7 +11,7 @@ import { NetworkFormatter } from './formatters/NetworkFormatter.js';
11
11
  import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
12
12
  import { UncaughtError } from './PageCollector.js';
13
13
  import { TextSnapshot } from './TextSnapshot.js';
14
- import { DevTools } from './third_party/index.js';
14
+ import { DevTools, toonEncode } from './third_party/index.js';
15
15
  import { handleDialog } from './tools/pages.js';
16
16
  import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
17
17
  import { paginate } from './utils/pagination.js';
@@ -61,7 +61,7 @@ export function replaceHtmlElementsWithUids(schema) {
61
61
  }
62
62
  }
63
63
  }
64
- async function getToolGroup(page) {
64
+ async function getToolGroups(page) {
65
65
  // Check if there is a `devtoolstooldiscovery` event listener
66
66
  const windowHandle = await page.pptrPage.evaluateHandle(() => window);
67
67
  // @ts-expect-error internal API
@@ -70,43 +70,78 @@ async function getToolGroup(page) {
70
70
  objectId: windowHandle.remoteObject().objectId,
71
71
  });
72
72
  if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
73
- return;
73
+ return [];
74
74
  }
75
- const toolGroup = await page.pptrPage.evaluate(() => {
75
+ const toolGroups = await page.pptrPage.evaluate(() => {
76
76
  return new Promise(resolve => {
77
77
  const event = new CustomEvent('devtoolstooldiscovery');
78
+ const groups = [];
78
79
  // @ts-expect-error Adding custom property
79
- event.respondWith = (toolGroup) => {
80
+ event.respondWith = toolGroup => {
80
81
  if (!window.__dtmcp) {
81
82
  window.__dtmcp = {};
82
83
  }
83
- window.__dtmcp.toolGroup = toolGroup;
84
+ if (!window.__dtmcp.toolGroups) {
85
+ window.__dtmcp.toolGroups = [];
86
+ }
87
+ if (typeof toolGroup.name !== 'string' ||
88
+ typeof toolGroup.description !== 'string' ||
89
+ !Array.isArray(toolGroup.tools)) {
90
+ console.error('Invalid toolGroup:', toolGroup);
91
+ return;
92
+ }
93
+ for (const tool of toolGroup.tools) {
94
+ if (typeof tool.name !== 'string' ||
95
+ typeof tool.description !== 'string' ||
96
+ typeof tool.inputSchema !== 'object' ||
97
+ typeof tool.execute !== 'function') {
98
+ console.error('Invalid tool:', tool);
99
+ return;
100
+ }
101
+ }
102
+ window.__dtmcp.toolGroups.push(toolGroup);
84
103
  // When receiving a toolGroup for the first time, expose a simple execution helper
85
104
  if (!window.__dtmcp.executeTool) {
86
105
  window.__dtmcp.executeTool = async (toolName, args) => {
87
- if (!window.__dtmcp?.toolGroup) {
106
+ if (!window.__dtmcp?.toolGroups ||
107
+ window.__dtmcp.toolGroups.length === 0) {
88
108
  throw new Error('No tools found on the page');
89
109
  }
90
- const tool = window.__dtmcp.toolGroup.tools.find(t => t.name === toolName);
91
- if (!tool) {
92
- throw new Error(`Tool ${toolName} not found`);
110
+ for (const group of window.__dtmcp.toolGroups) {
111
+ const tool = group.tools?.find(t => t.name === toolName);
112
+ if (tool) {
113
+ return await tool.execute(args);
114
+ }
93
115
  }
94
- return await tool.execute(args);
116
+ throw new Error(`Tool ${toolName} not found`);
95
117
  };
96
118
  }
97
- resolve(toolGroup);
119
+ groups.push(toolGroup);
98
120
  };
99
121
  window.dispatchEvent(event);
100
- // If the page does not synchronously call `event.respondWith`, return instead of timing out
101
- setTimeout(() => {
102
- resolve(null);
103
- }, 0);
122
+ // If at least one toolGroup was added synchronously, resolve with the array.
123
+ // Otherwise, use setTimeout to allow for any microtask/asynchronous respondWith calls, or resolve with an empty array.
124
+ if (groups.length > 0) {
125
+ resolve(groups);
126
+ }
127
+ else {
128
+ setTimeout(() => {
129
+ if (groups.length > 0) {
130
+ resolve(groups);
131
+ }
132
+ else {
133
+ resolve([]);
134
+ }
135
+ }, 0);
136
+ }
104
137
  });
105
138
  });
106
- for (const tool of toolGroup?.tools ?? []) {
107
- replaceHtmlElementsWithUids(tool.inputSchema);
139
+ for (const group of toolGroups) {
140
+ for (const tool of group.tools ?? []) {
141
+ replaceHtmlElementsWithUids(tool.inputSchema);
142
+ }
108
143
  }
109
- return toolGroup;
144
+ return toolGroups;
110
145
  }
111
146
  export class McpResponse {
112
147
  #includePages = false;
@@ -134,6 +169,9 @@ export class McpResponse {
134
169
  #redactNetworkHeaders = true;
135
170
  #error;
136
171
  #attachedWaitForResult;
172
+ get #deviceScope() {
173
+ return this.#page?.viewport?.isMobile ? 'PHONE' : 'DESKTOP';
174
+ }
137
175
  constructor(args) {
138
176
  this.#args = args;
139
177
  }
@@ -203,6 +241,7 @@ export class McpResponse {
203
241
  : undefined,
204
242
  types: options?.types,
205
243
  includePreservedMessages: options?.includePreservedMessages,
244
+ serviceWorkerId: options?.serviceWorkerId,
206
245
  };
207
246
  }
208
247
  setError(error) {
@@ -306,7 +345,7 @@ export class McpResponse {
306
345
  get listWebMcpTools() {
307
346
  return this.#listWebMcpTools;
308
347
  }
309
- async handle(toolName, context) {
348
+ async handle(toolName, context, useToon = false) {
310
349
  if (this.#includePages) {
311
350
  await context.createPagesSnapshot();
312
351
  }
@@ -383,12 +422,11 @@ export class McpResponse {
383
422
  if (this.#listExtensions) {
384
423
  extensions = await context.listExtensions();
385
424
  }
386
- // Null indicates no tools.
387
- let thirdPartyDeveloperTools;
425
+ let thirdPartyDeveloperTools = [];
388
426
  if (this.#args.categoryExperimentalThirdParty &&
389
427
  this.#listThirdPartyDeveloperTools &&
390
428
  this.#page) {
391
- thirdPartyDeveloperTools = await getToolGroup(this.#page);
429
+ thirdPartyDeveloperTools = await getToolGroups(this.#page);
392
430
  if (thirdPartyDeveloperTools) {
393
431
  this.#page.thirdPartyDeveloperTools = thirdPartyDeveloperTools;
394
432
  }
@@ -401,11 +439,18 @@ export class McpResponse {
401
439
  }
402
440
  let consoleMessages;
403
441
  if (this.#consoleDataOptions?.include) {
404
- if (!this.#page) {
405
- throw new Error(`Response must have an McpPage`);
442
+ let messages;
443
+ let page;
444
+ if (this.#consoleDataOptions.serviceWorkerId) {
445
+ messages = context.getServiceWorkerConsoleData(this.#consoleDataOptions.serviceWorkerId);
446
+ }
447
+ else {
448
+ page = this.#page;
449
+ if (!page) {
450
+ throw new Error(`Response must have an McpPage`);
451
+ }
452
+ messages = context.getConsoleData(page, this.#consoleDataOptions.includePreservedMessages);
406
453
  }
407
- const page = this.#page;
408
- let messages = context.getConsoleData(this.#page, this.#consoleDataOptions.includePreservedMessages);
409
454
  if (this.#consoleDataOptions.types?.length) {
410
455
  const normalizedTypes = new Set(this.#consoleDataOptions.types);
411
456
  messages = messages.filter(message => {
@@ -422,7 +467,9 @@ export class McpResponse {
422
467
  const consoleMessageStableId = context.getConsoleMessageStableId(item);
423
468
  if ('args' in item || item instanceof UncaughtError) {
424
469
  const consoleMessage = item;
425
- const devTools = context.getDevToolsUniverse(page);
470
+ const devTools = page
471
+ ? context.getDevToolsUniverse(page)
472
+ : null;
426
473
  return await ConsoleFormatter.from(consoleMessage, {
427
474
  id: consoleMessageStableId,
428
475
  fetchDetailedData: false,
@@ -479,9 +526,9 @@ export class McpResponse {
479
526
  thirdPartyDeveloperTools,
480
527
  webmcpTools,
481
528
  errorMessage: this.#error?.message,
482
- });
529
+ }, useToon);
483
530
  }
484
- format(toolName, context, data) {
531
+ format(toolName, context, data, useToon) {
485
532
  const structuredContent = {};
486
533
  const response = [];
487
534
  if (this.#textResponseLines.length) {
@@ -603,7 +650,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
603
650
  structuredContent.tabId = this.#tabId;
604
651
  }
605
652
  if (data.traceSummary) {
606
- const summary = getTraceSummary(data.traceSummary);
653
+ const summary = getTraceSummary(data.traceSummary, this.#deviceScope);
607
654
  response.push(summary);
608
655
  structuredContent.traceSummary = summary;
609
656
  structuredContent.traceInsights = [];
@@ -617,7 +664,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
617
664
  }
618
665
  }
619
666
  if (data.traceInsight) {
620
- const insightOutput = getInsightOutput(data.traceInsight.trace, data.traceInsight.insightSetId, data.traceInsight.insightName);
667
+ const insightOutput = getInsightOutput(data.traceInsight.trace, data.traceInsight.insightSetId, data.traceInsight.insightName, this.#deviceScope);
621
668
  if ('error' in insightOutput) {
622
669
  response.push(insightOutput.error);
623
670
  }
@@ -651,9 +698,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
651
698
  structuredContent.snapshotFilePath = data.snapshot;
652
699
  }
653
700
  else {
654
- response.push('## Latest page snapshot');
655
- response.push(data.snapshot.toString());
656
701
  structuredContent.snapshot = data.snapshot.toJSON();
702
+ response.push('## Latest page snapshot');
703
+ response.push(useToon
704
+ ? toonEncode(structuredContent.snapshot)
705
+ : data.snapshot.toString());
657
706
  }
658
707
  }
659
708
  if (this.#heapSnapshotOptions?.include) {
@@ -678,8 +727,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
678
727
  response.push(...paginationData.info);
679
728
  const paginatedRecord = Object.fromEntries(paginationData.items);
680
729
  const formatter = new HeapSnapshotFormatter(paginatedRecord);
681
- response.push(formatter.toString());
682
730
  structuredContent.heapSnapshotData = formatter.toJSON();
731
+ response.push(useToon
732
+ ? toonEncode(structuredContent.heapSnapshotData)
733
+ : formatter.toString());
683
734
  }
684
735
  const nodes = this.#heapSnapshotOptions.nodes;
685
736
  if (nodes) {
@@ -728,18 +779,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
728
779
  response.push(extensionsMessage);
729
780
  }
730
781
  }
731
- if (data.thirdPartyDeveloperTools !== undefined) {
732
- if (data.thirdPartyDeveloperTools) {
733
- structuredContent.thirdPartyDeveloperTools =
734
- data.thirdPartyDeveloperTools;
735
- }
782
+ if (data.thirdPartyDeveloperTools.length) {
783
+ structuredContent.thirdPartyDeveloperTools =
784
+ data.thirdPartyDeveloperTools;
736
785
  response.push('## Third-party developer tools');
737
- if (data.thirdPartyDeveloperTools === null ||
738
- !data.thirdPartyDeveloperTools?.tools) {
739
- response.push('No third-party developer tools available.');
740
- }
741
- else {
742
- const toolGroup = data.thirdPartyDeveloperTools;
786
+ for (const toolGroup of data.thirdPartyDeveloperTools) {
743
787
  response.push(`${toolGroup.name}: ${toolGroup.description}`);
744
788
  response.push('Available tools:');
745
789
  const toolDefinitionsMessage = toolGroup.tools
@@ -778,11 +822,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
778
822
  structuredContent.pagination = paginationData.pagination;
779
823
  response.push(...paginationData.info);
780
824
  if (data.networkRequests) {
781
- structuredContent.networkRequests = [];
782
- for (const formatter of paginationData.items) {
783
- response.push(formatter.toString());
784
- structuredContent.networkRequests.push(formatter.toJSON());
785
- }
825
+ structuredContent.networkRequests = paginationData.items.map(i => i.toJSON());
826
+ response.push(...(useToon
827
+ ? [toonEncode(structuredContent.networkRequests)]
828
+ : paginationData.items.map(i => i.toString())));
786
829
  }
787
830
  }
788
831
  else {
@@ -796,9 +839,14 @@ Call ${handleDialog.name} to handle it before continuing.`);
796
839
  const grouped = ConsoleFormatter.groupConsecutive(messages);
797
840
  const paginationData = this.#dataWithPagination(grouped, this.#consoleDataOptions.pagination);
798
841
  structuredContent.pagination = paginationData.pagination;
799
- response.push(...paginationData.info);
800
- response.push(...paginationData.items.map(item => item.toString()));
801
842
  structuredContent.consoleMessages = paginationData.items.map(item => item.toJSON());
843
+ response.push(...paginationData.info);
844
+ if (useToon) {
845
+ response.push(toonEncode(structuredContent.consoleMessages));
846
+ }
847
+ else {
848
+ response.push(...paginationData.items.map(item => item.toString()));
849
+ }
802
850
  }
803
851
  else {
804
852
  response.push('<no console messages found>');
@@ -50,7 +50,7 @@ export class PageCollector {
50
50
  this.addPage(page);
51
51
  }
52
52
  catch (err) {
53
- logger('Error getting a page for a target onTargetCreated', err);
53
+ logger?.('Error getting a page for a target onTargetCreated', err);
54
54
  }
55
55
  };
56
56
  #onTargetDestroyed = async (target) => {
@@ -62,7 +62,7 @@ export class PageCollector {
62
62
  this.cleanupPageDestroyed(page);
63
63
  }
64
64
  catch (err) {
65
- logger('Error getting a page for a target onTargetDestroyed', err);
65
+ logger?.('Error getting a page for a target onTargetDestroyed', err);
66
66
  }
67
67
  };
68
68
  addPage(page) {
@@ -136,10 +136,10 @@ export class PageCollector {
136
136
  throw new Error('No requests found for selected page');
137
137
  }
138
138
  const item = this.find(page, item => item[stableIdSymbol] === stableId);
139
- if (item) {
140
- return item;
139
+ if (!item) {
140
+ throw new Error('Request not found for selected page');
141
141
  }
142
- throw new Error('Request not found for selected page');
142
+ return item;
143
143
  }
144
144
  find(page, filter) {
145
145
  const navigations = this.storage.get(page);
@@ -241,7 +241,7 @@ class PageEventSubscriber {
241
241
  // @ts-expect-error Protocol types diverge.
242
242
  inspectorIssue)[0];
243
243
  if (!issue) {
244
- logger('No issue mapping for for the issue: ', inspectorIssue.code);
244
+ logger?.('No issue mapping for for the issue: ', inspectorIssue.code);
245
245
  return;
246
246
  }
247
247
  const primaryKey = issue.primaryKey();
@@ -256,7 +256,7 @@ class PageEventSubscriber {
256
256
  });
257
257
  }
258
258
  catch (error) {
259
- logger('Error creating a new issue', error);
259
+ logger?.('Error creating a new issue', error);
260
260
  }
261
261
  };
262
262
  }