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.
- package/README.md +59 -10
- package/build/src/DevtoolsUtils.js +14 -1
- package/build/src/HeapSnapshotManager.js +42 -18
- package/build/src/McpContext.js +61 -23
- package/build/src/McpResponse.js +51 -21
- package/build/src/ToolHandler.js +30 -1
- 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 +81 -39
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
- package/build/src/bin/chrome-devtools-mcp-main.js +38 -0
- package/build/src/browser.js +36 -2
- package/build/src/daemon/client.js +12 -6
- package/build/src/daemon/daemon.js +62 -5
- package/build/src/formatters/HeapSnapshotFormatter.js +30 -9
- package/build/src/index.js +3 -1
- package/build/src/telemetry/ClearcutLogger.js +8 -119
- package/build/src/telemetry/errors.js +4 -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/third_party/THIRD_PARTY_NOTICES +4 -719
- package/build/src/third_party/bundled-packages.json +2 -2
- package/build/src/third_party/devtools-formatter-worker.js +447 -114
- package/build/src/third_party/devtools-heap-snapshot-worker.js +2 -3
- package/build/src/third_party/index.js +3443 -30153
- package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
- package/build/src/tools/ToolDefinition.js +2 -2
- package/build/src/tools/emulation.js +28 -2
- package/build/src/tools/extensions.js +2 -0
- package/build/src/tools/input.js +19 -10
- package/build/src/tools/lighthouse.js +1 -1
- package/build/src/tools/memory.js +39 -17
- package/build/src/tools/network.js +2 -2
- package/build/src/tools/performance.js +9 -6
- package/build/src/tools/screencast.js +1 -1
- package/build/src/tools/screenshot.js +1 -1
- package/build/src/tools/script.js +32 -10
- package/build/src/tools/snapshot.js +1 -1
- package/build/src/trace-processing/parse.js +2 -2
- package/build/src/utils/files.js +43 -0
- package/build/src/version.js +1 -1
- package/package.json +7 -4
- 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 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/)
|
|
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
|
|
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** (
|
|
518
|
-
- [`
|
|
519
|
-
- [`
|
|
520
|
-
- [`
|
|
521
|
-
- [`
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
34
|
+
const id = await this.getOrCreateIdForClassKey(filePath, key);
|
|
34
35
|
const aggregate = aggregates[key];
|
|
35
36
|
if (aggregate) {
|
|
36
|
-
aggregate[stableIdSymbol] =
|
|
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
|
|
50
|
+
async getOrCreateIdForClassKey(filePath, classKey) {
|
|
50
51
|
const cached = this.#getCachedSnapshot(filePath);
|
|
51
|
-
let
|
|
52
|
-
if (!
|
|
53
|
-
|
|
54
|
-
cached.
|
|
55
|
-
cached.
|
|
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
|
|
58
|
+
return id;
|
|
58
59
|
}
|
|
59
|
-
async
|
|
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.
|
|
63
|
+
const className = await this.resolveClassKeyFromId(filePath, id);
|
|
63
64
|
if (!className) {
|
|
64
|
-
throw new Error(`Class with
|
|
65
|
+
throw new Error(`Class with ID ${id} 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);
|
|
@@ -75,9 +99,9 @@ export class HeapSnapshotManager {
|
|
|
75
99
|
}
|
|
76
100
|
return cached;
|
|
77
101
|
}
|
|
78
|
-
async
|
|
102
|
+
async resolveClassKeyFromId(filePath, id) {
|
|
79
103
|
const cached = this.#getCachedSnapshot(filePath);
|
|
80
|
-
return cached.
|
|
104
|
+
return cached.idToClassKey.get(id);
|
|
81
105
|
}
|
|
82
106
|
async #loadSnapshot(absolutePath) {
|
|
83
107
|
const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
|
package/build/src/McpContext.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
601
|
-
this.
|
|
602
|
-
|
|
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
|
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}, 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
|
-
|
|
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 {
|
package/build/src/ToolHandler.js
CHANGED
|
@@ -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/
|
|
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;
|