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.
- package/README.md +55 -6
- package/build/src/DevtoolsUtils.js +13 -0
- package/build/src/HeapSnapshotManager.js +26 -2
- package/build/src/McpContext.js +3 -0
- package/build/src/McpResponse.js +51 -21
- package/build/src/ToolHandler.js +217 -0
- package/build/src/WaitForHelper.js +18 -4
- package/build/src/bin/check-latest-version.js +25 -1
- package/build/src/bin/chrome-devtools-cli-options.js +38 -2
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
- package/build/src/bin/chrome-devtools-mcp-main.js +4 -3
- package/build/src/bin/chrome-devtools.js +0 -2
- package/build/src/daemon/client.js +12 -6
- package/build/src/formatters/HeapSnapshotFormatter.js +27 -6
- package/build/src/index.js +11 -164
- package/build/src/telemetry/ClearcutLogger.js +34 -118
- package/build/src/telemetry/errors.js +18 -0
- package/build/src/telemetry/flagUtils.js +4 -3
- package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
- package/build/src/telemetry/persistence.js +20 -2
- package/build/src/telemetry/transformation.js +134 -0
- package/build/src/telemetry/types.js +0 -8
- package/build/src/third_party/THIRD_PARTY_NOTICES +140 -857
- package/build/src/third_party/bundled-packages.json +3 -3
- package/build/src/third_party/devtools-formatter-worker.js +475 -146
- package/build/src/third_party/devtools-heap-snapshot-worker.js +39 -44
- package/build/src/third_party/index.js +4055 -30401
- package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +4236 -4219
- package/build/src/tools/ToolDefinition.js +1 -1
- package/build/src/tools/emulation.js +3 -2
- package/build/src/tools/input.js +46 -16
- package/build/src/tools/lighthouse.js +7 -7
- package/build/src/tools/memory.js +24 -0
- package/build/src/tools/script.js +32 -10
- package/build/src/version.js +1 -1
- package/package.json +10 -7
- package/build/src/telemetry/metricUtils.js +0 -15
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Chrome DevTools for
|
|
1
|
+
# Chrome DevTools for agents
|
|
2
2
|
|
|
3
3
|
[](https://npmjs.org/package/chrome-devtools-mcp)
|
|
4
4
|
|
|
5
|
-
Chrome DevTools for
|
|
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/)
|
|
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** (
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
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);
|
package/build/src/McpContext.js
CHANGED
|
@@ -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
|
package/build/src/McpResponse.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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.#
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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.#
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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 (
|
|
704
|
-
|
|
705
|
-
|
|
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 (
|
|
708
|
-
!data.thirdPartyDeveloperTools
|
|
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
|
-
|
|
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(
|
|
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' &&
|