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.
Files changed (51) hide show
  1. package/README.md +87 -21
  2. package/build/src/HeapSnapshotManager.js +94 -0
  3. package/build/src/McpContext.js +26 -181
  4. package/build/src/McpPage.js +214 -0
  5. package/build/src/McpResponse.js +151 -13
  6. package/build/src/PageCollector.js +10 -24
  7. package/build/src/TextSnapshot.js +230 -0
  8. package/build/src/WaitForHelper.js +31 -0
  9. package/build/src/bin/check-latest-version.js +25 -0
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +34 -10
  11. package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
  12. package/build/src/bin/chrome-devtools.js +25 -14
  13. package/build/src/bin/cliDefinitions.js +14 -8
  14. package/build/src/daemon/client.js +11 -11
  15. package/build/src/daemon/daemon.js +6 -9
  16. package/build/src/daemon/utils.js +19 -14
  17. package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
  18. package/build/src/formatters/NetworkFormatter.js +24 -7
  19. package/build/src/index.js +12 -1
  20. package/build/src/telemetry/ClearcutLogger.js +34 -12
  21. package/build/src/telemetry/flagUtils.js +46 -4
  22. package/build/src/telemetry/toolMetricsUtils.js +88 -0
  23. package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
  24. package/build/src/third_party/THIRD_PARTY_NOTICES +59 -32
  25. package/build/src/third_party/bundled-packages.json +6 -4
  26. package/build/src/third_party/devtools-formatter-worker.js +61 -64
  27. package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
  28. package/build/src/third_party/index.js +62661 -60590
  29. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +3501 -2658
  30. package/build/src/tools/categories.js +3 -0
  31. package/build/src/tools/console.js +42 -39
  32. package/build/src/tools/emulation.js +1 -1
  33. package/build/src/tools/extensions.js +5 -11
  34. package/build/src/tools/inPage.js +3 -13
  35. package/build/src/tools/input.js +15 -16
  36. package/build/src/tools/lighthouse.js +2 -2
  37. package/build/src/tools/memory.js +48 -3
  38. package/build/src/tools/network.js +4 -4
  39. package/build/src/tools/pages.js +212 -146
  40. package/build/src/tools/performance.js +1 -1
  41. package/build/src/tools/screencast.js +20 -8
  42. package/build/src/tools/screenshot.js +3 -3
  43. package/build/src/tools/script.js +22 -16
  44. package/build/src/tools/tools.js +2 -0
  45. package/build/src/tools/webmcp.js +63 -0
  46. package/build/src/utils/check-for-updates.js +73 -0
  47. package/build/src/utils/files.js +4 -0
  48. package/build/src/utils/id.js +15 -0
  49. package/build/src/version.js +1 -1
  50. package/package.json +13 -8
  51. 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 remoted debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.',
11
- conflicts: ['isolated', 'executablePath', 'categoryExtensions'],
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', 'categoryExtensions'],
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', 'categoryExtensions'],
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: ['stable', 'canary', 'beta', 'dev'],
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 vision tools',
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: true,
197
- conflicts: ['browserUrl', 'autoConnect', 'wsEndpoint'],
198
- describe: 'Set to true to include tools related to extensions. Note: This feature is only supported with a pipe connection. autoConnect is not supported.',
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
- async function start(args) {
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
- if (!isDaemonRunning()) {
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 a input, text area or select an option from a <select> element.',
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 requests.',
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 open in the browser.',
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: 'Performance',
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 pages screenshot.',
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
  }