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 +21 -2
- package/build/src/McpContext.js +11 -0
- package/build/src/McpResponse.js +4 -1
- package/build/src/browser.js +1 -3
- package/build/src/formatters/snapshotFormatter.js +14 -47
- package/build/src/main.js +10 -22
- package/build/src/tools/input.js +57 -20
- package/build/src/tools/pages.js +2 -2
- package/package.json +6 -6
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://
|
|
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://
|
|
371
|
+
"--browser-url=http://127.0.0.1:9222"
|
|
353
372
|
]
|
|
354
373
|
}
|
|
355
374
|
}
|
package/build/src/McpContext.js
CHANGED
|
@@ -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
|
};
|
package/build/src/McpResponse.js
CHANGED
|
@@ -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()}
|
|
124
|
+
${dialog.type()}: ${dialog.message()}${defaultValueIfNeeded}.
|
|
122
125
|
Call ${handleDialog.name} to handle it before continuing.`);
|
|
123
126
|
}
|
|
124
127
|
if (this.#includePages) {
|
package/build/src/browser.js
CHANGED
|
@@ -101,9 +101,7 @@ export async function launch(options) {
|
|
|
101
101
|
}
|
|
102
102
|
catch (error) {
|
|
103
103
|
if (userDataDir &&
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
if (
|
|
47
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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${
|
|
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
|
}
|
package/build/src/tools/input.js
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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);
|
package/build/src/tools/pages.js
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
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.
|
|
41
|
-
"core-js": "3.
|
|
40
|
+
"@modelcontextprotocol/sdk": "1.20.0",
|
|
41
|
+
"core-js": "3.46.0",
|
|
42
42
|
"debug": "4.4.3",
|
|
43
|
-
"puppeteer-core": "^24.24.
|
|
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.
|
|
63
|
+
"puppeteer": "24.24.1",
|
|
64
64
|
"sinon": "^21.0.0",
|
|
65
65
|
"typescript": "^5.9.2",
|
|
66
66
|
"typescript-eslint": "^8.43.0"
|