chrome-devtools-mcp 0.25.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 (38) hide show
  1. package/README.md +55 -6
  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 +217 -0
  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 +38 -2
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
  11. package/build/src/bin/chrome-devtools-mcp-main.js +4 -3
  12. package/build/src/bin/chrome-devtools.js +0 -2
  13. package/build/src/daemon/client.js +12 -6
  14. package/build/src/formatters/HeapSnapshotFormatter.js +27 -6
  15. package/build/src/index.js +11 -164
  16. package/build/src/telemetry/ClearcutLogger.js +34 -118
  17. package/build/src/telemetry/errors.js +18 -0
  18. package/build/src/telemetry/flagUtils.js +4 -3
  19. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
  20. package/build/src/telemetry/persistence.js +20 -2
  21. package/build/src/telemetry/transformation.js +134 -0
  22. package/build/src/telemetry/types.js +0 -8
  23. package/build/src/third_party/THIRD_PARTY_NOTICES +140 -857
  24. package/build/src/third_party/bundled-packages.json +3 -3
  25. package/build/src/third_party/devtools-formatter-worker.js +475 -146
  26. package/build/src/third_party/devtools-heap-snapshot-worker.js +39 -44
  27. package/build/src/third_party/index.js +4055 -30401
  28. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  29. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +4236 -4219
  30. package/build/src/tools/ToolDefinition.js +1 -1
  31. package/build/src/tools/emulation.js +3 -2
  32. package/build/src/tools/input.js +46 -16
  33. package/build/src/tools/lighthouse.js +7 -7
  34. package/build/src/tools/memory.js +24 -0
  35. package/build/src/tools/script.js +32 -10
  36. package/build/src/version.js +1 -1
  37. package/package.json +10 -7
  38. 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
 
@@ -161,7 +161,7 @@ To install Chrome DevTools MCP with skills, add the marketplace registry in Clau
161
161
  Then, install the plugin:
162
162
 
163
163
  ```sh
164
- /plugin install chrome-devtools-mcp
164
+ /plugin install chrome-devtools-mcp@chrome-devtools-plugins
165
165
  ```
166
166
 
167
167
  Restart Claude Code to have the MCP server and skills load (check with `/skills`).
@@ -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 {
@@ -0,0 +1,217 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { logger } from './logger.js';
7
+ import { McpResponse } from './McpResponse.js';
8
+ import { SlimMcpResponse } from './SlimMcpResponse.js';
9
+ import { ClearcutLogger } from './telemetry/ClearcutLogger.js';
10
+ import { bucketizeLatency } from './telemetry/transformation.js';
11
+ import { zod } from './third_party/index.js';
12
+ import { labels, OFF_BY_DEFAULT_CATEGORIES } from './tools/categories.js';
13
+ import { pageIdSchema } from './tools/ToolDefinition.js';
14
+ export function buildFlag(category) {
15
+ return `category${category.charAt(0).toUpperCase() + category.slice(1)}`;
16
+ }
17
+ function buildDisabledMessage(toolName, flag, categoryLabel) {
18
+ const reason = categoryLabel
19
+ ? `is in category ${categoryLabel} which`
20
+ : `requires experimental feature ${flag} and`;
21
+ return `Tool ${toolName} ${reason} is currently disabled. Enable it by running chrome-devtools start ${flag}=true. For more information check the README.`;
22
+ }
23
+ function getCategoryStatus(category, serverArgs) {
24
+ const categoryFlag = buildFlag(category);
25
+ const flagValue = serverArgs[categoryFlag];
26
+ const isDisabled = OFF_BY_DEFAULT_CATEGORIES.includes(category)
27
+ ? !flagValue
28
+ : flagValue === false;
29
+ if (isDisabled) {
30
+ return {
31
+ categoryFlag,
32
+ disabled: true,
33
+ };
34
+ }
35
+ return {
36
+ disabled: false,
37
+ };
38
+ }
39
+ function getConditionStatus(condition, serverArgs) {
40
+ if (condition && !serverArgs[condition]) {
41
+ return { conditionFlag: condition, disabled: true };
42
+ }
43
+ return { disabled: false };
44
+ }
45
+ function getToolStatusInfo(tool, serverArgs) {
46
+ const category = tool.annotations.category;
47
+ const categoryCheck = getCategoryStatus(category, serverArgs);
48
+ if (category && categoryCheck.disabled) {
49
+ if (!categoryCheck.categoryFlag) {
50
+ throw new Error('when the category is disabled there should always be a flag set');
51
+ }
52
+ return {
53
+ disabled: true,
54
+ reason: buildDisabledMessage(tool.name, `--${categoryCheck.categoryFlag}`, labels[category]),
55
+ };
56
+ }
57
+ for (const condition of tool.annotations.conditions || []) {
58
+ const conditionCheck = getConditionStatus(condition, serverArgs);
59
+ if (conditionCheck.disabled) {
60
+ if (!conditionCheck.conditionFlag) {
61
+ throw new Error('when the condition is disabled there should always be a flag set');
62
+ }
63
+ return {
64
+ disabled: true,
65
+ reason: buildDisabledMessage(tool.name, `--${conditionCheck.conditionFlag}`),
66
+ };
67
+ }
68
+ }
69
+ return { disabled: false };
70
+ }
71
+ function isPageScopedTool(tool) {
72
+ return 'pageScoped' in tool && tool.pageScoped === true;
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
+ }
85
+ export class ToolHandler {
86
+ tool;
87
+ serverArgs;
88
+ getContext;
89
+ toolMutex;
90
+ inputSchema;
91
+ registeredInputSchema;
92
+ shouldRegister;
93
+ disabledReason;
94
+ constructor(tool, serverArgs, getContext, toolMutex) {
95
+ this.tool = tool;
96
+ this.serverArgs = serverArgs;
97
+ this.getContext = getContext;
98
+ this.toolMutex = toolMutex;
99
+ const { disabled, reason } = getToolStatusInfo(tool, serverArgs);
100
+ this.disabledReason = reason;
101
+ this.shouldRegister = !(disabled && !serverArgs.viaCli);
102
+ this.inputSchema =
103
+ 'pageScoped' in tool &&
104
+ tool.pageScoped &&
105
+ serverArgs.experimentalPageIdRouting &&
106
+ !serverArgs.slim
107
+ ? { ...tool.schema, ...pageIdSchema }
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));
113
+ }
114
+ async handle(params) {
115
+ if (this.disabledReason) {
116
+ return {
117
+ content: [
118
+ {
119
+ type: 'text',
120
+ text: this.disabledReason,
121
+ },
122
+ ],
123
+ isError: true,
124
+ };
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
+ }
138
+ const guard = await this.toolMutex.acquire();
139
+ const startTime = Date.now();
140
+ let success = false;
141
+ try {
142
+ logger(`${this.tool.name} request: ${JSON.stringify(params, null, ' ')}`);
143
+ const context = await this.getContext();
144
+ logger(`${this.tool.name} context: resolved`);
145
+ await context.detectOpenDevToolsWindows();
146
+ const response = this.serverArgs.slim
147
+ ? new SlimMcpResponse(this.serverArgs)
148
+ : new McpResponse(this.serverArgs);
149
+ response.setRedactNetworkHeaders(this.serverArgs.redactNetworkHeaders);
150
+ try {
151
+ if (isPageScopedTool(this.tool)) {
152
+ const pageId = typeof params.pageId === 'number' ? params.pageId : undefined;
153
+ const page = this.serverArgs.experimentalPageIdRouting &&
154
+ pageId !== undefined &&
155
+ !this.serverArgs.slim
156
+ ? context.getPageById(pageId)
157
+ : context.getSelectedMcpPage();
158
+ response.setPage(page);
159
+ if (this.tool.blockedByDialog) {
160
+ page.throwIfDialogOpen();
161
+ }
162
+ await this.tool.handler({
163
+ params,
164
+ page,
165
+ }, response, context);
166
+ }
167
+ else {
168
+ await this.tool.handler({
169
+ params,
170
+ }, response, context);
171
+ }
172
+ }
173
+ catch (err) {
174
+ response.setError(err);
175
+ }
176
+ const { content, structuredContent } = await response.handle(this.tool.name, context);
177
+ const result = {
178
+ content,
179
+ };
180
+ if (response.error) {
181
+ result.isError = true;
182
+ }
183
+ success = true;
184
+ if (this.serverArgs.experimentalStructuredContent) {
185
+ result.structuredContent = structuredContent;
186
+ }
187
+ return result;
188
+ }
189
+ catch (err) {
190
+ logger(`${this.tool.name} error:`, err, err?.stack);
191
+ let errorText = err && 'message' in err ? err.message : String(err);
192
+ if ('cause' in err && err.cause) {
193
+ errorText += `\nCause: ${err.cause.message}`;
194
+ }
195
+ return {
196
+ content: [
197
+ {
198
+ type: 'text',
199
+ text: errorText,
200
+ },
201
+ ],
202
+ isError: true,
203
+ };
204
+ }
205
+ finally {
206
+ void ClearcutLogger.get()?.logToolInvocation({
207
+ toolName: this.tool.name,
208
+ params,
209
+ schema: this.inputSchema,
210
+ success,
211
+ latencyMs: bucketizeLatency(Date.now() - startTime),
212
+ });
213
+ guard.dispose();
214
+ }
215
+ }
216
+ }
217
+ //# sourceMappingURL=ToolHandler.js.map
@@ -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' &&