chrome-devtools-mcp 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,6 +53,16 @@ Add the following config to your MCP client:
53
53
 
54
54
  ### MCP Client configuration
55
55
 
56
+ <details>
57
+ <summary>Amp</summary>
58
+ Follow https://ampcode.com/manual#mcp and use the config provided above. You can also install the Chrome DevTools MCP server using the CLI:
59
+
60
+ ```bash
61
+ amp mcp add chrome-devtools -- npx chrome-devtools-mcp@latest
62
+ ```
63
+
64
+ </details>
65
+
56
66
  <details>
57
67
  <summary>Claude Code</summary>
58
68
  Use the Claude Code CLI to add the Chrome DevTools MCP server (<a href="https://docs.anthropic.com/en/docs/claude-code/mcp">guide</a>):
@@ -177,6 +187,15 @@ The same way chrome-devtools-mcp can be configured for JetBrains Junie in `Setti
177
187
 
178
188
  </details>
179
189
 
190
+ <details>
191
+ <summary>Kiro</summary>
192
+
193
+ In **Kiro Settings**, go to `Configure MCP` > `Open Workspace or User MCP Config` > Use the configuration snippet provided above.
194
+
195
+ Or, from the IDE **Activity Bar** > `Kiro` > `MCP Servers` > `Click Open MCP Config`. Use the configuration snippet provided above.
196
+
197
+ </details>
198
+
180
199
  <details>
181
200
  <summary>Visual Studio</summary>
182
201
 
@@ -291,6 +310,10 @@ The Chrome DevTools MCP server supports the following configuration option:
291
310
  If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
292
311
  - **Type:** boolean
293
312
 
313
+ - **`--chromeArg`**
314
+ Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
315
+ - **Type:** array
316
+
294
317
  <!-- END AUTO GENERATED OPTIONS -->
295
318
 
296
319
  Pass them via the `args` property in the JSON configuration. For example:
@@ -336,7 +359,7 @@ Here is a step-by-step guide on how to connect to a running Chrome Stable instan
336
359
 
337
360
  **Step 1: Configure the MCP client**
338
361
 
339
- Add the `--browser-url` option to your MCP client configuration. The value of this option should be the URL of the running Chrome instance. `http://localhost:9222` is a common default.
362
+ Add the `--browser-url` option to your MCP client configuration. The value of this option should be the URL of the running Chrome instance. `http://127.0.0.1:9222` is a common default.
340
363
 
341
364
  ```json
342
365
  {
@@ -345,7 +368,7 @@ Add the `--browser-url` option to your MCP client configuration. The value of th
345
368
  "command": "npx",
346
369
  "args": [
347
370
  "chrome-devtools-mcp@latest",
348
- "--browser-url=http://localhost:9222"
371
+ "--browser-url=http://127.0.0.1:9222"
349
372
  ]
350
373
  }
351
374
  }
@@ -201,6 +201,9 @@ export class McpContext {
201
201
  const page = this.getSelectedPage();
202
202
  return page.getDefaultNavigationTimeout();
203
203
  }
204
+ getAXNodeByUid(uid) {
205
+ return this.#textSnapshot?.idToNode.get(uid);
206
+ }
204
207
  async getElementByUid(uid) {
205
208
  if (!this.#textSnapshot?.idToNode.size) {
206
209
  throw new Error(`No snapshot found. Use ${takeSnapshot.name} to capture one.`);
@@ -253,6 +256,14 @@ export class McpContext {
253
256
  ? node.children.map(child => assignIds(child))
254
257
  : [],
255
258
  };
259
+ // The AXNode for an option doesn't contain its `value`.
260
+ // Therefore, set text content of the option as value.
261
+ if (node.role === 'option') {
262
+ const optionText = node.name;
263
+ if (optionText) {
264
+ nodeWithId.value = optionText.toString();
265
+ }
266
+ }
256
267
  idToNode.set(nodeWithId.id, nodeWithId);
257
268
  return nodeWithId;
258
269
  };
@@ -117,8 +117,11 @@ export class McpResponse {
117
117
  }
118
118
  const dialog = context.getDialog();
119
119
  if (dialog) {
120
+ const defaultValueIfNeeded = dialog.type() === 'prompt'
121
+ ? ` (default value: "${dialog.defaultValue()}")`
122
+ : '';
120
123
  response.push(`# Open dialog
121
- ${dialog.type()}: ${dialog.message()} (default value: ${dialog.message()}).
124
+ ${dialog.type()}: ${dialog.message()}${defaultValueIfNeeded}.
122
125
  Call ${handleDialog.name} to handle it before continuing.`);
123
126
  }
124
127
  if (this.#includePages) {
@@ -42,7 +42,7 @@ export async function ensureBrowserConnected(options) {
42
42
  return browser;
43
43
  }
44
44
  export async function launch(options) {
45
- const { channel, executablePath, customDevTools, headless, isolated } = options;
45
+ const { channel, executablePath, headless, isolated } = options;
46
46
  const profileDirName = channel && channel !== 'stable'
47
47
  ? `chrome-profile-${channel}`
48
48
  : 'chrome-profile';
@@ -57,9 +57,6 @@ export async function launch(options) {
57
57
  ...(options.args ?? []),
58
58
  '--hide-crash-restore-bubble',
59
59
  ];
60
- if (customDevTools) {
61
- args.push(`--custom-devtools-frontend=file://${customDevTools}`);
62
- }
63
60
  if (headless) {
64
61
  args.push('--screen-info={3840x2160}');
65
62
  }
@@ -104,9 +101,7 @@ export async function launch(options) {
104
101
  }
105
102
  catch (error) {
106
103
  if (userDataDir &&
107
- (error.message.includes('The browser is already running') ||
108
- error.message.includes('Target closed') ||
109
- error.message.includes('Connection closed'))) {
104
+ error.message.includes('The browser is already running')) {
110
105
  throw new Error(`The browser is already running for ${userDataDir}. Use --isolated to run multiple browser instances.`, {
111
106
  cause: error,
112
107
  });
package/build/src/cli.js CHANGED
@@ -39,13 +39,6 @@ export const cliOptions = {
39
39
  description: 'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed.',
40
40
  default: false,
41
41
  },
42
- customDevtools: {
43
- type: 'string',
44
- description: 'Path to custom DevTools.',
45
- hidden: true,
46
- conflicts: 'browserUrl',
47
- alias: 'd',
48
- },
49
42
  channel: {
50
43
  type: 'string',
51
44
  description: 'Specify a different Chrome channel that should be used. The default is the stable channel version.',
@@ -86,6 +79,10 @@ export const cliOptions = {
86
79
  describe: 'Whether to enable automation over DevTools targets',
87
80
  hidden: true,
88
81
  },
82
+ chromeArg: {
83
+ type: 'array',
84
+ describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
85
+ },
89
86
  };
90
87
  export function parseArguments(version, argv = process.argv) {
91
88
  const yargsInstance = yargs(hideBin(argv))
@@ -114,6 +111,10 @@ export function parseArguments(version, argv = process.argv) {
114
111
  '$0 --viewport 1280x720',
115
112
  'Launch Chrome with the initial viewport size of 1280x720px',
116
113
  ],
114
+ [
115
+ `$0 --chrome-arg='--no-sandbox' --chrome-arg='--disable-setuid-sandbox'`,
116
+ 'Launch Chrome without sandboxes. Use with caution.',
117
+ ],
117
118
  ]);
118
119
  return yargsInstance
119
120
  .wrap(Math.min(120, yargsInstance.terminalWidth()))
@@ -14,61 +14,28 @@ function getAttributes(serializedAXNodeRoot) {
14
14
  serializedAXNodeRoot.role,
15
15
  `"${serializedAXNodeRoot.name || ''}"`, // Corrected: Added quotes around name
16
16
  ];
17
- // Value properties
18
- const valueProperties = [
19
- 'value',
20
- 'valuetext',
21
- 'valuemin',
22
- 'valuemax',
23
- 'level',
24
- 'autocomplete',
25
- 'haspopup',
26
- 'invalid',
27
- 'orientation',
28
- 'description',
29
- 'keyshortcuts',
30
- 'roledescription',
31
- ];
32
- for (const property of valueProperties) {
33
- if (property in serializedAXNodeRoot &&
34
- serializedAXNodeRoot[property] !== undefined) {
35
- attributes.push(`${property}="${serializedAXNodeRoot[property]}"`);
36
- }
37
- }
38
- // Boolean properties that also have an 'able' attribute
17
+ const excluded = new Set(['id', 'role', 'name', 'elementHandle', 'children']);
39
18
  const booleanPropertyMap = {
40
19
  disabled: 'disableable',
41
20
  expanded: 'expandable',
42
21
  focused: 'focusable',
43
22
  selected: 'selectable',
44
23
  };
45
- for (const [property, ableAttribute] of Object.entries(booleanPropertyMap)) {
46
- if (property in serializedAXNodeRoot) {
47
- attributes.push(ableAttribute);
48
- if (serializedAXNodeRoot[property]) {
49
- attributes.push(property);
50
- }
51
- }
52
- }
53
- const booleanProperties = [
54
- 'modal',
55
- 'multiline',
56
- 'readonly',
57
- 'required',
58
- 'multiselectable',
59
- ];
60
- for (const property of booleanProperties) {
61
- if (property in serializedAXNodeRoot && serializedAXNodeRoot[property]) {
62
- attributes.push(property);
24
+ for (const attr of Object.keys(serializedAXNodeRoot).sort()) {
25
+ if (excluded.has(attr)) {
26
+ continue;
63
27
  }
64
- }
65
- // Mixed boolean/string attributes
66
- for (const property of ['pressed', 'checked']) {
67
- if (property in serializedAXNodeRoot) {
68
- attributes.push(property);
69
- if (serializedAXNodeRoot[property]) {
70
- attributes.push(`${property}="${serializedAXNodeRoot[property]}"`);
28
+ const value = serializedAXNodeRoot[attr];
29
+ if (typeof value === 'boolean') {
30
+ if (booleanPropertyMap[attr]) {
31
+ attributes.push(booleanPropertyMap[attr]);
71
32
  }
33
+ if (value) {
34
+ attributes.push(attr);
35
+ }
36
+ }
37
+ else if (typeof value === 'string' || typeof value === 'number') {
38
+ attributes.push(`${attr}="${value}"`);
72
39
  }
73
40
  }
74
41
  return attributes;
package/build/src/main.js CHANGED
@@ -4,9 +4,6 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import './polyfill.js';
7
- import assert from 'node:assert';
8
- import fs from 'node:fs';
9
- import path from 'node:path';
10
7
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
9
  import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
@@ -25,36 +22,24 @@ import * as performanceTools from './tools/performance.js';
25
22
  import * as screenshotTools from './tools/screenshot.js';
26
23
  import * as scriptTools from './tools/script.js';
27
24
  import * as snapshotTools from './tools/snapshot.js';
28
- function readPackageJson() {
29
- const currentDir = import.meta.dirname;
30
- const packageJsonPath = path.join(currentDir, '..', '..', 'package.json');
31
- if (!fs.existsSync(packageJsonPath)) {
32
- return {};
33
- }
34
- try {
35
- const json = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
36
- assert.strict(json['name'], 'chrome-devtools-mcp');
37
- return json;
38
- }
39
- catch {
40
- return {};
41
- }
42
- }
43
- const version = readPackageJson().version ?? 'unknown';
44
- export const args = parseArguments(version);
25
+ // If moved update release-please config
26
+ // x-release-please-start-version
27
+ const VERSION = '0.8.1';
28
+ // x-release-please-end
29
+ export const args = parseArguments(VERSION);
45
30
  const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
46
- logger(`Starting Chrome DevTools MCP Server v${version}`);
31
+ logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
47
32
  const server = new McpServer({
48
33
  name: 'chrome_devtools',
49
34
  title: 'Chrome DevTools MCP server',
50
- version,
35
+ version: VERSION,
51
36
  }, { capabilities: { logging: {} } });
52
37
  server.server.setRequestHandler(SetLevelRequestSchema, () => {
53
38
  return {};
54
39
  });
55
40
  let context;
56
41
  async function getContext() {
57
- const extraArgs = [];
42
+ const extraArgs = (args.chromeArg ?? []).map(String);
58
43
  if (args.proxyServer) {
59
44
  extraArgs.push(`--proxy-server=${args.proxyServer}`);
60
45
  }
@@ -67,7 +52,6 @@ async function getContext() {
67
52
  : await ensureBrowserLaunched({
68
53
  headless: args.headless,
69
54
  executablePath: args.executablePath,
70
- customDevTools: args.customDevtools,
71
55
  channel: args.channel,
72
56
  isolated: args.isolated,
73
57
  logFile,
@@ -140,6 +124,9 @@ const tools = [
140
124
  ...Object.values(scriptTools),
141
125
  ...Object.values(snapshotTools),
142
126
  ];
127
+ tools.sort((a, b) => {
128
+ return a.name.localeCompare(b.name);
129
+ });
143
130
  for (const tool of tools) {
144
131
  registerTool(tool);
145
132
  }
@@ -68,6 +68,55 @@ export const hover = defineTool({
68
68
  }
69
69
  },
70
70
  });
71
+ // The AXNode for an option doesn't contain its `value`. We set text content of the option as value.
72
+ // If the form is a combobox, we need to find the correct option by its text value.
73
+ // To do that, loop through the children while checking which child's text matches the requested value (requested value is actually the text content).
74
+ // When the correct option is found, use the element handle to get the real value.
75
+ async function selectOption(handle, aXNode, value) {
76
+ let optionFound = false;
77
+ for (const child of aXNode.children) {
78
+ if (child.role === 'option' && child.name === value && child.value) {
79
+ optionFound = true;
80
+ const childHandle = await child.elementHandle();
81
+ if (childHandle) {
82
+ try {
83
+ const childValueHandle = await childHandle.getProperty('value');
84
+ try {
85
+ const childValue = await childValueHandle.jsonValue();
86
+ if (childValue) {
87
+ await handle.asLocator().fill(childValue.toString());
88
+ }
89
+ }
90
+ finally {
91
+ void childValueHandle.dispose();
92
+ }
93
+ break;
94
+ }
95
+ finally {
96
+ void childHandle.dispose();
97
+ }
98
+ }
99
+ }
100
+ }
101
+ if (!optionFound) {
102
+ throw new Error(`Could not find option with text "${value}"`);
103
+ }
104
+ }
105
+ async function fillFormElement(uid, value, context) {
106
+ const handle = await context.getElementByUid(uid);
107
+ try {
108
+ const aXNode = context.getAXNodeByUid(uid);
109
+ if (aXNode && aXNode.role === 'combobox') {
110
+ await selectOption(handle, aXNode, value);
111
+ }
112
+ else {
113
+ await handle.asLocator().fill(value);
114
+ }
115
+ }
116
+ finally {
117
+ void handle.dispose();
118
+ }
119
+ }
71
120
  export const fill = defineTool({
72
121
  name: 'fill',
73
122
  description: `Type text into a input, text area or select an option from a <select> element.`,
@@ -82,17 +131,11 @@ export const fill = defineTool({
82
131
  value: z.string().describe('The value to fill in'),
83
132
  },
84
133
  handler: async (request, response, context) => {
85
- const handle = await context.getElementByUid(request.params.uid);
86
- try {
87
- await context.waitForEventsAfterAction(async () => {
88
- await handle.asLocator().fill(request.params.value);
89
- });
90
- response.appendResponseLine(`Successfully filled out the element`);
91
- response.setIncludeSnapshot(true);
92
- }
93
- finally {
94
- void handle.dispose();
95
- }
134
+ await context.waitForEventsAfterAction(async () => {
135
+ await fillFormElement(request.params.uid, request.params.value, context);
136
+ });
137
+ response.appendResponseLine(`Successfully filled out the element`);
138
+ response.setIncludeSnapshot(true);
96
139
  },
97
140
  });
98
141
  export const drag = defineTool({
@@ -141,15 +184,9 @@ export const fillForm = defineTool({
141
184
  },
142
185
  handler: async (request, response, context) => {
143
186
  for (const element of request.params.elements) {
144
- const handle = await context.getElementByUid(element.uid);
145
- try {
146
- await context.waitForEventsAfterAction(async () => {
147
- await handle.asLocator().fill(element.value);
148
- });
149
- }
150
- finally {
151
- void handle.dispose();
152
- }
187
+ await context.waitForEventsAfterAction(async () => {
188
+ await fillFormElement(element.uid, element.value, context);
189
+ });
153
190
  }
154
191
  response.appendResponseLine(`Successfully filled out the form`);
155
192
  response.setIncludeSnapshot(true);
@@ -133,8 +133,8 @@ export const navigatePageHistory = defineTool({
133
133
  await page.goForward(options);
134
134
  }
135
135
  }
136
- catch {
137
- response.appendResponseLine(`Unable to navigate ${request.params.navigate} in currently selected page.`);
136
+ catch (error) {
137
+ response.appendResponseLine(`Unable to navigate ${request.params.navigate} in currently selected page. ${error.message}`);
138
138
  }
139
139
  response.setIncludePages(true);
140
140
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "MCP server for Chrome DevTools",
5
5
  "type": "module",
6
6
  "bin": "./build/src/index.js",
@@ -20,7 +20,7 @@
20
20
  "test:only:no-build": "node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",
21
21
  "test:update-snapshots": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-force-exit --test --test-update-snapshots \"build/tests/**/*.test.js\"",
22
22
  "prepare": "node --experimental-strip-types scripts/prepare.ts",
23
- "sync-server-json-version": "node --experimental-strip-types scripts/sync-server-json-version.ts && npm run format"
23
+ "verify-server-json-version": "node --experimental-strip-types scripts/verify-server-json-version.ts"
24
24
  },
25
25
  "files": [
26
26
  "build/src",
@@ -37,10 +37,10 @@
37
37
  "homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp#readme",
38
38
  "mcpName": "io.github.ChromeDevTools/chrome-devtools-mcp",
39
39
  "dependencies": {
40
- "@modelcontextprotocol/sdk": "1.19.1",
41
- "core-js": "3.45.1",
40
+ "@modelcontextprotocol/sdk": "1.20.0",
41
+ "core-js": "3.46.0",
42
42
  "debug": "4.4.3",
43
- "puppeteer-core": "^24.24.0",
43
+ "puppeteer-core": "^24.24.1",
44
44
  "yargs": "18.0.0",
45
45
  "zod": "^3.25.76"
46
46
  },
@@ -60,7 +60,7 @@
60
60
  "eslint-plugin-import": "^2.32.0",
61
61
  "globals": "^16.4.0",
62
62
  "prettier": "^3.6.2",
63
- "puppeteer": "24.24.0",
63
+ "puppeteer": "24.24.1",
64
64
  "sinon": "^21.0.0",
65
65
  "typescript": "^5.9.2",
66
66
  "typescript-eslint": "^8.43.0"