chrome-devtools-mcp 0.26.0 → 1.1.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 (45) hide show
  1. package/README.md +59 -10
  2. package/build/src/DevtoolsUtils.js +14 -1
  3. package/build/src/HeapSnapshotManager.js +42 -18
  4. package/build/src/McpContext.js +61 -23
  5. package/build/src/McpResponse.js +51 -21
  6. package/build/src/ToolHandler.js +30 -1
  7. package/build/src/WaitForHelper.js +18 -4
  8. package/build/src/bin/check-latest-version.js +25 -1
  9. package/build/src/bin/chrome-devtools-cli-options.js +81 -39
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
  11. package/build/src/bin/chrome-devtools-mcp-main.js +38 -0
  12. package/build/src/browser.js +36 -2
  13. package/build/src/daemon/client.js +12 -6
  14. package/build/src/daemon/daemon.js +62 -5
  15. package/build/src/formatters/HeapSnapshotFormatter.js +30 -9
  16. package/build/src/index.js +3 -1
  17. package/build/src/telemetry/ClearcutLogger.js +8 -119
  18. package/build/src/telemetry/errors.js +4 -0
  19. package/build/src/telemetry/flagUtils.js +4 -3
  20. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
  21. package/build/src/telemetry/persistence.js +20 -2
  22. package/build/src/telemetry/transformation.js +134 -0
  23. package/build/src/third_party/THIRD_PARTY_NOTICES +4 -719
  24. package/build/src/third_party/bundled-packages.json +2 -2
  25. package/build/src/third_party/devtools-formatter-worker.js +447 -114
  26. package/build/src/third_party/devtools-heap-snapshot-worker.js +2 -3
  27. package/build/src/third_party/index.js +3443 -30153
  28. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  29. package/build/src/tools/ToolDefinition.js +2 -2
  30. package/build/src/tools/emulation.js +28 -2
  31. package/build/src/tools/extensions.js +2 -0
  32. package/build/src/tools/input.js +19 -10
  33. package/build/src/tools/lighthouse.js +1 -1
  34. package/build/src/tools/memory.js +39 -17
  35. package/build/src/tools/network.js +2 -2
  36. package/build/src/tools/performance.js +9 -6
  37. package/build/src/tools/screencast.js +1 -1
  38. package/build/src/tools/screenshot.js +1 -1
  39. package/build/src/tools/script.js +32 -10
  40. package/build/src/tools/snapshot.js +1 -1
  41. package/build/src/trace-processing/parse.js +2 -2
  42. package/build/src/utils/files.js +43 -0
  43. package/build/src/version.js +1 -1
  44. package/package.json +7 -4
  45. package/build/src/telemetry/metricUtils.js +0 -15
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Chrome DevTools for Agents
1
+ # Chrome DevTools for agents
2
2
 
3
3
  [![npm chrome-devtools-mcp package](https://img.shields.io/npm/v/chrome-devtools-mcp.svg)](https://npmjs.org/package/chrome-devtools-mcp)
4
4
 
5
- Chrome DevTools for Agents (`chrome-devtools-mcp`) lets your coding agent (such as Gemini, Claude, Cursor or Copilot)
5
+ Chrome DevTools for agents (`chrome-devtools-mcp`) lets your coding agent (such as Antigravity, Claude, Cursor or Copilot)
6
6
  control and inspect a live Chrome browser. It acts as a Model-Context-Protocol
7
7
  (MCP) server, giving your AI coding assistant access to the full power of
8
8
  Chrome DevTools for reliable automation, in-depth debugging, and performance analysis.
@@ -61,7 +61,7 @@ You can disable these update checks by setting the `CHROME_DEVTOOLS_MCP_NO_UPDAT
61
61
 
62
62
  ## Requirements
63
63
 
64
- - [Node.js](https://nodejs.org/) v20.19 or a newer [latest maintenance LTS](https://github.com/nodejs/Release#release-schedule) version.
64
+ - [Node.js](https://nodejs.org/) [LTS](https://github.com/nodejs/Release#release-schedule) version.
65
65
  - [Chrome](https://www.google.com/chrome/) current stable version or newer.
66
66
  - [npm](https://www.npmjs.com/)
67
67
 
@@ -249,7 +249,7 @@ and the expert guidance it needs to use them effectively.
249
249
 
250
250
  1. Open the **Command Palette** (`Cmd+Shift+P` on macOS or `Ctrl+Shift+P` on Windows/Linux).
251
251
  2. Search for and run the **Chat: Install Plugin From Source** command.
252
- 3. Paste in our repository URL: `https://github.com/ChromeDevTools/chrome-devtools-mcp`
252
+ 3. Paste in our repository name: `ChromeDevTools/chrome-devtools-mcp`.
253
253
 
254
254
  That's it! Your agent is now supercharged with Chrome DevTools capabilities.
255
255
 
@@ -514,11 +514,12 @@ 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** (4 tools)
518
- - [`take_memory_snapshot`](docs/tool-reference.md#take_memory_snapshot)
519
- - [`get_memory_snapshot_details`](docs/tool-reference.md#get_memory_snapshot_details)
520
- - [`get_nodes_by_class`](docs/tool-reference.md#get_nodes_by_class)
521
- - [`load_memory_snapshot`](docs/tool-reference.md#load_memory_snapshot)
517
+ - **Memory** (5 tools)
518
+ - [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
519
+ - [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
520
+ - [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
521
+ - [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
522
+ - [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
522
523
  - **Extensions** (5 tools)
523
524
  - [`install_extension`](docs/tool-reference.md#install_extension)
524
525
  - [`list_extensions`](docs/tool-reference.md#list_extensions)
@@ -595,10 +596,30 @@ The Chrome DevTools MCP server supports the following configuration option:
595
596
  If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
596
597
  - **Type:** boolean
597
598
 
599
+ - **`--experimentalPageIdRouting`/ `--experimental-page-id-routing`**
600
+ Whether to expose pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions).
601
+ - **Type:** boolean
602
+
603
+ - **`--experimentalDevtools`/ `--experimental-devtools`**
604
+ Whether to enable automation over DevTools targets
605
+ - **Type:** boolean
606
+
598
607
  - **`--experimentalVision`/ `--experimental-vision`**
599
608
  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.
600
609
  - **Type:** boolean
601
610
 
611
+ - **`--experimentalMemory`/ `--experimental-memory`**
612
+ Whether to enable experimental memory tools.
613
+ - **Type:** boolean
614
+
615
+ - **`--experimentalStructuredContent`/ `--experimental-structured-content`**
616
+ Whether to output structured formatted content.
617
+ - **Type:** boolean
618
+
619
+ - **`--experimentalIncludeAllPages`/ `--experimental-include-all-pages`**
620
+ Whether to include all kinds of pages such as webviews or background pages as pages.
621
+ - **Type:** boolean
622
+
602
623
  - **`--experimentalScreencast`/ `--experimental-screencast`**
603
624
  Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
604
625
  - **Type:** boolean
@@ -659,7 +680,7 @@ The Chrome DevTools MCP server supports the following configuration option:
659
680
  - **Type:** boolean
660
681
 
661
682
  - **`--redactNetworkHeaders`/ `--redact-network-headers`**
662
- If true, redacts some of the network headers considered senstive before returning to the client.
683
+ If true, redacts some of the network headers considered sensitive before returning to the client.
663
684
  - **Type:** boolean
664
685
  - **Default:** `false`
665
686
 
@@ -708,6 +729,34 @@ You can also run `npx chrome-devtools-mcp@latest --help` to see all available co
708
729
 
709
730
  ## Concepts
710
731
 
732
+ ### Concurrent sessions
733
+
734
+ Most MCP clients start one Chrome DevTools MCP server per conversation. If your
735
+ client shares a single server instance across concurrent agents or subagents,
736
+ start the server with `--experimentalPageIdRouting`. This exposes `pageId` on
737
+ page-scoped tools so each agent can route tool calls to the tab it is working
738
+ with.
739
+
740
+ ```json
741
+ {
742
+ "mcpServers": {
743
+ "chrome-devtools": {
744
+ "command": "npx",
745
+ "args": [
746
+ "-y",
747
+ "chrome-devtools-mcp@latest",
748
+ "--experimentalPageIdRouting"
749
+ ]
750
+ }
751
+ }
752
+ }
753
+ ```
754
+
755
+ If you run multiple independent MCP client sessions and want each session to
756
+ launch its own temporary Chrome profile, also pass `--isolated`. This avoids
757
+ sharing the default Chrome DevTools MCP user data directory between those
758
+ server instances.
759
+
711
760
  ### User data directory
712
761
 
713
762
  `chrome-devtools-mcp` starts a Chrome's stable channel instance using the following user
@@ -106,9 +106,10 @@ const DEFAULT_FACTORY = async (page) => {
106
106
  const connection = new PuppeteerDevToolsConnection(session);
107
107
  const targetManager = universe.context.get(DevTools.TargetManager);
108
108
  targetManager.observeModels(DevTools.DebuggerModel, SKIP_ALL_PAUSES);
109
+ targetManager.observeModels(DevTools.NetworkManager.NetworkManager, DISABLE_NETWORK);
109
110
  const target = targetManager.createTarget('main', '', 'frame', // eslint-disable-line @typescript-eslint/no-explicit-any
110
111
  /* parentTarget */ null, session.id(), undefined, connection);
111
- return { target, universe };
112
+ return { target, universe, session };
112
113
  };
113
114
  // We don't want to pause any DevTools universe session ever on the MCP side.
114
115
  //
@@ -123,6 +124,18 @@ const SKIP_ALL_PAUSES = {
123
124
  // Do nothing.
124
125
  },
125
126
  };
127
+ // Not recording network requests in the DevTools universe.
128
+ //
129
+ // The network requests are collected through pptr and there isn't a use case for
130
+ // enabling devtools SDK's network domain.
131
+ const DISABLE_NETWORK = {
132
+ modelAdded(model) {
133
+ void model.target().networkAgent().invoke_disable();
134
+ },
135
+ modelRemoved() {
136
+ // Do nothing.
137
+ },
138
+ };
126
139
  /**
127
140
  * Constructed from Runtime.ExceptionDetails of an uncaught error.
128
141
  *
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import fsSync from 'node:fs';
7
7
  import path from 'node:path';
8
+ import { isNodeLike } from './formatters/HeapSnapshotFormatter.js';
8
9
  import { DevTools } from './third_party/index.js';
9
10
  import { createIdGenerator, stableIdSymbol, } from './utils/id.js';
10
11
  export class HeapSnapshotManager {
@@ -19,8 +20,8 @@ export class HeapSnapshotManager {
19
20
  this.#snapshots.set(absolutePath, {
20
21
  snapshot,
21
22
  worker,
22
- uidToClassKey: new Map(),
23
- classKeyToUid: new Map(),
23
+ idToClassKey: new Map(),
24
+ classKeyToId: new Map(),
24
25
  idGenerator: createIdGenerator(),
25
26
  });
26
27
  return snapshot;
@@ -30,10 +31,10 @@ export class HeapSnapshotManager {
30
31
  const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
31
32
  const aggregates = await snapshot.aggregatesWithFilter(filter);
32
33
  for (const key of Object.keys(aggregates)) {
33
- const uid = await this.getOrCreateUidForClassKey(filePath, key);
34
+ const id = await this.getOrCreateIdForClassKey(filePath, key);
34
35
  const aggregate = aggregates[key];
35
36
  if (aggregate) {
36
- aggregate[stableIdSymbol] = uid;
37
+ aggregate[stableIdSymbol] = id;
37
38
  }
38
39
  }
39
40
  return aggregates;
@@ -46,26 +47,49 @@ export class HeapSnapshotManager {
46
47
  const snapshot = await this.getSnapshot(filePath);
47
48
  return snapshot.staticData;
48
49
  }
49
- async getOrCreateUidForClassKey(filePath, classKey) {
50
+ async getOrCreateIdForClassKey(filePath, classKey) {
50
51
  const cached = this.#getCachedSnapshot(filePath);
51
- let uid = cached.classKeyToUid.get(classKey);
52
- if (!uid) {
53
- uid = cached.idGenerator();
54
- cached.classKeyToUid.set(classKey, uid);
55
- cached.uidToClassKey.set(uid, classKey);
52
+ let id = cached.classKeyToId.get(classKey);
53
+ if (!id) {
54
+ id = cached.idGenerator();
55
+ cached.classKeyToId.set(classKey, id);
56
+ cached.idToClassKey.set(id, classKey);
56
57
  }
57
- return uid;
58
+ return id;
58
59
  }
59
- async getNodesByUid(filePath, uid) {
60
+ async getNodesById(filePath, id) {
60
61
  const snapshot = await this.getSnapshot(filePath);
61
62
  const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
62
- const className = await this.resolveClassKeyFromUid(filePath, uid);
63
+ const className = await this.resolveClassKeyFromId(filePath, id);
63
64
  if (!className) {
64
- throw new Error(`Class with UID ${uid} not found in heap snapshot`);
65
+ throw new Error(`Class with ID ${id} not found in heap snapshot`);
65
66
  }
66
67
  const provider = snapshot.createNodesProviderForClass(className, filter);
67
- const range = await provider.serializeItemsRange(0, 1);
68
- return await provider.serializeItemsRange(0, range.totalLength);
68
+ return await provider.serializeItemsRange(0, Infinity);
69
+ }
70
+ async findNodeIndexById(filePath, nodeId) {
71
+ const snapshot = await this.getSnapshot(filePath);
72
+ const aggregates = await this.getAggregates(filePath);
73
+ const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
74
+ for (const classKey of Object.keys(aggregates)) {
75
+ const provider = snapshot.createNodesProviderForClass(classKey, filter);
76
+ const range = await provider.serializeItemsRange(0, Infinity);
77
+ for (const item of range.items) {
78
+ if (isNodeLike(item) && item.id === nodeId) {
79
+ return item.nodeIndex;
80
+ }
81
+ }
82
+ }
83
+ return undefined;
84
+ }
85
+ async getRetainers(filePath, nodeId) {
86
+ const nodeIndex = await this.findNodeIndexById(filePath, nodeId);
87
+ if (nodeIndex === undefined) {
88
+ throw new Error(`Node with ID ${nodeId} not found`);
89
+ }
90
+ const snapshot = await this.getSnapshot(filePath);
91
+ const provider = snapshot.createRetainingEdgesProvider(nodeIndex);
92
+ return await provider.serializeItemsRange(0, Infinity);
69
93
  }
70
94
  #getCachedSnapshot(filePath) {
71
95
  const absolutePath = path.resolve(filePath);
@@ -75,9 +99,9 @@ export class HeapSnapshotManager {
75
99
  }
76
100
  return cached;
77
101
  }
78
- async resolveClassKeyFromUid(filePath, uid) {
102
+ async resolveClassKeyFromId(filePath, id) {
79
103
  const cached = this.#getCachedSnapshot(filePath);
80
- return cached.uidToClassKey.get(uid);
104
+ return cached.idToClassKey.get(id);
81
105
  }
82
106
  async #loadSnapshot(absolutePath) {
83
107
  const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import fs from 'node:fs/promises';
7
+ import fsPromises from 'node:fs/promises';
7
8
  import os from 'node:os';
8
9
  import path from 'node:path';
9
10
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -14,7 +15,7 @@ import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
14
15
  import { Locator, PredefinedNetworkConditions, } from './third_party/index.js';
15
16
  import { listPages } from './tools/pages.js';
16
17
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
17
- import { ensureExtension, getTempFilePath } from './utils/files.js';
18
+ import { ensureExtension, getTempFilePath, resolveCanonicalPath, } from './utils/files.js';
18
19
  import { getNetworkMultiplierFromString } from './WaitForHelper.js';
19
20
  const DEFAULT_TIMEOUT = 5_000;
20
21
  const NAVIGATION_TIMEOUT = 10_000;
@@ -106,7 +107,7 @@ export class McpContext {
106
107
  setRoots(roots) {
107
108
  this.#roots = roots;
108
109
  }
109
- validatePath(filePath) {
110
+ async validatePath(filePath) {
110
111
  if (filePath === undefined) {
111
112
  return;
112
113
  }
@@ -114,15 +115,36 @@ export class McpContext {
114
115
  if (roots === undefined) {
115
116
  return;
116
117
  }
117
- const absolutePath = path.resolve(filePath);
118
+ let canonicalPath;
119
+ try {
120
+ canonicalPath = await resolveCanonicalPath(filePath);
121
+ }
122
+ catch (err) {
123
+ const errMsg = err instanceof Error ? err.message : String(err);
124
+ console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
125
+ throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
126
+ }
127
+ let allowed = false;
118
128
  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;
129
+ try {
130
+ const rootPathUri = root.uri;
131
+ const rootPath = path.resolve(fileURLToPath(rootPathUri));
132
+ const canonicalRoot = await fsPromises.realpath(rootPath);
133
+ if (canonicalPath === canonicalRoot ||
134
+ canonicalPath.startsWith(canonicalRoot + path.sep)) {
135
+ allowed = true;
136
+ break;
137
+ }
138
+ }
139
+ catch (rootErr) {
140
+ const errMsg = rootErr instanceof Error ? rootErr.message : String(rootErr);
141
+ console.warn(`[MCP Context] Could not resolve configured root ${root.uri}: ${errMsg}`);
142
+ // Skip this root if it cannot be resolved.
123
143
  }
124
144
  }
125
- throw new Error(`Access denied: path ${filePath} is not within any of the workspace roots ${JSON.stringify(roots)}.`);
145
+ if (!allowed) {
146
+ throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
147
+ }
126
148
  }
127
149
  resolveCdpRequestId(page, cdpRequestId) {
128
150
  if (!cdpRequestId) {
@@ -213,12 +235,23 @@ export class McpContext {
213
235
  await page.emulateNetworkConditions(networkCondition);
214
236
  newSettings.networkConditions = options.networkConditions;
215
237
  }
238
+ const secondarySession = this.getDevToolsUniverse(mcpPage)?.session;
216
239
  if (!options.cpuThrottlingRate) {
217
240
  await page.emulateCPUThrottling(1);
241
+ if (secondarySession) {
242
+ await secondarySession.send('Emulation.setCPUThrottlingRate', {
243
+ rate: 1,
244
+ });
245
+ }
218
246
  delete newSettings.cpuThrottlingRate;
219
247
  }
220
248
  else {
221
249
  await page.emulateCPUThrottling(options.cpuThrottlingRate);
250
+ if (secondarySession) {
251
+ await secondarySession.send('Emulation.setCPUThrottlingRate', {
252
+ rate: options.cpuThrottlingRate,
253
+ });
254
+ }
222
255
  newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
223
256
  }
224
257
  if (!options.geolocation) {
@@ -250,7 +283,6 @@ export class McpContext {
250
283
  newSettings.colorScheme = options.colorScheme;
251
284
  }
252
285
  if (!options.viewport) {
253
- await page.setViewport(null);
254
286
  delete newSettings.viewport;
255
287
  }
256
288
  else {
@@ -260,14 +292,22 @@ export class McpContext {
260
292
  hasTouch: false,
261
293
  isLandscape: false,
262
294
  };
263
- const viewport = { ...defaults, ...options.viewport };
264
- await page.setViewport(viewport);
265
- newSettings.viewport = viewport;
295
+ newSettings.viewport = { ...defaults, ...options.viewport };
296
+ }
297
+ if (options.extraHttpHeaders !== undefined) {
298
+ await page.setExtraHTTPHeaders(options.extraHttpHeaders);
299
+ newSettings.extraHttpHeaders = options.extraHttpHeaders;
300
+ if (Object.keys(options.extraHttpHeaders).length === 0) {
301
+ delete newSettings.extraHttpHeaders;
302
+ }
266
303
  }
267
304
  mcpPage.emulationSettings = Object.keys(newSettings).length
268
305
  ? newSettings
269
306
  : {};
270
307
  this.#updateSelectedPageTimeouts();
308
+ // This should happen after updating the page timeouts.
309
+ // Setting the viewport can trigger a reload which we don't want to timeout.
310
+ await page.setViewport(newSettings.viewport ?? null);
271
311
  }
272
312
  setIsRunningPerformanceTrace(x) {
273
313
  this.#isRunningTrace = x;
@@ -333,9 +373,9 @@ export class McpContext {
333
373
  page.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
334
374
  // 10sec should be enough for the load event to be emitted during
335
375
  // navigations.
336
- // Increased in case we throttle the network requests
376
+ // Increased in case we throttle the network requests or the CPU
337
377
  const networkMultiplier = getNetworkMultiplierFromString(page.networkConditions);
338
- page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
378
+ page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier * cpuMultiplier);
339
379
  }
340
380
  // Linear scan over per-page snapshots. The page count is small (typically
341
381
  // 2-10) so a reverse index isn't worthwhile given the uid-reuse lifecycle
@@ -499,7 +539,7 @@ export class McpContext {
499
539
  }
500
540
  async saveTemporaryFile(data, filename) {
501
541
  const filepath = await getTempFilePath(filename);
502
- this.validatePath(filepath);
542
+ await this.validatePath(filepath);
503
543
  try {
504
544
  await fs.writeFile(filepath, data);
505
545
  }
@@ -509,7 +549,7 @@ export class McpContext {
509
549
  return { filepath };
510
550
  }
511
551
  async saveFile(data, clientProvidedFilePath, extension) {
512
- this.validatePath(clientProvidedFilePath);
552
+ await this.validatePath(clientProvidedFilePath);
513
553
  try {
514
554
  const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
515
555
  await fs.mkdir(path.dirname(filePath), { recursive: true });
@@ -562,7 +602,6 @@ export class McpContext {
562
602
  await this.#networkCollector.init(pages);
563
603
  }
564
604
  async installExtension(extensionPath) {
565
- this.validatePath(extensionPath);
566
605
  const id = await this.browser.installExtension(extensionPath);
567
606
  return id;
568
607
  }
@@ -586,20 +625,19 @@ export class McpContext {
586
625
  return pptrExtensions.get(id);
587
626
  }
588
627
  async getHeapSnapshotAggregates(filePath) {
589
- this.validatePath(filePath);
590
628
  return await this.#heapSnapshotManager.getAggregates(filePath);
591
629
  }
592
630
  async getHeapSnapshotStats(filePath) {
593
- this.validatePath(filePath);
594
631
  return await this.#heapSnapshotManager.getStats(filePath);
595
632
  }
596
633
  async getHeapSnapshotStaticData(filePath) {
597
- this.validatePath(filePath);
598
634
  return await this.#heapSnapshotManager.getStaticData(filePath);
599
635
  }
600
- async getHeapSnapshotNodesByUid(filePath, uid) {
601
- this.validatePath(filePath);
602
- return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
636
+ async getHeapSnapshotNodesById(filePath, id) {
637
+ return await this.#heapSnapshotManager.getNodesById(filePath, id);
638
+ }
639
+ async getHeapSnapshotRetainers(filePath, nodeId) {
640
+ return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
603
641
  }
604
642
  }
605
643
  //# sourceMappingURL=McpContext.js.map
@@ -5,7 +5,7 @@
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
+ import { isEdgeLike, isNodeLike } from './formatters/HeapSnapshotFormatter.js';
9
9
  import { IssueFormatter } from './formatters/IssueFormatter.js';
10
10
  import { NetworkFormatter } from './formatters/NetworkFormatter.js';
11
11
  import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
@@ -99,7 +99,7 @@ async function getToolGroup(page) {
99
99
  window.dispatchEvent(event);
100
100
  // If the page does not synchronously call `event.respondWith`, return instead of timing out
101
101
  setTimeout(() => {
102
- resolve(undefined);
102
+ resolve(null);
103
103
  }, 0);
104
104
  });
105
105
  });
@@ -133,6 +133,7 @@ export class McpResponse {
133
133
  #page;
134
134
  #redactNetworkHeaders = true;
135
135
  #error;
136
+ #attachedWaitForResult;
136
137
  constructor(args) {
137
138
  this.#args = args;
138
139
  }
@@ -164,9 +165,7 @@ export class McpResponse {
164
165
  this.#listExtensions = true;
165
166
  }
166
167
  setListThirdPartyDeveloperTools() {
167
- if (this.#args.categoryExperimentalThirdParty) {
168
- this.#listThirdPartyDeveloperTools = true;
169
- }
168
+ this.#listThirdPartyDeveloperTools = true;
170
169
  }
171
170
  setListWebMcpTools() {
172
171
  this.#listWebMcpTools = true;
@@ -265,6 +264,9 @@ export class McpResponse {
265
264
  appendResponseLine(value) {
266
265
  this.#textResponseLines.push(value);
267
266
  }
267
+ attachWaitForResult(result) {
268
+ this.#attachedWaitForResult = result;
269
+ }
268
270
  setHeapSnapshotAggregates(aggregates, options) {
269
271
  this.#heapSnapshotOptions = {
270
272
  ...this.#heapSnapshotOptions,
@@ -381,16 +383,21 @@ export class McpResponse {
381
383
  if (this.#listExtensions) {
382
384
  extensions = await context.listExtensions();
383
385
  }
386
+ // Null indicates no tools.
384
387
  let thirdPartyDeveloperTools;
385
- if (this.#listThirdPartyDeveloperTools) {
386
- const page = this.#page ?? context.getSelectedMcpPage();
387
- thirdPartyDeveloperTools = await getToolGroup(page);
388
- page.thirdPartyDeveloperTools = thirdPartyDeveloperTools;
388
+ if (this.#args.categoryExperimentalThirdParty &&
389
+ this.#listThirdPartyDeveloperTools &&
390
+ this.#page) {
391
+ thirdPartyDeveloperTools = await getToolGroup(this.#page);
392
+ if (thirdPartyDeveloperTools) {
393
+ this.#page.thirdPartyDeveloperTools = thirdPartyDeveloperTools;
394
+ }
389
395
  }
390
396
  let webmcpTools;
391
- if (this.#listWebMcpTools && this.#args.categoryExperimentalWebmcp) {
392
- const page = this.#page ?? context.getSelectedMcpPage();
393
- webmcpTools = page.getWebMcpTools();
397
+ if (this.#args.categoryExperimentalWebmcp &&
398
+ this.#listWebMcpTools &&
399
+ this.#page) {
400
+ webmcpTools = this.#page.getWebMcpTools();
394
401
  }
395
402
  let consoleMessages;
396
403
  if (this.#consoleDataOptions?.include) {
@@ -481,6 +488,13 @@ export class McpResponse {
481
488
  structuredContent.message = this.#textResponseLines.join('\n');
482
489
  response.push(...this.#textResponseLines);
483
490
  }
491
+ if (this.#attachedWaitForResult) {
492
+ if (this.#attachedWaitForResult.navigatedToUrl) {
493
+ response.push(`Page navigated to ${this.#attachedWaitForResult.navigatedToUrl}.`);
494
+ structuredContent.navigatedToUrl =
495
+ this.#attachedWaitForResult.navigatedToUrl;
496
+ }
497
+ }
484
498
  const networkConditions = this.#page?.networkConditions;
485
499
  if (networkConditions) {
486
500
  const timeout = this.#page.pptrPage.getDefaultNavigationTimeout();
@@ -489,6 +503,11 @@ export class McpResponse {
489
503
  structuredContent.networkConditions = networkConditions;
490
504
  structuredContent.navigationTimeout = timeout;
491
505
  }
506
+ const geolocation = this.#page?.geolocation;
507
+ if (geolocation) {
508
+ response.push(`Emulating geolocation: latitude=${geolocation.latitude}, longitude=${geolocation.longitude}`);
509
+ structuredContent.geolocation = geolocation;
510
+ }
492
511
  const viewport = this.#page?.viewport;
493
512
  if (viewport) {
494
513
  response.push(`Emulating viewport: ${JSON.stringify(viewport)}`);
@@ -664,10 +683,19 @@ Call ${handleDialog.name} to handle it before continuing.`);
664
683
  }
665
684
  const nodes = this.#heapSnapshotOptions.nodes;
666
685
  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);
686
+ let items = Array.from(nodes.items);
687
+ const firstItem = nodes.items[0];
688
+ if (firstItem) {
689
+ if (isNodeLike(firstItem)) {
690
+ items = items
691
+ .filter(isNodeLike)
692
+ .sort((a, b) => b.retainedSize - a.retainedSize);
693
+ }
694
+ else if (isEdgeLike(firstItem)) {
695
+ items = items.filter(isEdgeLike);
696
+ }
697
+ }
698
+ const paginationData = this.#dataWithPagination(items, this.#heapSnapshotOptions.pagination);
671
699
  response.push(HeapSnapshotFormatter.formatNodes(paginationData.items));
672
700
  structuredContent.pagination = paginationData.pagination;
673
701
  response.push(...paginationData.info);
@@ -700,12 +728,14 @@ Call ${handleDialog.name} to handle it before continuing.`);
700
728
  response.push(extensionsMessage);
701
729
  }
702
730
  }
703
- if (this.#listThirdPartyDeveloperTools) {
704
- structuredContent.thirdPartyDeveloperTools =
705
- data.thirdPartyDeveloperTools ?? undefined;
731
+ if (data.thirdPartyDeveloperTools !== undefined) {
732
+ if (data.thirdPartyDeveloperTools) {
733
+ structuredContent.thirdPartyDeveloperTools =
734
+ data.thirdPartyDeveloperTools;
735
+ }
706
736
  response.push('## Third-party developer tools');
707
- if (!data.thirdPartyDeveloperTools ||
708
- !data.thirdPartyDeveloperTools.tools) {
737
+ if (data.thirdPartyDeveloperTools === null ||
738
+ !data.thirdPartyDeveloperTools?.tools) {
709
739
  response.push('No third-party developer tools available.');
710
740
  }
711
741
  else {
@@ -7,7 +7,8 @@ import { logger } from './logger.js';
7
7
  import { McpResponse } from './McpResponse.js';
8
8
  import { SlimMcpResponse } from './SlimMcpResponse.js';
9
9
  import { ClearcutLogger } from './telemetry/ClearcutLogger.js';
10
- import { bucketizeLatency } from './telemetry/metricUtils.js';
10
+ import { bucketizeLatency } from './telemetry/transformation.js';
11
+ import { zod } from './third_party/index.js';
11
12
  import { labels, OFF_BY_DEFAULT_CATEGORIES } from './tools/categories.js';
12
13
  import { pageIdSchema } from './tools/ToolDefinition.js';
13
14
  export function buildFlag(category) {
@@ -70,12 +71,24 @@ function getToolStatusInfo(tool, serverArgs) {
70
71
  function isPageScopedTool(tool) {
71
72
  return 'pageScoped' in tool && tool.pageScoped === true;
72
73
  }
74
+ function formatArgumentNames(names) {
75
+ return names.map(name => `"${name}"`).join(', ');
76
+ }
77
+ function buildUnknownArgumentsMessage(toolName, unknownArgumentNames, expectedArgumentNames) {
78
+ const unknownLabel = unknownArgumentNames.length === 1 ? 'argument' : 'arguments';
79
+ const expectedArguments = expectedArgumentNames.length
80
+ ? `Expected arguments: ${formatArgumentNames(expectedArgumentNames)}.`
81
+ : 'This tool does not accept any arguments.';
82
+ const correction = unknownArgumentNames.length === 1 ? 'Remove it' : 'Remove them';
83
+ return `Unknown ${unknownLabel} for tool "${toolName}": ${formatArgumentNames(unknownArgumentNames)}. ${expectedArguments} ${correction} and retry.`;
84
+ }
73
85
  export class ToolHandler {
74
86
  tool;
75
87
  serverArgs;
76
88
  getContext;
77
89
  toolMutex;
78
90
  inputSchema;
91
+ registeredInputSchema;
79
92
  shouldRegister;
80
93
  disabledReason;
81
94
  constructor(tool, serverArgs, getContext, toolMutex) {
@@ -93,6 +106,10 @@ export class ToolHandler {
93
106
  !serverArgs.slim
94
107
  ? { ...tool.schema, ...pageIdSchema }
95
108
  : tool.schema;
109
+ this.registeredInputSchema = zod.object(this.inputSchema).passthrough();
110
+ }
111
+ unknownArgumentNames(params) {
112
+ return Object.keys(params).filter(key => !Object.hasOwn(this.inputSchema, key));
96
113
  }
97
114
  async handle(params) {
98
115
  if (this.disabledReason) {
@@ -106,6 +123,18 @@ export class ToolHandler {
106
123
  isError: true,
107
124
  };
108
125
  }
126
+ const unknownArgumentNames = this.unknownArgumentNames(params);
127
+ if (unknownArgumentNames.length) {
128
+ return {
129
+ content: [
130
+ {
131
+ type: 'text',
132
+ text: buildUnknownArgumentsMessage(this.tool.name, unknownArgumentNames, Object.keys(this.inputSchema)),
133
+ },
134
+ ],
135
+ isError: true,
136
+ };
137
+ }
109
138
  const guard = await this.toolMutex.acquire();
110
139
  const startTime = Date.now();
111
140
  let success = false;