chrome-devtools-mcp 0.21.0 → 0.23.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 +87 -21
- package/build/src/HeapSnapshotManager.js +94 -0
- package/build/src/McpContext.js +26 -181
- package/build/src/McpPage.js +214 -0
- package/build/src/McpResponse.js +151 -13
- package/build/src/PageCollector.js +10 -24
- package/build/src/TextSnapshot.js +230 -0
- package/build/src/WaitForHelper.js +31 -0
- package/build/src/bin/check-latest-version.js +25 -0
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +34 -10
- package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
- package/build/src/bin/chrome-devtools.js +25 -14
- package/build/src/bin/cliDefinitions.js +14 -8
- package/build/src/daemon/client.js +11 -11
- package/build/src/daemon/daemon.js +6 -9
- package/build/src/daemon/utils.js +19 -14
- package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
- package/build/src/formatters/NetworkFormatter.js +24 -7
- package/build/src/index.js +12 -1
- package/build/src/telemetry/ClearcutLogger.js +34 -12
- package/build/src/telemetry/flagUtils.js +46 -4
- package/build/src/telemetry/toolMetricsUtils.js +88 -0
- package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
- package/build/src/third_party/THIRD_PARTY_NOTICES +59 -32
- package/build/src/third_party/bundled-packages.json +6 -4
- package/build/src/third_party/devtools-formatter-worker.js +61 -64
- package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
- package/build/src/third_party/index.js +62661 -60590
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +3501 -2658
- package/build/src/tools/categories.js +3 -0
- package/build/src/tools/console.js +42 -39
- package/build/src/tools/emulation.js +1 -1
- package/build/src/tools/extensions.js +5 -11
- package/build/src/tools/inPage.js +3 -13
- package/build/src/tools/input.js +15 -16
- package/build/src/tools/lighthouse.js +2 -2
- package/build/src/tools/memory.js +48 -3
- package/build/src/tools/network.js +4 -4
- package/build/src/tools/pages.js +212 -146
- package/build/src/tools/performance.js +1 -1
- package/build/src/tools/screencast.js +20 -8
- package/build/src/tools/screenshot.js +3 -3
- package/build/src/tools/script.js +22 -16
- package/build/src/tools/tools.js +2 -0
- package/build/src/tools/webmcp.js +63 -0
- package/build/src/utils/check-for-updates.js +73 -0
- package/build/src/utils/files.js +4 -0
- package/build/src/utils/id.js +15 -0
- package/build/src/version.js +1 -1
- package/package.json +13 -8
- package/build/src/utils/ExtensionRegistry.js +0 -35
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
export class TextSnapshot {
|
|
8
|
+
static nextSnapshotId = 1;
|
|
9
|
+
static resetCounter() {
|
|
10
|
+
TextSnapshot.nextSnapshotId = 1;
|
|
11
|
+
}
|
|
12
|
+
root;
|
|
13
|
+
idToNode;
|
|
14
|
+
snapshotId;
|
|
15
|
+
selectedElementUid;
|
|
16
|
+
hasSelectedElement;
|
|
17
|
+
verbose;
|
|
18
|
+
constructor(data) {
|
|
19
|
+
this.root = data.root;
|
|
20
|
+
this.idToNode = data.idToNode;
|
|
21
|
+
this.snapshotId = data.snapshotId;
|
|
22
|
+
this.selectedElementUid = data.selectedElementUid;
|
|
23
|
+
this.hasSelectedElement = data.hasSelectedElement;
|
|
24
|
+
this.verbose = data.verbose;
|
|
25
|
+
}
|
|
26
|
+
static async create(page, options = {}) {
|
|
27
|
+
const verbose = options.verbose ?? false;
|
|
28
|
+
const rootNode = await page.pptrPage.accessibility.snapshot({
|
|
29
|
+
includeIframes: true,
|
|
30
|
+
interestingOnly: !verbose,
|
|
31
|
+
});
|
|
32
|
+
if (!rootNode) {
|
|
33
|
+
throw new Error('Failed to create accessibility snapshot');
|
|
34
|
+
}
|
|
35
|
+
const { uniqueBackendNodeIdToMcpId } = page;
|
|
36
|
+
const snapshotId = TextSnapshot.nextSnapshotId++;
|
|
37
|
+
// Iterate through the whole accessibility node tree and assign node ids that
|
|
38
|
+
// will be used for the tree serialization and mapping ids back to nodes.
|
|
39
|
+
let idCounter = 0;
|
|
40
|
+
const idToNode = new Map();
|
|
41
|
+
const seenUniqueIds = new Set();
|
|
42
|
+
const seenBackendNodeIds = new Set();
|
|
43
|
+
const assignIds = (node) => {
|
|
44
|
+
let id = '';
|
|
45
|
+
// @ts-expect-error untyped backendNodeId.
|
|
46
|
+
const backendNodeId = node.backendNodeId;
|
|
47
|
+
// @ts-expect-error untyped loaderId.
|
|
48
|
+
const uniqueBackendId = `${node.loaderId}_${backendNodeId}`;
|
|
49
|
+
const existingMcpId = uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
|
|
50
|
+
if (existingMcpId !== undefined) {
|
|
51
|
+
// Re-use MCP exposed ID if the uniqueId is the same.
|
|
52
|
+
id = existingMcpId;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Only generate a new ID if we have not seen the node before.
|
|
56
|
+
id = `${snapshotId}_${idCounter++}`;
|
|
57
|
+
uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
|
|
58
|
+
}
|
|
59
|
+
seenUniqueIds.add(uniqueBackendId);
|
|
60
|
+
seenBackendNodeIds.add(backendNodeId);
|
|
61
|
+
const nodeWithId = {
|
|
62
|
+
...node,
|
|
63
|
+
id,
|
|
64
|
+
children: node.children
|
|
65
|
+
? node.children.map(child => assignIds(child))
|
|
66
|
+
: [],
|
|
67
|
+
};
|
|
68
|
+
// The AXNode for an option doesn't contain its `value`.
|
|
69
|
+
// Therefore, set text content of the option as value.
|
|
70
|
+
if (node.role === 'option') {
|
|
71
|
+
const optionText = node.name;
|
|
72
|
+
if (optionText) {
|
|
73
|
+
nodeWithId.value = optionText.toString();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
idToNode.set(nodeWithId.id, nodeWithId);
|
|
77
|
+
return nodeWithId;
|
|
78
|
+
};
|
|
79
|
+
const rootNodeWithId = assignIds(rootNode);
|
|
80
|
+
await TextSnapshot.insertExtraNodes(page, idToNode, seenUniqueIds, snapshotId, idCounter, rootNodeWithId, seenBackendNodeIds, options.extraHandles ?? []);
|
|
81
|
+
const snapshot = new TextSnapshot({
|
|
82
|
+
root: rootNodeWithId,
|
|
83
|
+
snapshotId: String(snapshotId),
|
|
84
|
+
idToNode,
|
|
85
|
+
hasSelectedElement: false,
|
|
86
|
+
verbose,
|
|
87
|
+
});
|
|
88
|
+
const data = options.devtoolsData ?? (await page.getDevToolsData());
|
|
89
|
+
if (data?.cdpBackendNodeId) {
|
|
90
|
+
snapshot.hasSelectedElement = true;
|
|
91
|
+
snapshot.selectedElementUid = page.resolveCdpElementId(data.cdpBackendNodeId);
|
|
92
|
+
}
|
|
93
|
+
// Clean up unique IDs that we did not see anymore.
|
|
94
|
+
for (const key of uniqueBackendNodeIdToMcpId.keys()) {
|
|
95
|
+
if (!seenUniqueIds.has(key)) {
|
|
96
|
+
uniqueBackendNodeIdToMcpId.delete(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return snapshot;
|
|
100
|
+
}
|
|
101
|
+
// ExtraHandles represent DOM nodes which might not be part of the accessibility tree, e.g. DOM nodes
|
|
102
|
+
// returned by in-page tools. We insert them into the tree by finding the closest ancestor in the
|
|
103
|
+
// tree and inserting the node as a child. The ancestor's child nodes are re-parented if necessary.
|
|
104
|
+
static async insertExtraNodes(page, idToNode, seenUniqueIds, snapshotId, idCounter, rootNodeWithId, seenBackendNodeIds, extraHandles) {
|
|
105
|
+
const { uniqueBackendNodeIdToMcpId } = page;
|
|
106
|
+
const createExtraNode = async (handle) => {
|
|
107
|
+
const backendNodeId = await handle.backendNodeId();
|
|
108
|
+
if (!backendNodeId || seenBackendNodeIds.has(backendNodeId)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const uniqueBackendId = `custom_${backendNodeId}`;
|
|
112
|
+
if (seenUniqueIds.has(uniqueBackendId)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
seenBackendNodeIds.add(backendNodeId);
|
|
116
|
+
let id = '';
|
|
117
|
+
const mcpId = uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
|
|
118
|
+
if (mcpId !== undefined) {
|
|
119
|
+
id = mcpId;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
id = `${snapshotId}_${idCounter++}`;
|
|
123
|
+
uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
|
|
124
|
+
}
|
|
125
|
+
seenUniqueIds.add(uniqueBackendId);
|
|
126
|
+
const tagHandle = await handle.getProperty('localName');
|
|
127
|
+
const tagValue = await tagHandle.jsonValue();
|
|
128
|
+
const extraNode = {
|
|
129
|
+
role: tagValue,
|
|
130
|
+
id,
|
|
131
|
+
backendNodeId,
|
|
132
|
+
children: [],
|
|
133
|
+
elementHandle: async () => handle,
|
|
134
|
+
};
|
|
135
|
+
return extraNode;
|
|
136
|
+
};
|
|
137
|
+
const findAncestorNode = async (handle) => {
|
|
138
|
+
let ancestorHandle = await handle.evaluateHandle(el => el.parentElement);
|
|
139
|
+
while (ancestorHandle) {
|
|
140
|
+
const ancestorElement = ancestorHandle.asElement();
|
|
141
|
+
if (!ancestorElement) {
|
|
142
|
+
await ancestorHandle.dispose();
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const ancestorBackendId = await ancestorElement.backendNodeId();
|
|
146
|
+
if (ancestorBackendId) {
|
|
147
|
+
const ancestorNode = idToNode
|
|
148
|
+
.values()
|
|
149
|
+
.find(node => node.backendNodeId === ancestorBackendId);
|
|
150
|
+
if (ancestorNode) {
|
|
151
|
+
await ancestorHandle.dispose();
|
|
152
|
+
return ancestorNode;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const nextHandle = await ancestorElement.evaluateHandle(el => el.parentElement);
|
|
156
|
+
await ancestorHandle.dispose();
|
|
157
|
+
ancestorHandle = nextHandle;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
const findDescendantNodes = async (backendNodeId) => {
|
|
162
|
+
const descendantIds = new Set();
|
|
163
|
+
try {
|
|
164
|
+
// @ts-expect-error internal API
|
|
165
|
+
const client = page.pptrPage._client();
|
|
166
|
+
if (client) {
|
|
167
|
+
const { node } = await client.send('DOM.describeNode', {
|
|
168
|
+
backendNodeId,
|
|
169
|
+
depth: -1,
|
|
170
|
+
pierce: true,
|
|
171
|
+
});
|
|
172
|
+
const collect = (node) => {
|
|
173
|
+
if (node.backendNodeId && node.backendNodeId !== backendNodeId) {
|
|
174
|
+
descendantIds.add(node.backendNodeId);
|
|
175
|
+
}
|
|
176
|
+
if (node.children) {
|
|
177
|
+
for (const child of node.children) {
|
|
178
|
+
collect(child);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
collect(node);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
logger(`Failed to collect descendants for backend node ${backendNodeId}`, e);
|
|
187
|
+
}
|
|
188
|
+
return descendantIds;
|
|
189
|
+
};
|
|
190
|
+
const moveChildNodes = (attachTarget, extraNode, descendantIds) => {
|
|
191
|
+
let firstMovedIndex = -1;
|
|
192
|
+
if (descendantIds.size > 0 && attachTarget.children) {
|
|
193
|
+
const remainingChildren = [];
|
|
194
|
+
for (const child of attachTarget.children) {
|
|
195
|
+
if (child.backendNodeId && descendantIds.has(child.backendNodeId)) {
|
|
196
|
+
if (firstMovedIndex === -1) {
|
|
197
|
+
firstMovedIndex = remainingChildren.length;
|
|
198
|
+
}
|
|
199
|
+
extraNode.children.push(child);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
remainingChildren.push(child);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
attachTarget.children = remainingChildren;
|
|
206
|
+
}
|
|
207
|
+
return firstMovedIndex !== -1
|
|
208
|
+
? firstMovedIndex
|
|
209
|
+
: attachTarget.children
|
|
210
|
+
? attachTarget.children.length
|
|
211
|
+
: 0;
|
|
212
|
+
};
|
|
213
|
+
if (extraHandles.length) {
|
|
214
|
+
page.extraHandles = extraHandles;
|
|
215
|
+
}
|
|
216
|
+
for (const handle of page.extraHandles) {
|
|
217
|
+
const extraNode = await createExtraNode(handle);
|
|
218
|
+
if (!extraNode) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
idToNode.set(extraNode.id, extraNode);
|
|
222
|
+
const attachTarget = (await findAncestorNode(handle)) || rootNodeWithId;
|
|
223
|
+
if (extraNode.backendNodeId !== undefined) {
|
|
224
|
+
const descendantIds = await findDescendantNodes(extraNode.backendNodeId);
|
|
225
|
+
const index = moveChildNodes(attachTarget, extraNode, descendantIds);
|
|
226
|
+
attachTarget.children.splice(index, 0, extraNode);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -104,6 +104,23 @@ export class WaitForHelper {
|
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
async waitForEventsAfterAction(action, options) {
|
|
107
|
+
if (options?.handleDialog) {
|
|
108
|
+
const dialogHandler = (dialog) => {
|
|
109
|
+
if (options.handleDialog === 'dismiss') {
|
|
110
|
+
void dialog.dismiss();
|
|
111
|
+
}
|
|
112
|
+
else if (options.handleDialog === 'accept') {
|
|
113
|
+
void dialog.accept();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
void dialog.accept(options.handleDialog);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
this.#page.on('dialog', dialogHandler);
|
|
120
|
+
this.#abortController.signal.addEventListener('abort', () => {
|
|
121
|
+
this.#page.off('dialog', dialogHandler);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
107
124
|
const navigationFinished = this.waitForNavigationStarted()
|
|
108
125
|
.then(navigationStated => {
|
|
109
126
|
if (navigationStated) {
|
|
@@ -137,3 +154,17 @@ export class WaitForHelper {
|
|
|
137
154
|
}
|
|
138
155
|
}
|
|
139
156
|
}
|
|
157
|
+
export function getNetworkMultiplierFromString(condition) {
|
|
158
|
+
const puppeteerCondition = condition;
|
|
159
|
+
switch (puppeteerCondition) {
|
|
160
|
+
case 'Fast 4G':
|
|
161
|
+
return 1;
|
|
162
|
+
case 'Slow 4G':
|
|
163
|
+
return 2.5;
|
|
164
|
+
case 'Fast 3G':
|
|
165
|
+
return 5;
|
|
166
|
+
case 'Slow 3G':
|
|
167
|
+
return 10;
|
|
168
|
+
}
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
const cachePath = process.argv[2];
|
|
10
|
+
if (cachePath) {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('https://registry.npmjs.org/chrome-devtools-mcp/latest');
|
|
13
|
+
const data = response.ok ? await response.json() : null;
|
|
14
|
+
if (data &&
|
|
15
|
+
typeof data === 'object' &&
|
|
16
|
+
'version' in data &&
|
|
17
|
+
typeof data.version === 'string') {
|
|
18
|
+
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
|
19
|
+
await fs.writeFile(cachePath, JSON.stringify({ version: data.version }));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Ignore errors.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -7,8 +7,8 @@ import { yargs, hideBin } from '../third_party/index.js';
|
|
|
7
7
|
export const cliOptions = {
|
|
8
8
|
autoConnect: {
|
|
9
9
|
type: 'boolean',
|
|
10
|
-
description: 'If specified, automatically connects to a browser (Chrome 144+) running locally from the user data directory identified by the channel param (default channel is stable). Requires the
|
|
11
|
-
conflicts: ['isolated', 'executablePath'
|
|
10
|
+
description: 'If specified, automatically connects to a browser (Chrome 144+) running locally from the user data directory identified by the channel param (default channel is stable). Requires the remote debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.',
|
|
11
|
+
conflicts: ['isolated', 'executablePath'],
|
|
12
12
|
default: false,
|
|
13
13
|
coerce: (value) => {
|
|
14
14
|
if (!value) {
|
|
@@ -21,7 +21,7 @@ export const cliOptions = {
|
|
|
21
21
|
type: 'string',
|
|
22
22
|
description: 'Connect to a running, debuggable Chrome instance (e.g. `http://127.0.0.1:9222`). For more details see: https://github.com/ChromeDevTools/chrome-devtools-mcp#connecting-to-a-running-chrome-instance.',
|
|
23
23
|
alias: 'u',
|
|
24
|
-
conflicts: ['wsEndpoint'
|
|
24
|
+
conflicts: ['wsEndpoint'],
|
|
25
25
|
coerce: (url) => {
|
|
26
26
|
if (!url) {
|
|
27
27
|
return;
|
|
@@ -39,7 +39,7 @@ export const cliOptions = {
|
|
|
39
39
|
type: 'string',
|
|
40
40
|
description: 'WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.',
|
|
41
41
|
alias: 'w',
|
|
42
|
-
conflicts: ['browserUrl'
|
|
42
|
+
conflicts: ['browserUrl'],
|
|
43
43
|
coerce: (url) => {
|
|
44
44
|
if (!url) {
|
|
45
45
|
return;
|
|
@@ -102,7 +102,7 @@ export const cliOptions = {
|
|
|
102
102
|
channel: {
|
|
103
103
|
type: 'string',
|
|
104
104
|
description: 'Specify a different Chrome channel that should be used. The default is the stable channel version.',
|
|
105
|
-
choices: ['
|
|
105
|
+
choices: ['canary', 'dev', 'beta', 'stable'],
|
|
106
106
|
conflicts: ['browserUrl', 'wsEndpoint', 'executablePath'],
|
|
107
107
|
},
|
|
108
108
|
logFile: {
|
|
@@ -146,7 +146,12 @@ export const cliOptions = {
|
|
|
146
146
|
},
|
|
147
147
|
experimentalVision: {
|
|
148
148
|
type: 'boolean',
|
|
149
|
-
describe: 'Whether to enable
|
|
149
|
+
describe: 'Whether to enable coordinate-based tools such as click_at(x,y). Usually requires a computer-use model able to produce accurate coordinates by looking at screenshots.',
|
|
150
|
+
hidden: false,
|
|
151
|
+
},
|
|
152
|
+
experimentalMemory: {
|
|
153
|
+
type: 'boolean',
|
|
154
|
+
describe: 'Whether to enable experimental memory tools.',
|
|
150
155
|
hidden: true,
|
|
151
156
|
},
|
|
152
157
|
experimentalStructuredContent: {
|
|
@@ -159,6 +164,11 @@ export const cliOptions = {
|
|
|
159
164
|
describe: 'Whether to include all kinds of pages such as webviews or background pages as pages.',
|
|
160
165
|
hidden: true,
|
|
161
166
|
},
|
|
167
|
+
experimentalNavigationAllowlist: {
|
|
168
|
+
type: 'boolean',
|
|
169
|
+
describe: 'Whether to enable navigation allowlist tool parameter.',
|
|
170
|
+
hidden: true,
|
|
171
|
+
},
|
|
162
172
|
experimentalInteropTools: {
|
|
163
173
|
type: 'boolean',
|
|
164
174
|
describe: 'Whether to enable interoperability tools',
|
|
@@ -168,6 +178,15 @@ export const cliOptions = {
|
|
|
168
178
|
type: 'boolean',
|
|
169
179
|
describe: 'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
|
|
170
180
|
},
|
|
181
|
+
experimentalFfmpegPath: {
|
|
182
|
+
type: 'string',
|
|
183
|
+
describe: 'Path to ffmpeg executable for screencast recording.',
|
|
184
|
+
implies: 'experimentalScreencast',
|
|
185
|
+
},
|
|
186
|
+
experimentalWebmcp: {
|
|
187
|
+
type: 'boolean',
|
|
188
|
+
describe: 'Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`',
|
|
189
|
+
},
|
|
171
190
|
chromeArg: {
|
|
172
191
|
type: 'array',
|
|
173
192
|
describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
|
|
@@ -193,9 +212,9 @@ export const cliOptions = {
|
|
|
193
212
|
},
|
|
194
213
|
categoryExtensions: {
|
|
195
214
|
type: 'boolean',
|
|
196
|
-
hidden:
|
|
197
|
-
|
|
198
|
-
describe: 'Set to true to include tools related to extensions. Note: This feature is only supported with a pipe connection. autoConnect
|
|
215
|
+
hidden: false,
|
|
216
|
+
default: false,
|
|
217
|
+
describe: 'Set to true to include tools related to extensions. Note: This feature is currently only supported with a pipe connection. autoConnect, browserUrl, and wsEndpoint are not supported with this feature until 149 will be released.',
|
|
199
218
|
},
|
|
200
219
|
categoryInPageTools: {
|
|
201
220
|
type: 'boolean',
|
|
@@ -210,7 +229,7 @@ export const cliOptions = {
|
|
|
210
229
|
usageStatistics: {
|
|
211
230
|
type: 'boolean',
|
|
212
231
|
default: true,
|
|
213
|
-
describe: 'Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.',
|
|
232
|
+
describe: 'Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if `CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS` or `CI` env variables are set.',
|
|
214
233
|
},
|
|
215
234
|
clearcutEndpoint: {
|
|
216
235
|
type: 'string',
|
|
@@ -236,6 +255,11 @@ export const cliOptions = {
|
|
|
236
255
|
describe: 'Set by Chrome DevTools CLI if the MCP server is started via the CLI client (this arg exists for usage stats)',
|
|
237
256
|
hidden: true,
|
|
238
257
|
},
|
|
258
|
+
redactNetworkHeaders: {
|
|
259
|
+
type: 'boolean',
|
|
260
|
+
describe: 'If true, redacts some of the network headers considered senstive before returning to the client.',
|
|
261
|
+
default: false,
|
|
262
|
+
},
|
|
239
263
|
};
|
|
240
264
|
export function parseArguments(version, argv = process.argv) {
|
|
241
265
|
const yargsInstance = yargs(hideBin(argv))
|
|
@@ -9,8 +9,10 @@ import { createMcpServer, logDisclaimers } from '../index.js';
|
|
|
9
9
|
import { logger, saveLogsToFile } from '../logger.js';
|
|
10
10
|
import { computeFlagUsage } from '../telemetry/flagUtils.js';
|
|
11
11
|
import { StdioServerTransport } from '../third_party/index.js';
|
|
12
|
+
import { checkForUpdates } from '../utils/check-for-updates.js';
|
|
12
13
|
import { VERSION } from '../version.js';
|
|
13
14
|
import { cliOptions, parseArguments } from './chrome-devtools-mcp-cli-options.js';
|
|
15
|
+
await checkForUpdates('Run `npm install chrome-devtools-mcp@latest` to update.');
|
|
14
16
|
export const args = parseArguments(VERSION);
|
|
15
17
|
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
16
18
|
if (process.env['CI'] ||
|
|
@@ -10,12 +10,14 @@ import { startDaemon, stopDaemon, sendCommand, handleResponse, } from '../daemon
|
|
|
10
10
|
import { isDaemonRunning, serializeArgs } from '../daemon/utils.js';
|
|
11
11
|
import { logDisclaimers } from '../index.js';
|
|
12
12
|
import { hideBin, yargs } from '../third_party/index.js';
|
|
13
|
+
import { checkForUpdates } from '../utils/check-for-updates.js';
|
|
13
14
|
import { VERSION } from '../version.js';
|
|
14
15
|
import { commands } from './chrome-devtools-cli-options.js';
|
|
15
16
|
import { cliOptions, parseArguments } from './chrome-devtools-mcp-cli-options.js';
|
|
16
|
-
|
|
17
|
+
await checkForUpdates('Run `npm install -g chrome-devtools-mcp@latest` and `chrome-devtools start` to update and restart the daemon.');
|
|
18
|
+
async function start(args, sessionId) {
|
|
17
19
|
const combinedArgs = [...args, ...defaultArgs];
|
|
18
|
-
await startDaemon(combinedArgs);
|
|
20
|
+
await startDaemon(combinedArgs, sessionId);
|
|
19
21
|
logDisclaimers(parseArguments(VERSION, combinedArgs));
|
|
20
22
|
}
|
|
21
23
|
const defaultArgs = ['--viaCli', '--experimentalStructuredContent'];
|
|
@@ -30,6 +32,7 @@ delete startCliOptions.viewport;
|
|
|
30
32
|
// tools, they need to be enabled during CLI generation.
|
|
31
33
|
delete startCliOptions.experimentalPageIdRouting;
|
|
32
34
|
delete startCliOptions.experimentalVision;
|
|
35
|
+
delete startCliOptions.experimentalWebmcp;
|
|
33
36
|
delete startCliOptions.experimentalInteropTools;
|
|
34
37
|
delete startCliOptions.experimentalScreencast;
|
|
35
38
|
delete startCliOptions.categoryEmulation;
|
|
@@ -53,6 +56,12 @@ const y = yargs(hideBin(process.argv))
|
|
|
53
56
|
.showHelpOnFail(true)
|
|
54
57
|
.usage('chrome-devtools <command> [...args] --flags')
|
|
55
58
|
.usage(`Run 'chrome-devtools <command> --help' for help on the specific command.`)
|
|
59
|
+
.option('sessionId', {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Session ID for daemon scoping',
|
|
62
|
+
default: '',
|
|
63
|
+
hidden: true,
|
|
64
|
+
})
|
|
56
65
|
.demandCommand()
|
|
57
66
|
.version(VERSION)
|
|
58
67
|
.strict()
|
|
@@ -62,8 +71,8 @@ y.command('start', 'Start or restart chrome-devtools-mcp', y => y
|
|
|
62
71
|
.options(startCliOptions)
|
|
63
72
|
.example('$0 start --browserUrl http://localhost:9222', 'Start the server connecting to an existing browser')
|
|
64
73
|
.strict(), async (argv) => {
|
|
65
|
-
if (isDaemonRunning()) {
|
|
66
|
-
await stopDaemon();
|
|
74
|
+
if (isDaemonRunning(argv.sessionId)) {
|
|
75
|
+
await stopDaemon(argv.sessionId);
|
|
67
76
|
}
|
|
68
77
|
// Defaults but we do not want to affect the yargs conflict resolution.
|
|
69
78
|
if (argv.isolated === undefined && argv.userDataDir === undefined) {
|
|
@@ -73,15 +82,15 @@ y.command('start', 'Start or restart chrome-devtools-mcp', y => y
|
|
|
73
82
|
argv.headless = true;
|
|
74
83
|
}
|
|
75
84
|
const args = serializeArgs(cliOptions, argv);
|
|
76
|
-
await start(args);
|
|
85
|
+
await start(args, argv.sessionId);
|
|
77
86
|
process.exit(0);
|
|
78
87
|
}).strict(); // Re-enable strict validation for other commands; this is applied to the yargs instance itself
|
|
79
|
-
y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
|
|
80
|
-
if (isDaemonRunning()) {
|
|
88
|
+
y.command('status', 'Checks if chrome-devtools-mcp is running', y => y, async (argv) => {
|
|
89
|
+
if (isDaemonRunning(argv.sessionId)) {
|
|
81
90
|
console.log('chrome-devtools-mcp daemon is running.');
|
|
82
91
|
const response = await sendCommand({
|
|
83
92
|
method: 'status',
|
|
84
|
-
});
|
|
93
|
+
}, argv.sessionId);
|
|
85
94
|
if (response.success) {
|
|
86
95
|
const data = JSON.parse(response.result);
|
|
87
96
|
console.log(`pid=${data.pid} socket=${data.socketPath} start-date=${data.startDate} version=${data.version}`);
|
|
@@ -97,11 +106,12 @@ y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
|
|
|
97
106
|
}
|
|
98
107
|
process.exit(0);
|
|
99
108
|
});
|
|
100
|
-
y.command('stop', 'Stop chrome-devtools-mcp if any', async () => {
|
|
101
|
-
|
|
109
|
+
y.command('stop', 'Stop chrome-devtools-mcp if any', y => y, async (argv) => {
|
|
110
|
+
const sessionId = argv.sessionId;
|
|
111
|
+
if (!isDaemonRunning(sessionId)) {
|
|
102
112
|
process.exit(0);
|
|
103
113
|
}
|
|
104
|
-
await stopDaemon();
|
|
114
|
+
await stopDaemon(sessionId);
|
|
105
115
|
process.exit(0);
|
|
106
116
|
});
|
|
107
117
|
for (const [commandName, commandDef] of Object.entries(commands)) {
|
|
@@ -156,9 +166,10 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
|
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
}, async (argv) => {
|
|
169
|
+
const sessionId = argv.sessionId;
|
|
159
170
|
try {
|
|
160
|
-
if (!isDaemonRunning()) {
|
|
161
|
-
await start([]);
|
|
171
|
+
if (!isDaemonRunning(sessionId)) {
|
|
172
|
+
await start([], sessionId);
|
|
162
173
|
}
|
|
163
174
|
const commandArgs = {};
|
|
164
175
|
for (const argName of Object.keys(args)) {
|
|
@@ -170,7 +181,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
|
|
|
170
181
|
method: 'invoke_tool',
|
|
171
182
|
tool: commandName,
|
|
172
183
|
args: commandArgs,
|
|
173
|
-
});
|
|
184
|
+
}, sessionId);
|
|
174
185
|
if (response.success) {
|
|
175
186
|
console.log(await handleResponse(JSON.parse(response.result), argv['output-format']));
|
|
176
187
|
}
|
|
@@ -84,7 +84,7 @@ export const commands = {
|
|
|
84
84
|
geolocation: {
|
|
85
85
|
name: 'geolocation',
|
|
86
86
|
type: 'string',
|
|
87
|
-
description: 'Geolocation (`<latitude>x<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit clear the geolocation override.',
|
|
87
|
+
description: 'Geolocation (`<latitude>x<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.',
|
|
88
88
|
required: false,
|
|
89
89
|
},
|
|
90
90
|
userAgent: {
|
|
@@ -124,10 +124,16 @@ export const commands = {
|
|
|
124
124
|
description: 'An optional list of arguments to pass to the function.',
|
|
125
125
|
required: false,
|
|
126
126
|
},
|
|
127
|
+
dialogAction: {
|
|
128
|
+
name: 'dialogAction',
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.',
|
|
131
|
+
required: false,
|
|
132
|
+
},
|
|
127
133
|
},
|
|
128
134
|
},
|
|
129
135
|
fill: {
|
|
130
|
-
description: 'Type text into
|
|
136
|
+
description: 'Type text into an input, text area or select an option from a <select> element.',
|
|
131
137
|
category: 'Input automation',
|
|
132
138
|
args: {
|
|
133
139
|
uid: {
|
|
@@ -175,13 +181,13 @@ export const commands = {
|
|
|
175
181
|
requestFilePath: {
|
|
176
182
|
name: 'requestFilePath',
|
|
177
183
|
type: 'string',
|
|
178
|
-
description: 'The absolute or relative path to save the request body to. If omitted, the body is returned inline.',
|
|
184
|
+
description: 'The absolute or relative path to a .network-request file to save the request body to. If omitted, the body is returned inline.',
|
|
179
185
|
required: false,
|
|
180
186
|
},
|
|
181
187
|
responseFilePath: {
|
|
182
188
|
name: 'responseFilePath',
|
|
183
189
|
type: 'string',
|
|
184
|
-
description: 'The absolute or relative path to save the response body to. If omitted, the body is returned inline.',
|
|
190
|
+
description: 'The absolute or relative path to a .network-response file to save the response body to. If omitted, the body is returned inline.',
|
|
185
191
|
required: false,
|
|
186
192
|
},
|
|
187
193
|
},
|
|
@@ -258,7 +264,7 @@ export const commands = {
|
|
|
258
264
|
pageSize: {
|
|
259
265
|
name: 'pageSize',
|
|
260
266
|
type: 'integer',
|
|
261
|
-
description: 'Maximum number of messages to return. When omitted, returns all
|
|
267
|
+
description: 'Maximum number of messages to return. When omitted, returns all messages.',
|
|
262
268
|
required: false,
|
|
263
269
|
},
|
|
264
270
|
pageIdx: {
|
|
@@ -314,7 +320,7 @@ export const commands = {
|
|
|
314
320
|
},
|
|
315
321
|
},
|
|
316
322
|
list_pages: {
|
|
317
|
-
description: 'Get a list of pages
|
|
323
|
+
description: 'Get a list of pages open in the browser.',
|
|
318
324
|
category: 'Navigation automation',
|
|
319
325
|
args: {},
|
|
320
326
|
},
|
|
@@ -504,7 +510,7 @@ export const commands = {
|
|
|
504
510
|
},
|
|
505
511
|
take_memory_snapshot: {
|
|
506
512
|
description: 'Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.',
|
|
507
|
-
category: '
|
|
513
|
+
category: 'Memory',
|
|
508
514
|
args: {
|
|
509
515
|
filePath: {
|
|
510
516
|
name: 'filePath',
|
|
@@ -535,7 +541,7 @@ export const commands = {
|
|
|
535
541
|
uid: {
|
|
536
542
|
name: 'uid',
|
|
537
543
|
type: 'string',
|
|
538
|
-
description: 'The uid of an element on the page from the page content snapshot. If omitted takes a
|
|
544
|
+
description: 'The uid of an element on the page from the page content snapshot. If omitted, takes a page screenshot.',
|
|
539
545
|
required: false,
|
|
540
546
|
},
|
|
541
547
|
fullPage: {
|
|
@@ -48,12 +48,12 @@ function waitForFile(filePath, removed = false) {
|
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
-
export async function startDaemon(mcpArgs = []) {
|
|
52
|
-
if (isDaemonRunning()) {
|
|
51
|
+
export async function startDaemon(mcpArgs = [], sessionId) {
|
|
52
|
+
if (isDaemonRunning(sessionId)) {
|
|
53
53
|
logger('Daemon is already running');
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
-
const pidFilePath = getPidFilePath();
|
|
56
|
+
const pidFilePath = getPidFilePath(sessionId);
|
|
57
57
|
if (fs.existsSync(pidFilePath)) {
|
|
58
58
|
fs.unlinkSync(pidFilePath);
|
|
59
59
|
}
|
|
@@ -61,7 +61,7 @@ export async function startDaemon(mcpArgs = []) {
|
|
|
61
61
|
const child = spawn(process.execPath, [DAEMON_SCRIPT_PATH, ...mcpArgs], {
|
|
62
62
|
detached: true,
|
|
63
63
|
stdio: 'ignore',
|
|
64
|
-
env: process.env,
|
|
64
|
+
env: { ...process.env, CHROME_DEVTOOLS_MCP_SESSION_ID: sessionId },
|
|
65
65
|
cwd: process.cwd(),
|
|
66
66
|
windowsHide: true,
|
|
67
67
|
});
|
|
@@ -72,8 +72,8 @@ const SEND_COMMAND_TIMEOUT = 60_000; // ms
|
|
|
72
72
|
/**
|
|
73
73
|
* `sendCommand` opens a socket connection sends a single command and disconnects.
|
|
74
74
|
*/
|
|
75
|
-
export async function sendCommand(command) {
|
|
76
|
-
const socketPath = getSocketPath();
|
|
75
|
+
export async function sendCommand(command, sessionId) {
|
|
76
|
+
const socketPath = getSocketPath(sessionId);
|
|
77
77
|
const socket = net.createConnection({
|
|
78
78
|
path: socketPath,
|
|
79
79
|
});
|
|
@@ -102,13 +102,13 @@ export async function sendCommand(command) {
|
|
|
102
102
|
transport.send(JSON.stringify(command));
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
|
-
export async function stopDaemon() {
|
|
106
|
-
if (!isDaemonRunning()) {
|
|
105
|
+
export async function stopDaemon(sessionId) {
|
|
106
|
+
if (!isDaemonRunning(sessionId)) {
|
|
107
107
|
logger('Daemon is not running');
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
|
-
const pidFilePath = getPidFilePath();
|
|
111
|
-
await sendCommand({ method: 'stop' });
|
|
110
|
+
const pidFilePath = getPidFilePath(sessionId);
|
|
111
|
+
await sendCommand({ method: 'stop' }, sessionId);
|
|
112
112
|
await waitForFile(pidFilePath, /*removed=*/ true);
|
|
113
113
|
}
|
|
114
114
|
export async function handleResponse(response, format) {
|
|
@@ -135,7 +135,7 @@ export async function handleResponse(response, format) {
|
|
|
135
135
|
case 'image/jpeg':
|
|
136
136
|
extension = '.jpeg';
|
|
137
137
|
break;
|
|
138
|
-
case 'webp':
|
|
138
|
+
case 'image/webp':
|
|
139
139
|
extension = '.webp';
|
|
140
140
|
break;
|
|
141
141
|
}
|