chrome-devtools-mcp 0.26.0 → 1.0.1

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 (33) hide show
  1. package/README.md +54 -5
  2. package/build/src/DevtoolsUtils.js +13 -0
  3. package/build/src/HeapSnapshotManager.js +26 -2
  4. package/build/src/McpContext.js +3 -0
  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 +37 -1
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
  11. package/build/src/daemon/client.js +12 -6
  12. package/build/src/formatters/HeapSnapshotFormatter.js +27 -6
  13. package/build/src/index.js +3 -1
  14. package/build/src/telemetry/ClearcutLogger.js +8 -119
  15. package/build/src/telemetry/errors.js +4 -0
  16. package/build/src/telemetry/flagUtils.js +4 -3
  17. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
  18. package/build/src/telemetry/persistence.js +20 -2
  19. package/build/src/telemetry/transformation.js +134 -0
  20. package/build/src/third_party/THIRD_PARTY_NOTICES +2 -717
  21. package/build/src/third_party/bundled-packages.json +2 -2
  22. package/build/src/third_party/devtools-formatter-worker.js +445 -114
  23. package/build/src/third_party/devtools-heap-snapshot-worker.js +0 -3
  24. package/build/src/third_party/index.js +3255 -30087
  25. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  26. package/build/src/tools/ToolDefinition.js +1 -1
  27. package/build/src/tools/emulation.js +3 -2
  28. package/build/src/tools/input.js +18 -9
  29. package/build/src/tools/memory.js +24 -0
  30. package/build/src/tools/script.js +32 -10
  31. package/build/src/version.js +1 -1
  32. package/package.json +7 -4
  33. 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 Gemini, 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
 
@@ -514,9 +514,10 @@ 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)
517
+ - **Memory** (5 tools)
518
518
  - [`take_memory_snapshot`](docs/tool-reference.md#take_memory_snapshot)
519
519
  - [`get_memory_snapshot_details`](docs/tool-reference.md#get_memory_snapshot_details)
520
+ - [`get_node_retainers`](docs/tool-reference.md#get_node_retainers)
520
521
  - [`get_nodes_by_class`](docs/tool-reference.md#get_nodes_by_class)
521
522
  - [`load_memory_snapshot`](docs/tool-reference.md#load_memory_snapshot)
522
523
  - **Extensions** (5 tools)
@@ -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,6 +106,7 @@ 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
112
  return { target, universe };
@@ -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 {
@@ -64,8 +65,31 @@ export class HeapSnapshotManager {
64
65
  throw new Error(`Class with UID ${uid} 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);
@@ -601,5 +601,8 @@ export class McpContext {
601
601
  this.validatePath(filePath);
602
602
  return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
603
603
  }
604
+ async getHeapSnapshotRetainers(filePath, nodeId) {
605
+ return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
606
+ }
604
607
  }
605
608
  //# 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}, longtitude=${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;
@@ -11,12 +11,15 @@ export class WaitForHelper {
11
11
  #stableDomFor;
12
12
  #expectNavigationIn;
13
13
  #navigationTimeout;
14
+ #dialogOpened = false;
15
+ #initialUrl;
14
16
  constructor(page, cpuTimeoutMultiplier, networkTimeoutMultiplier) {
15
17
  this.#stableDomTimeout = 3000 * cpuTimeoutMultiplier;
16
18
  this.#stableDomFor = 100 * cpuTimeoutMultiplier;
17
19
  this.#expectNavigationIn = 100 * cpuTimeoutMultiplier;
18
20
  this.#navigationTimeout = 3000 * networkTimeoutMultiplier;
19
21
  this.#page = page;
22
+ this.#initialUrl = page.url();
20
23
  }
21
24
  /**
22
25
  * A wrapper that executes a action and waits for
@@ -104,10 +107,12 @@ export class WaitForHelper {
104
107
  });
105
108
  }
106
109
  async waitForEventsAfterAction(action, options) {
107
- let dialogOpened = false;
110
+ if (this.#abortController.signal.aborted) {
111
+ throw new Error("Can't re-use a WaitForHelper");
112
+ }
108
113
  if (options?.handleDialog) {
109
114
  const dialogHandler = (dialog) => {
110
- dialogOpened = true;
115
+ this.#dialogOpened = true;
111
116
  if (options.handleDialog === 'dismiss') {
112
117
  void dialog.dismiss();
113
118
  }
@@ -144,8 +149,8 @@ export class WaitForHelper {
144
149
  }
145
150
  try {
146
151
  await navigationFinished;
147
- if (dialogOpened) {
148
- return;
152
+ if (this.#dialogOpened) {
153
+ return this.#getResult();
149
154
  }
150
155
  // Wait for stable dom after navigation so we execute in
151
156
  // the correct context
@@ -157,6 +162,15 @@ export class WaitForHelper {
157
162
  finally {
158
163
  this.#abortController.abort();
159
164
  }
165
+ return this.#getResult();
166
+ }
167
+ #getResult() {
168
+ const urlAfterAction = this.#page.url();
169
+ return {
170
+ ...(urlAfterAction !== this.#initialUrl
171
+ ? { navigatedToUrl: urlAfterAction }
172
+ : {}),
173
+ };
160
174
  }
161
175
  }
162
176
  export function getNetworkMultiplierFromString(condition) {
@@ -3,13 +3,37 @@
3
3
  * Copyright 2026 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ import { execSync } from 'node:child_process';
6
7
  import fs from 'node:fs/promises';
7
8
  import path from 'node:path';
8
9
  import process from 'node:process';
10
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
11
+ function getRegistry() {
12
+ // Use the user's configured npm registry so update checks work behind
13
+ // corporate proxies and private registries. `npm config get registry`
14
+ // honors .npmrc files at every scope and respects npm_config_registry,
15
+ // so it covers direct CLI invocations as well as `npx` / `npm run`.
16
+ try {
17
+ const registry = execSync('npm config get registry', {
18
+ encoding: 'utf8',
19
+ stdio: ['ignore', 'pipe', 'ignore'],
20
+ timeout: 5000,
21
+ })
22
+ .trim()
23
+ .replace(/\/$/, '');
24
+ if (registry && registry !== 'undefined' && /^https?:\/\//.test(registry)) {
25
+ return registry;
26
+ }
27
+ }
28
+ catch {
29
+ // npm not on PATH or other errors, fall back to default.
30
+ }
31
+ return DEFAULT_REGISTRY;
32
+ }
9
33
  const cachePath = process.argv[2];
10
34
  if (cachePath) {
11
35
  try {
12
- const response = await fetch('https://registry.npmjs.org/chrome-devtools-mcp/latest');
36
+ const response = await fetch(`${getRegistry()}/chrome-devtools-mcp/latest`);
13
37
  const data = response.ok ? await response.json() : null;
14
38
  if (data &&
15
39
  typeof data === 'object' &&
@@ -114,7 +114,7 @@ export const commands = {
114
114
  geolocation: {
115
115
  name: 'geolocation',
116
116
  type: 'string',
117
- description: 'Geolocation (`<latitude>x<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.',
117
+ description: 'Geolocation (`<latitude>,<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.',
118
118
  required: false,
119
119
  },
120
120
  userAgent: {
@@ -154,6 +154,12 @@ export const commands = {
154
154
  description: 'An optional list of arguments to pass to the function.',
155
155
  required: false,
156
156
  },
157
+ filePath: {
158
+ name: 'filePath',
159
+ type: 'string',
160
+ description: 'The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline.',
161
+ required: false,
162
+ },
157
163
  dialogAction: {
158
164
  name: 'dialogAction',
159
165
  type: 'string',
@@ -282,6 +288,36 @@ export const commands = {
282
288
  },
283
289
  },
284
290
  },
291
+ get_node_retainers: {
292
+ description: 'Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --experimentalMemory=true)',
293
+ category: 'Memory',
294
+ args: {
295
+ filePath: {
296
+ name: 'filePath',
297
+ type: 'string',
298
+ description: 'A path to a .heapsnapshot file to read.',
299
+ required: true,
300
+ },
301
+ nodeId: {
302
+ name: 'nodeId',
303
+ type: 'number',
304
+ description: 'The stable node ID to get retainers for.',
305
+ required: true,
306
+ },
307
+ pageIdx: {
308
+ name: 'pageIdx',
309
+ type: 'number',
310
+ description: 'The page index for pagination.',
311
+ required: false,
312
+ },
313
+ pageSize: {
314
+ name: 'pageSize',
315
+ type: 'number',
316
+ description: 'The page size for pagination.',
317
+ required: false,
318
+ },
319
+ },
320
+ },
285
321
  get_nodes_by_class: {
286
322
  description: 'Loads a memory heapsnapshot and returns instances of a specific class with their stable IDs. (requires flag: --experimentalMemory=true)',
287
323
  category: 'Memory',
@@ -136,33 +136,27 @@ export const cliOptions = {
136
136
  },
137
137
  experimentalPageIdRouting: {
138
138
  type: 'boolean',
139
- describe: 'Whether to expose pageId on page-scoped tools and route requests by page ID.',
140
- hidden: true,
139
+ describe: 'Whether to expose pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions).',
141
140
  },
142
141
  experimentalDevtools: {
143
142
  type: 'boolean',
144
143
  describe: 'Whether to enable automation over DevTools targets',
145
- hidden: true,
146
144
  },
147
145
  experimentalVision: {
148
146
  type: 'boolean',
149
147
  describe: '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.',
150
- hidden: false,
151
148
  },
152
149
  experimentalMemory: {
153
150
  type: 'boolean',
154
151
  describe: 'Whether to enable experimental memory tools.',
155
- hidden: true,
156
152
  },
157
153
  experimentalStructuredContent: {
158
154
  type: 'boolean',
159
155
  describe: 'Whether to output structured formatted content.',
160
- hidden: true,
161
156
  },
162
157
  experimentalIncludeAllPages: {
163
158
  type: 'boolean',
164
159
  describe: 'Whether to include all kinds of pages such as webviews or background pages as pages.',
165
- hidden: true,
166
160
  },
167
161
  experimentalNavigationAllowlist: {
168
162
  type: 'boolean',
@@ -257,7 +251,7 @@ export const cliOptions = {
257
251
  },
258
252
  redactNetworkHeaders: {
259
253
  type: 'boolean',
260
- describe: 'If true, redacts some of the network headers considered senstive before returning to the client.',
254
+ describe: 'If true, redacts some of the network headers considered sensitive before returning to the client.',
261
255
  default: false,
262
256
  },
263
257
  };
@@ -115,13 +115,8 @@ export async function handleResponse(response, format) {
115
115
  if (response.isError) {
116
116
  return JSON.stringify(response.content);
117
117
  }
118
- if (format === 'json') {
119
- if (response.structuredContent) {
120
- return JSON.stringify(response.structuredContent);
121
- }
122
- // Fall-through to text for backward compatibility.
123
- }
124
118
  const chunks = [];
119
+ const images = [];
125
120
  for (const content of response.content) {
126
121
  if (content.type === 'text') {
127
122
  chunks.push(content.text);
@@ -143,12 +138,23 @@ export async function handleResponse(response, format) {
143
138
  const name = crypto.randomUUID();
144
139
  const filepath = await getTempFilePath(`${name}${extension}`);
145
140
  fs.writeFileSync(filepath, data);
141
+ images.push({ filePath: filepath, mimeType });
146
142
  chunks.push(`Saved to ${filepath}.`);
147
143
  }
148
144
  else {
149
145
  throw new Error('Not supported response content type');
150
146
  }
151
147
  }
148
+ if (format === 'json') {
149
+ if (response.structuredContent) {
150
+ const structuredContent = {
151
+ ...response.structuredContent,
152
+ ...(images.length ? { images } : {}),
153
+ };
154
+ return JSON.stringify(structuredContent);
155
+ }
156
+ // Fall-through to text for backward compatibility.
157
+ }
152
158
  return format === 'md' ? chunks.join(' ') : JSON.stringify(chunks);
153
159
  }
154
160
  //# sourceMappingURL=client.js.map