chrome-devtools-mcp 0.8.0 → 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
 
@@ -340,7 +359,7 @@ Here is a step-by-step guide on how to connect to a running Chrome Stable instan
340
359
 
341
360
  **Step 1: Configure the MCP client**
342
361
 
343
- 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.
344
363
 
345
364
  ```json
346
365
  {
@@ -349,7 +368,7 @@ Add the `--browser-url` option to your MCP client configuration. The value of th
349
368
  "command": "npx",
350
369
  "args": [
351
370
  "chrome-devtools-mcp@latest",
352
- "--browser-url=http://localhost:9222"
371
+ "--browser-url=http://127.0.0.1:9222"
353
372
  ]
354
373
  }
355
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) {
@@ -101,9 +101,7 @@ export async function launch(options) {
101
101
  }
102
102
  catch (error) {
103
103
  if (userDataDir &&
104
- (error.message.includes('The browser is already running') ||
105
- error.message.includes('Target closed') ||
106
- error.message.includes('Connection closed'))) {
104
+ error.message.includes('The browser is already running')) {
107
105
  throw new Error(`The browser is already running for ${userDataDir}. Use --isolated to run multiple browser instances.`, {
108
106
  cause: error,
109
107
  });
@@ -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,29 +22,17 @@ 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 {};
@@ -139,6 +124,9 @@ const tools = [
139
124
  ...Object.values(scriptTools),
140
125
  ...Object.values(snapshotTools),
141
126
  ];
127
+ tools.sort((a, b) => {
128
+ return a.name.localeCompare(b.name);
129
+ });
142
130
  for (const tool of tools) {
143
131
  registerTool(tool);
144
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.8.0",
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"