diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 210d2903..bcd05228 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.5.1"
+ ".": "0.6.0"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2085a7ea..d708adcb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
# Changelog
+## [0.6.0](https://github.com/ChromeDevTools/chrome-devtools-mcp/compare/chrome-devtools-mcp-v0.5.1...chrome-devtools-mcp-v0.6.0) (2025-10-01)
+
+
+### Features
+
+* **screenshot:** add WebP format support with quality parameter ([#220](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/220)) ([03e02a2](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/03e02a2d769fbfc0c98599444dfed5413d15ae6e))
+* **screenshot:** adds ability to output screenshot to a specific pat… ([#172](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/172)) ([f030726](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/f03072698ddda8587ce23229d733405f88b7c89e))
+* support --accept-insecure-certs CLI ([#231](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/231)) ([efb106d](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/efb106dc94af0057f88c89f810beb65114eeaa4b))
+* support --proxy-server CLI ([#230](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/230)) ([dfacc75](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/dfacc75ee9f46137b5194e35fc604b89a00ff53f))
+* support initial viewport in the CLI ([#229](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/229)) ([ef61a08](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/ef61a08707056c5078d268a83a2c95d10e224f31))
+* support timeouts in wait_for and navigations ([#228](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/228)) ([36e64d5](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/36e64d5ae21e8bb244a18201a23a16932947e938))
+
+
+### Bug Fixes
+
+* **network:** show only selected request ([#236](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/236)) ([73f0aec](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/73f0aecd8a48b9d1ee354897fe14d785c80e863e))
+* PageCollector subscribing multiple times ([#241](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/241)) ([0412878](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/0412878bf51ae46e48a171183bb38cfbbee1038a))
+* snapshot does not capture Iframe content ([#217](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/217)) ([ce356f2](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/ce356f256545e805db74664797de5f42e7b92bed)), closes [#186](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/186)
+
## [0.5.1](https://github.com/ChromeDevTools/chrome-devtools-mcp/compare/chrome-devtools-mcp-v0.5.0...chrome-devtools-mcp-v0.5.1) (2025-09-29)
diff --git a/README.md b/README.md
index eab1ab8f..469ef252 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,8 @@ control and inspect a live Chrome browser. It acts as a Model-Context-Protocol
(MCP) server, giving your AI coding assistant access to the full power of
Chrome DevTools for reliable automation, in-depth debugging, and performance analysis.
+## [Tool reference](./docs/tool-reference.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) | [Troubleshooting](./docs/troubleshooting.md)
+
## Key features
- **Get performance insights**: Uses [Chrome
@@ -40,7 +42,7 @@ Add the following config to your MCP client:
"mcpServers": {
"chrome-devtools": {
"command": "npx",
- "args": ["chrome-devtools-mcp@latest"]
+ "args": ["-y", "chrome-devtools-mcp@latest"]
}
}
}
@@ -94,6 +96,30 @@ startup_timeout_ms = 20_000
+
+ Copilot CLI
+
+Start Copilot CLI:
+
+```
+copilot
+```
+
+Start the dialog to add a new MCP server by running:
+
+```
+/mcp add
+```
+
+Configure the following fields and press `CTR-S` to save the configuration:
+
+- **Server name:** `chrome-devtools`
+- **Server Type:** `[1] Local`
+- **Command:** `npx`
+- **Arguments:** `-y, chrome-devtools-mcp@latest`
+
+
+
Copilot / VS Code
Follow the MCP install guide,
@@ -109,7 +135,7 @@ startup_timeout_ms = 20_000
**Click the button to install:**
-[
](https://cursor.com/en/install-mcp?name=chrome-devtools&config=eyJjb21tYW5kIjoibnB4IGNocm9tZS1kZXZ0b29scy1tY3BAbGF0ZXN0In0%3D)
+[
](https://cursor.com/en/install-mcp?name=chrome-devtools&config=eyJjb21tYW5kIjoibnB4IC15IGNocm9tZS1kZXZ0b29scy1tY3BAbGF0ZXN0In0%3D)
**Or install manually:**
@@ -151,6 +177,14 @@ The same way chrome-devtools-mcp can be configured for JetBrains Junie in `Setti
+
+ Visual Studio
+
+ **Click the button to install:**
+
+ [
](https://vs-open.link/mcp-install?%7B%22name%22%3A%22chrome-devtools%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22chrome-devtools-mcp%40latest%22%5D%7D)
+
+
### Your first prompt
Enter the following prompt in your MCP Client to check if everything is working:
@@ -166,6 +200,8 @@ Your MCP client should open the browser and record a performance trace.
## Tools
+If you run into any issues, checkout our [troubleshooting guide](./docs/troubleshooting.md).
+
- **Input automation** (7 tools)
@@ -236,6 +272,18 @@ The Chrome DevTools MCP server supports the following configuration option:
Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.
- **Type:** string
+- **`--viewport`**
+ Initial viewport size for the Chromee instances started by the server. For example, `1280x720`
+ - **Type:** string
+
+- **`--proxyServer`**
+ Proxy server configuration for Chrome passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details.
+ - **Type:** string
+
+- **`--acceptInsecureCerts`**
+ If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
+ - **Type:** boolean
+
Pass them via the `args` property in the JSON configuration. For example:
diff --git a/docs/tool-reference.md b/docs/tool-reference.md
index 1b39e869..c4c4bc9e 100644
--- a/docs/tool-reference.md
+++ b/docs/tool-reference.md
@@ -138,6 +138,7 @@
**Parameters:**
+- **timeout** (integer) _(optional)_: Maximum wait time in milliseconds. If set to 0, the default timeout will be used.
- **url** (string) **(required)**: URL to navigate the page to
---
@@ -149,6 +150,7 @@
**Parameters:**
- **navigate** (enum: "back", "forward") **(required)**: Whether to navigate back or navigate forward in the selected pages history
+- **timeout** (integer) _(optional)_: Maximum wait time in milliseconds. If set to 0, the default timeout will be used.
---
@@ -158,6 +160,7 @@
**Parameters:**
+- **timeout** (integer) _(optional)_: Maximum wait time in milliseconds. If set to 0, the default timeout will be used.
- **url** (string) **(required)**: URL to load in a new page.
---
@@ -179,6 +182,7 @@
**Parameters:**
- **text** (string) **(required)**: Text to appear on the page
+- **timeout** (integer) _(optional)_: Maximum wait time in milliseconds. If set to 0, the default timeout will be used.
---
@@ -306,9 +310,10 @@ so returned values have to JSON-serializable.
**Parameters:**
-- **format** (enum: "png", "jpeg") _(optional)_: Type of format to save the screenshot as. Default is "png"
+- **filePath** (string) _(optional)_: The absolute path, or a path relative to the current working directory, to save the screenshot to instead of attaching it to the response.
+- **format** (enum: "png", "jpeg", "webp") _(optional)_: Type of format to save the screenshot as. Default is "png"
- **fullPage** (boolean) _(optional)_: If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid.
-- **quality** (number) _(optional)_: Compression quality for JPEG format (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.
+- **quality** (number) _(optional)_: Compression quality for JPEG and WebP formats (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.
- **uid** (string) _(optional)_: The uid of an element on the page from the page content snapshot. If omitted takes a pages screenshot.
---
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 00000000..302fe560
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,29 @@
+# Troubleshooting
+
+## General tips
+
+- Run `npx chrome-devtools-mcp@latest --help` to test if the MCP server runs on your machine.
+- Make sure that your MCP client uses the same npm and node version as your terminal.
+- When configuring your MCP client, try using the `--yes` argument to `npx` to
+ auto-accept installation prompt.
+- Find a specific error in the output of the `chrome-devtools-mcp` server.
+ Usually, if you client is an IDE, logs would be in the Output pane.
+
+## Specific problems
+
+### `Error [ERR_MODULE_NOT_FOUND]: Cannot find module ...`
+
+This usually indicates either a non-supported Node version is in use or that the
+`npm`/`npx` cache is corrupted. Try clearing the cache, uninstalling
+`chrome-devtools-mcp` and installing it again. Clear the cache by running:
+
+```sh
+rm -rf ~/.npm/_npx # NOTE: this might remove other installed npx executables.
+npm cache clean --force
+```
+
+### `Target closed` error
+
+This indicates that the browser could not be started. Make sure that no Chrome
+instances are running or close them. Make sure you have the latest stable Chrome
+installed and that [your system is able to run Chrome](https://support.google.com/chrome/a/answer/7100626?hl=en).
diff --git a/package-lock.json b/package-lock.json
index 0cf872dd..f5c40be3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "chrome-devtools-mcp",
- "version": "0.5.1",
+ "version": "0.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chrome-devtools-mcp",
- "version": "0.5.1",
+ "version": "0.6.0",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "1.18.2",
@@ -28,7 +28,7 @@
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
- "chrome-devtools-frontend": "1.0.1520535",
+ "chrome-devtools-frontend": "1.0.1521880",
"eslint": "^9.35.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
@@ -40,7 +40,7 @@
"typescript-eslint": "^8.43.0"
},
"engines": {
- "node": ">=22.12.0"
+ "node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/@babel/code-frame": {
@@ -265,9 +265,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.35.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz",
- "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==",
+ "version": "9.36.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz",
+ "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -589,20 +589,6 @@
"eslint": ">=9.0.0"
}
},
- "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/types": {
- "version": "8.44.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.1.tgz",
- "integrity": "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
"node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
@@ -702,13 +688,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.3.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz",
- "integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==",
+ "version": "24.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.0.tgz",
+ "integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.10.0"
+ "undici-types": "~7.13.0"
}
},
"node_modules/@types/sinon": {
@@ -756,17 +742,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz",
- "integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
+ "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.43.0",
- "@typescript-eslint/type-utils": "8.43.0",
- "@typescript-eslint/utils": "8.43.0",
- "@typescript-eslint/visitor-keys": "8.43.0",
+ "@typescript-eslint/scope-manager": "8.45.0",
+ "@typescript-eslint/type-utils": "8.45.0",
+ "@typescript-eslint/utils": "8.45.0",
+ "@typescript-eslint/visitor-keys": "8.45.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -786,16 +772,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz",
- "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz",
+ "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.43.0",
- "@typescript-eslint/types": "8.43.0",
- "@typescript-eslint/typescript-estree": "8.43.0",
- "@typescript-eslint/visitor-keys": "8.43.0",
+ "@typescript-eslint/scope-manager": "8.45.0",
+ "@typescript-eslint/types": "8.45.0",
+ "@typescript-eslint/typescript-estree": "8.45.0",
+ "@typescript-eslint/visitor-keys": "8.45.0",
"debug": "^4.3.4"
},
"engines": {
@@ -811,14 +797,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz",
- "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz",
+ "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.43.0",
- "@typescript-eslint/types": "^8.43.0",
+ "@typescript-eslint/tsconfig-utils": "^8.45.0",
+ "@typescript-eslint/types": "^8.45.0",
"debug": "^4.3.4"
},
"engines": {
@@ -833,14 +819,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz",
- "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz",
+ "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.43.0",
- "@typescript-eslint/visitor-keys": "8.43.0"
+ "@typescript-eslint/types": "8.45.0",
+ "@typescript-eslint/visitor-keys": "8.45.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -851,9 +837,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz",
- "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz",
+ "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -868,15 +854,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz",
- "integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz",
+ "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.43.0",
- "@typescript-eslint/typescript-estree": "8.43.0",
- "@typescript-eslint/utils": "8.43.0",
+ "@typescript-eslint/types": "8.45.0",
+ "@typescript-eslint/typescript-estree": "8.45.0",
+ "@typescript-eslint/utils": "8.45.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -893,9 +879,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz",
- "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz",
+ "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -907,16 +893,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz",
- "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz",
+ "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.43.0",
- "@typescript-eslint/tsconfig-utils": "8.43.0",
- "@typescript-eslint/types": "8.43.0",
- "@typescript-eslint/visitor-keys": "8.43.0",
+ "@typescript-eslint/project-service": "8.45.0",
+ "@typescript-eslint/tsconfig-utils": "8.45.0",
+ "@typescript-eslint/types": "8.45.0",
+ "@typescript-eslint/visitor-keys": "8.45.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -936,16 +922,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz",
- "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz",
+ "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
- "@typescript-eslint/scope-manager": "8.43.0",
- "@typescript-eslint/types": "8.43.0",
- "@typescript-eslint/typescript-estree": "8.43.0"
+ "@typescript-eslint/scope-manager": "8.45.0",
+ "@typescript-eslint/types": "8.45.0",
+ "@typescript-eslint/typescript-estree": "8.45.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -960,13 +946,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz",
- "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz",
+ "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.43.0",
+ "@typescript-eslint/types": "8.45.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -1761,9 +1747,9 @@
}
},
"node_modules/chrome-devtools-frontend": {
- "version": "1.0.1520535",
- "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1520535.tgz",
- "integrity": "sha512-kCjiu7ZQ3vLNQ5hmmQZRwwpGpQykJECWuDUc3nKjXOOsMEWgJJh+3OhG6vOrtEG5BxL9jWZoBvf1GG8ddNpkeg==",
+ "version": "1.0.1521880",
+ "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1521880.tgz",
+ "integrity": "sha512-43m+y3kaBebVPu2Ja9u2GsvPgWvpn10jF4l3zr8ElhnxRRx1DOi58PXJ6EzxbjbrS5uzTZ0sa0cKNov/KtV6mQ==",
"dev": true,
"license": "BSD-3-Clause"
},
@@ -2408,9 +2394,9 @@
}
},
"node_modules/eslint": {
- "version": "9.35.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz",
- "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
+ "version": "9.36.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz",
+ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2420,7 +2406,7 @@
"@eslint/config-helpers": "^0.3.1",
"@eslint/core": "^0.15.2",
"@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.35.0",
+ "@eslint/js": "9.36.0",
"@eslint/plugin-kit": "^0.3.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -5759,16 +5745,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.43.0.tgz",
- "integrity": "sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==",
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.45.0.tgz",
+ "integrity": "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.43.0",
- "@typescript-eslint/parser": "8.43.0",
- "@typescript-eslint/typescript-estree": "8.43.0",
- "@typescript-eslint/utils": "8.43.0"
+ "@typescript-eslint/eslint-plugin": "8.45.0",
+ "@typescript-eslint/parser": "8.45.0",
+ "@typescript-eslint/typescript-estree": "8.45.0",
+ "@typescript-eslint/utils": "8.45.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5802,9 +5788,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.10.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
- "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
+ "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
"devOptional": true,
"license": "MIT"
},
diff --git a/package.json b/package.json
index 97f5f192..86f486db 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "chrome-devtools-mcp",
- "version": "0.5.1",
+ "version": "0.6.0",
"description": "MCP server for Chrome DevTools",
"type": "module",
"bin": "./build/src/index.js",
@@ -53,7 +53,7 @@
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
- "chrome-devtools-frontend": "1.0.1520535",
+ "chrome-devtools-frontend": "1.0.1521880",
"eslint": "^9.35.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
diff --git a/src/McpContext.ts b/src/McpContext.ts
index 71308676..ce980769 100644
--- a/src/McpContext.ts
+++ b/src/McpContext.ts
@@ -302,7 +302,9 @@ export class McpContext implements Context {
*/
async createTextSnapshot(): Promise {
const page = this.getSelectedPage();
- const rootNode = await page.accessibility.snapshot();
+ const rootNode = await page.accessibility.snapshot({
+ includeIframes: true,
+ });
if (!rootNode) {
return;
}
@@ -338,17 +340,20 @@ export class McpContext implements Context {
async saveTemporaryFile(
data: Uint8Array,
- mimeType: 'image/png' | 'image/jpeg',
+ mimeType: 'image/png' | 'image/jpeg' | 'image/webp',
): Promise<{filename: string}> {
try {
const dir = await fs.mkdtemp(
path.join(os.tmpdir(), 'chrome-devtools-mcp-'),
);
- const filename = path.join(
- dir,
- mimeType == 'image/png' ? `screenshot.png` : 'screenshot.jpg',
- );
- await fs.writeFile(path.join(dir, `screenshot.png`), data);
+ const ext =
+ mimeType === 'image/png'
+ ? 'png'
+ : mimeType === 'image/jpeg'
+ ? 'jpg'
+ : 'webp';
+ const filename = path.join(dir, `screenshot.${ext}`);
+ await fs.writeFile(filename, data);
return {filename};
} catch (err) {
this.logger(err);
diff --git a/src/McpResponse.ts b/src/McpResponse.ts
index 1d3d3ed7..77ab5e5a 100644
--- a/src/McpResponse.ts
+++ b/src/McpResponse.ts
@@ -154,7 +154,7 @@ export class McpResponse implements Response {
response.push(`## Network emulation`);
response.push(`Emulating: ${networkConditions}`);
response.push(
- `Navigation timeout set to ${context.getNavigationTimeout()} ms`,
+ `Default navigation timeout set to ${context.getNavigationTimeout()} ms`,
);
}
diff --git a/src/PageCollector.ts b/src/PageCollector.ts
index cb7618bd..9b078d55 100644
--- a/src/PageCollector.ts
+++ b/src/PageCollector.ts
@@ -9,6 +9,11 @@ import type {Browser, HTTPRequest, Page} from 'puppeteer-core';
export class PageCollector {
#browser: Browser;
#initializer: (page: Page, collector: (item: T) => void) => void;
+ /**
+ * The Array in this map should only be set once
+ * As we use the reference to it.
+ * Use methods that manipulate the array in place.
+ */
protected storage = new WeakMap();
constructor(
@@ -30,7 +35,6 @@ export class PageCollector {
if (!page) {
return;
}
-
this.#initializePage(page);
});
}
@@ -44,6 +48,9 @@ export class PageCollector {
return;
}
+ const stored: T[] = [];
+ this.storage.set(page, stored);
+
page.on('framenavigated', frame => {
// Only reset the storage on main frame navigation
if (frame !== page.mainFrame()) {
@@ -52,16 +59,16 @@ export class PageCollector {
this.cleanup(page);
});
this.#initializer(page, value => {
- const stored = this.storage.get(page) ?? [];
stored.push(value);
- this.storage.set(page, stored);
});
}
protected cleanup(page: Page) {
- const collection = this.storage.get(page) ?? [];
- // Keep the reference alive
- collection.length = 0;
+ const collection = this.storage.get(page);
+ if (collection) {
+ // Keep the reference alive
+ collection.length = 0;
+ }
}
getData(page: Page): T[] {
@@ -72,6 +79,9 @@ export class PageCollector {
export class NetworkCollector extends PageCollector {
override cleanup(page: Page) {
const requests = this.storage.get(page) ?? [];
+ if (!requests) {
+ return;
+ }
const lastRequestIdx = requests.findLastIndex(request => {
return request.frame() === page.mainFrame()
? request.isNavigationRequest()
@@ -79,6 +89,7 @@ export class NetworkCollector extends PageCollector {
});
// Keep all requests since the last navigation request including that
// navigation request itself.
- this.storage.set(page, requests.slice(Math.max(lastRequestIdx, 0)));
+ // Keep the reference
+ requests.splice(0, Math.max(lastRequestIdx, 0));
}
}
diff --git a/src/browser.ts b/src/browser.ts
index 2a171209..1cbaa6da 100644
--- a/src/browser.ts
+++ b/src/browser.ts
@@ -44,7 +44,7 @@ const connectOptions: ConnectOptions = {
protocolTimeout: 10_000,
};
-async function ensureBrowserConnected(browserURL: string) {
+export async function ensureBrowserConnected(browserURL: string) {
if (browser?.connected) {
return browser;
}
@@ -57,6 +57,7 @@ async function ensureBrowserConnected(browserURL: string) {
}
interface McpLaunchOptions {
+ acceptInsecureCerts?: boolean;
executablePath?: string;
customDevTools?: string;
channel?: Channel;
@@ -64,6 +65,11 @@ interface McpLaunchOptions {
headless: boolean;
isolated: boolean;
logFile?: fs.WriteStream;
+ viewport?: {
+ width: number;
+ height: number;
+ };
+ args?: string[];
}
export async function launch(options: McpLaunchOptions): Promise {
@@ -86,7 +92,10 @@ export async function launch(options: McpLaunchOptions): Promise {
});
}
- const args: LaunchOptions['args'] = ['--hide-crash-restore-bubble'];
+ const args: LaunchOptions['args'] = [
+ ...(options.args ?? []),
+ '--hide-crash-restore-bubble',
+ ];
if (customDevTools) {
args.push(`--custom-devtools-frontend=file://${customDevTools}`);
}
@@ -108,6 +117,7 @@ export async function launch(options: McpLaunchOptions): Promise {
pipe: true,
headless,
args,
+ acceptInsecureCerts: options.acceptInsecureCerts,
});
if (options.logFile) {
// FIXME: we are probably subscribing too late to catch startup logs. We
@@ -115,6 +125,14 @@ export async function launch(options: McpLaunchOptions): Promise {
browser.process()?.stderr?.pipe(options.logFile);
browser.process()?.stdout?.pipe(options.logFile);
}
+ if (options.viewport) {
+ const [page] = await browser.pages();
+ // @ts-expect-error internal API for now.
+ await page?.resize({
+ contentWidth: options.viewport.width,
+ contentHeight: options.viewport.height,
+ });
+ }
return browser;
} catch (error) {
if (
@@ -134,7 +152,7 @@ export async function launch(options: McpLaunchOptions): Promise {
}
}
-async function ensureBrowserLaunched(
+export async function ensureBrowserLaunched(
options: McpLaunchOptions,
): Promise {
if (browser?.connected) {
@@ -144,20 +162,4 @@ async function ensureBrowserLaunched(
return browser;
}
-export async function resolveBrowser(options: {
- browserUrl?: string;
- executablePath?: string;
- customDevTools?: string;
- channel?: Channel;
- headless: boolean;
- isolated: boolean;
- logFile?: fs.WriteStream;
-}) {
- const browser = options.browserUrl
- ? await ensureBrowserConnected(options.browserUrl)
- : await ensureBrowserLaunched(options);
-
- return browser;
-}
-
export type Channel = 'stable' | 'canary' | 'beta' | 'dev';
diff --git a/src/cli.ts b/src/cli.ts
index df07bfad..02ebf3d8 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -4,12 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import type {Options as YargsOptions} from 'yargs';
import yargs from 'yargs';
import {hideBin} from 'yargs/helpers';
export const cliOptions = {
browserUrl: {
- type: 'string' as const,
+ type: 'string',
description:
'Connect to a running Chrome instance using port forwarding. For more details see: https://developer.chrome.com/docs/devtools/remote-debugging/local-server.',
alias: 'u',
@@ -19,42 +20,68 @@ export const cliOptions = {
},
},
headless: {
- type: 'boolean' as const,
+ type: 'boolean',
description: 'Whether to run in headless (no UI) mode.',
default: false,
},
executablePath: {
- type: 'string' as const,
+ type: 'string',
description: 'Path to custom Chrome executable.',
conflicts: 'browserUrl',
alias: 'e',
},
isolated: {
- type: 'boolean' as const,
+ type: 'boolean',
description:
'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed.',
default: false,
},
customDevtools: {
- type: 'string' as const,
+ type: 'string',
description: 'Path to custom DevTools.',
hidden: true,
conflicts: 'browserUrl',
alias: 'd',
},
channel: {
- type: 'string' as const,
+ type: 'string',
description:
'Specify a different Chrome channel that should be used. The default is the stable channel version.',
choices: ['stable', 'canary', 'beta', 'dev'] as const,
conflicts: ['browserUrl', 'executablePath'],
},
logFile: {
- type: 'string' as const,
+ type: 'string',
describe:
'Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.',
},
-};
+ viewport: {
+ type: 'string',
+ describe:
+ 'Initial viewport size for the Chromee instances started by the server. For example, `1280x720`',
+ coerce: (arg: string | undefined) => {
+ if (arg === undefined) {
+ return;
+ }
+ const [width, height] = arg.split('x').map(Number);
+ if (!width || !height || Number.isNaN(width) || Number.isNaN(height)) {
+ throw new Error('Invalid viewport. Expected format is `1280x720`.');
+ }
+ return {
+ width,
+ height,
+ };
+ },
+ },
+ proxyServer: {
+ type: 'string',
+ description: `Proxy server configuration for Chrome passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details.`,
+ },
+ acceptInsecureCerts: {
+ type: 'boolean',
+ description: `If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.`,
+ },
+} satisfies Record;
export function parseArguments(version: string, argv = process.argv) {
const yargsInstance = yargs(hideBin(argv))
@@ -79,6 +106,10 @@ export function parseArguments(version: string, argv = process.argv) {
['$0 --channel stable', 'Use stable Chrome installed on this system'],
['$0 --logFile /tmp/log.txt', 'Save logs to a file'],
['$0 --help', 'Print CLI options'],
+ [
+ '$0 --viewport 1280x720',
+ 'Launch Chrome with the initial viewport size of 1280x720px',
+ ],
]);
return yargsInstance
diff --git a/src/logger.ts b/src/logger.ts
index f04b9c4d..f939e4cd 100644
--- a/src/logger.ts
+++ b/src/logger.ts
@@ -18,12 +18,12 @@ export function saveLogsToFile(fileName: string): fs.WriteStream {
// Enable overrides everything so we need to add them
debug.enable(namespacesToEnable.join(','));
- const logFile = fs.createWriteStream(fileName, {flags: 'a'});
+ const logFile = fs.createWriteStream(fileName, {flags: 'a+'});
debug.log = function (...chunks: any[]) {
logFile.write(`${chunks.join(' ')}\n`);
};
logFile.on('error', function (error) {
- console.log(`Error when opening/writing to log file: ${error.message}`);
+ console.error(`Error when opening/writing to log file: ${error.message}`);
logFile.end();
process.exit(1);
});
diff --git a/src/main.ts b/src/main.ts
index 6add9a90..2663d3c1 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -16,7 +16,7 @@ import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js';
import {SetLevelRequestSchema} from '@modelcontextprotocol/sdk/types.js';
import type {Channel} from './browser.js';
-import {resolveBrowser} from './browser.js';
+import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
import {parseArguments} from './cli.js';
import {logger, saveLogsToFile} from './logger.js';
import {McpContext} from './McpContext.js';
@@ -69,15 +69,24 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {
let context: McpContext;
async function getContext(): Promise {
- const browser = await resolveBrowser({
- browserUrl: args.browserUrl,
- headless: args.headless,
- executablePath: args.executablePath,
- customDevTools: args.customDevtools,
- channel: args.channel as Channel,
- isolated: args.isolated,
- logFile,
- });
+ const extraArgs: string[] = [];
+ if (args.proxyServer) {
+ extraArgs.push(`--proxy-server=${args.proxyServer}`);
+ }
+ const browser = args.browserUrl
+ ? await ensureBrowserConnected(args.browserUrl)
+ : await ensureBrowserLaunched({
+ headless: args.headless,
+ executablePath: args.executablePath,
+ customDevTools: args.customDevtools,
+ channel: args.channel as Channel,
+ isolated: args.isolated,
+ logFile,
+ viewport: args.viewport,
+ args: extraArgs,
+ acceptInsecureCerts: args.acceptInsecureCerts,
+ });
+
if (context?.browser !== browser) {
context = await McpContext.from(browser, logger);
}
diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts
index a0741f4c..35718e54 100644
--- a/src/tools/ToolDefinition.ts
+++ b/src/tools/ToolDefinition.ts
@@ -5,7 +5,7 @@
*/
import type {Dialog, ElementHandle, Page} from 'puppeteer-core';
-import type z from 'zod';
+import z from 'zod';
import type {TraceResult} from '../trace-processing/parse.js';
@@ -72,7 +72,7 @@ export type Context = Readonly<{
setCpuThrottlingRate(rate: number): void;
saveTemporaryFile(
data: Uint8Array,
- mimeType: 'image/png' | 'image/jpeg',
+ mimeType: 'image/png' | 'image/jpeg' | 'image/webp',
): Promise<{filename: string}>;
waitForEventsAfterAction(action: () => Promise): Promise;
}>;
@@ -85,3 +85,16 @@ export function defineTool(
export const CLOSE_PAGE_ERROR =
'The last open page cannot be closed. It is fine to keep it open.';
+
+export const timeoutSchema = {
+ timeout: z
+ .number()
+ .int()
+ .optional()
+ .describe(
+ `Maximum wait time in milliseconds. If set to 0, the default timeout will be used.`,
+ )
+ .transform(value => {
+ return value && value <= 0 ? undefined : value;
+ }),
+};
diff --git a/src/tools/input.ts b/src/tools/input.ts
index 541e9414..eda04e80 100644
--- a/src/tools/input.ts
+++ b/src/tools/input.ts
@@ -197,7 +197,7 @@ export const uploadFile = defineTool({
// a type=file element. In this case, we want to default to
// Page.waitForFileChooser() and upload the file this way.
try {
- const page = await context.getSelectedPage();
+ const page = context.getSelectedPage();
const [fileChooser] = await Promise.all([
page.waitForFileChooser({timeout: 3000}),
handle.asLocator().click(),
diff --git a/src/tools/network.ts b/src/tools/network.ts
index 6ff2bb92..5943b0f5 100644
--- a/src/tools/network.ts
+++ b/src/tools/network.ts
@@ -84,6 +84,5 @@ export const getNetworkRequest = defineTool({
},
handler: async (request, response, _context) => {
response.attachNetworkRequest(request.params.url);
- response.setIncludeNetworkRequests(true);
},
});
diff --git a/src/tools/pages.ts b/src/tools/pages.ts
index c9962e7c..65d2b093 100644
--- a/src/tools/pages.ts
+++ b/src/tools/pages.ts
@@ -9,7 +9,7 @@ import z from 'zod';
import {logger} from '../logger.js';
import {ToolCategories} from './categories.js';
-import {CLOSE_PAGE_ERROR, defineTool} from './ToolDefinition.js';
+import {CLOSE_PAGE_ERROR, defineTool, timeoutSchema} from './ToolDefinition.js';
export const listPages = defineTool({
name: 'list_pages',
@@ -83,12 +83,15 @@ export const newPage = defineTool({
},
schema: {
url: z.string().describe('URL to load in a new page.'),
+ ...timeoutSchema,
},
handler: async (request, response, context) => {
const page = await context.newPage();
await context.waitForEventsAfterAction(async () => {
- await page.goto(request.params.url);
+ await page.goto(request.params.url, {
+ timeout: request.params.timeout,
+ });
});
response.setIncludePages(true);
@@ -104,12 +107,15 @@ export const navigatePage = defineTool({
},
schema: {
url: z.string().describe('URL to navigate the page to'),
+ ...timeoutSchema,
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
await context.waitForEventsAfterAction(async () => {
- await page.goto(request.params.url);
+ await page.goto(request.params.url, {
+ timeout: request.params.timeout,
+ });
});
response.setIncludePages(true);
@@ -129,15 +135,18 @@ export const navigatePageHistory = defineTool({
.describe(
'Whether to navigate back or navigate forward in the selected pages history',
),
+ ...timeoutSchema,
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
-
+ const options = {
+ timeout: request.params.timeout,
+ };
try {
if (request.params.navigate === 'back') {
- await page.goBack();
+ await page.goBack(options);
} else {
- await page.goForward();
+ await page.goForward(options);
}
} catch {
response.appendResponseLine(
diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts
index ae1f98bf..1a24eff8 100644
--- a/src/tools/screenshot.ts
+++ b/src/tools/screenshot.ts
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import {writeFile} from 'node:fs/promises';
+
import type {ElementHandle, Page} from 'puppeteer-core';
import z from 'zod';
@@ -19,7 +21,7 @@ export const screenshot = defineTool({
},
schema: {
format: z
- .enum(['png', 'jpeg'])
+ .enum(['png', 'jpeg', 'webp'])
.default('png')
.describe('Type of format to save the screenshot as. Default is "png"'),
quality: z
@@ -28,7 +30,7 @@ export const screenshot = defineTool({
.max(100)
.optional()
.describe(
- 'Compression quality for JPEG format (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.',
+ 'Compression quality for JPEG and WebP formats (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.',
),
uid: z
.string()
@@ -42,6 +44,12 @@ export const screenshot = defineTool({
.describe(
'If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid.',
),
+ filePath: z
+ .string()
+ .optional()
+ .describe(
+ 'The absolute path, or a path relative to the current working directory, to save the screenshot to instead of attaching it to the response.',
+ ),
},
handler: async (request, response, context) => {
if (request.params.uid && request.params.fullPage) {
@@ -76,7 +84,12 @@ export const screenshot = defineTool({
);
}
- if (screenshot.length >= 2_000_000) {
+ if (request.params.filePath) {
+ await writeFile(request.params.filePath, screenshot);
+ response.appendResponseLine(
+ `Saved screenshot to ${request.params.filePath}.`,
+ );
+ } else if (screenshot.length >= 2_000_000) {
const {filename} = await context.saveTemporaryFile(
screenshot,
`image/${request.params.format}`,
diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts
index f84d6d44..427e4f79 100644
--- a/src/tools/snapshot.ts
+++ b/src/tools/snapshot.ts
@@ -8,7 +8,7 @@ import {Locator} from 'puppeteer-core';
import z from 'zod';
import {ToolCategories} from './categories.js';
-import {defineTool} from './ToolDefinition.js';
+import {defineTool, timeoutSchema} from './ToolDefinition.js';
export const takeSnapshot = defineTool({
name: 'take_snapshot',
@@ -33,14 +33,24 @@ export const waitFor = defineTool({
},
schema: {
text: z.string().describe('Text to appear on the page'),
+ ...timeoutSchema,
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
+ const frames = page.frames();
- await Locator.race([
- page.locator(`aria/${request.params.text}`),
- page.locator(`text/${request.params.text}`),
- ]).wait();
+ const locator = Locator.race(
+ frames.flatMap(frame => [
+ frame.locator(`aria/${request.params.text}`),
+ frame.locator(`text/${request.params.text}`),
+ ]),
+ );
+
+ if (request.params.timeout) {
+ locator.setTimeout(request.params.timeout);
+ }
+
+ await locator.wait();
response.appendResponseLine(
`Element with text "${request.params.text}" found.`,
diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts
index 43f70eb3..87529102 100644
--- a/tests/McpResponse.test.ts
+++ b/tests/McpResponse.test.ts
@@ -105,7 +105,7 @@ uid=1_0 RootWebArea "My test page"
`# test response
## Network emulation
Emulating: Slow 3G
-Navigation timeout set to 100000 ms`,
+Default navigation timeout set to 100000 ms`,
);
});
});
diff --git a/tests/PageCollector.test.ts b/tests/PageCollector.test.ts
index ce61ac8e..0e8248ce 100644
--- a/tests/PageCollector.test.ts
+++ b/tests/PageCollector.test.ts
@@ -6,25 +6,37 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';
-import type {Browser, Frame, Page, PageEvents} from 'puppeteer-core';
+import type {Browser, Frame, Page, Target} from 'puppeteer-core';
import {PageCollector} from '../src/PageCollector.js';
import {getMockRequest} from './utils.js';
-function getMockPage(): Page {
- const listeners: Record void> = {};
- const mainFrame = {} as Frame;
+function mockListener() {
+ const listeners: Record void>> = {};
return {
- on(eventName, listener) {
- listeners[eventName] = listener;
+ on(eventName: string, listener: (data: unknown) => void) {
+ if (listeners[eventName]) {
+ listeners[eventName].push(listener);
+ } else {
+ listeners[eventName] = [listener];
+ }
},
- emit(eventName, data) {
- listeners[eventName]?.(data);
+ emit(eventName: string, data: unknown) {
+ for (const listener of listeners[eventName] ?? []) {
+ listener(data);
+ }
},
+ };
+}
+
+function getMockPage(): Page {
+ const mainFrame = {} as Frame;
+ return {
mainFrame() {
return mainFrame;
},
+ ...mockListener(),
} as Page;
}
@@ -34,9 +46,7 @@ function getMockBrowser(): Browser {
pages() {
return Promise.resolve(pages);
},
- on(_type, _handler) {
- // Mock
- },
+ ...mockListener(),
} as Browser;
}
@@ -113,4 +123,34 @@ describe('PageCollector', () => {
assert.equal(collector.getData(page).length, 1);
});
+
+ it('should only subscribe once', async () => {
+ const browser = getMockBrowser();
+ const page = (await browser.pages())[0];
+ const request = getMockRequest();
+ const collector = new PageCollector(browser, (pageListener, collect) => {
+ pageListener.on('request', req => {
+ collect(req);
+ });
+ });
+ await collector.init();
+ browser.emit('targetcreated', {
+ page() {
+ return Promise.resolve(page);
+ },
+ } as Target);
+
+ // The page inside part is async so we need to await some time
+ await new Promise(res => res());
+
+ assert.equal(collector.getData(page).length, 0);
+
+ page.emit('request', request);
+
+ assert.equal(collector.getData(page).length, 1);
+
+ page.emit('request', request);
+
+ assert.equal(collector.getData(page).length, 2);
+ });
});
diff --git a/tests/browser.test.ts b/tests/browser.test.ts
index 139a3b59..3dc8eff0 100644
--- a/tests/browser.test.ts
+++ b/tests/browser.test.ts
@@ -42,4 +42,31 @@ describe('browser', () => {
await browser1.close();
}
});
+
+ it('launches with the initial viewport', async () => {
+ const tmpDir = os.tmpdir();
+ const folderPath = path.join(tmpDir, `temp-folder-${crypto.randomUUID()}`);
+ const browser = await launch({
+ headless: true,
+ isolated: false,
+ userDataDir: folderPath,
+ executablePath: executablePath(),
+ viewport: {
+ width: 700,
+ height: 500,
+ },
+ });
+ try {
+ const [page] = await browser.pages();
+ const result = await page.evaluate(() => {
+ return {width: window.innerWidth, height: window.innerHeight};
+ });
+ assert.deepStrictEqual(result, {
+ width: 700,
+ height: 500,
+ });
+ } finally {
+ await browser.close();
+ }
+ });
});
diff --git a/tests/cli.test.ts b/tests/cli.test.ts
index 35088622..014faf95 100644
--- a/tests/cli.test.ts
+++ b/tests/cli.test.ts
@@ -55,4 +55,24 @@ describe('cli args parsing', () => {
executablePath: '/tmp/test 123/chrome',
});
});
+
+ it('parses viewport', async () => {
+ const args = parseArguments('1.0.0', [
+ 'node',
+ 'main.js',
+ '--viewport',
+ '888x777',
+ ]);
+ assert.deepStrictEqual(args, {
+ _: [],
+ headless: false,
+ isolated: false,
+ $0: 'npx chrome-devtools-mcp@latest',
+ channel: 'stable',
+ viewport: {
+ width: 888,
+ height: 777,
+ },
+ });
+ });
});
diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts
index 4dc21e49..c53cbc1d 100644
--- a/tests/tools/network.test.ts
+++ b/tests/tools/network.test.ts
@@ -38,5 +38,17 @@ describe('network', () => {
);
});
});
+ it('should not add the request list', async () => {
+ await withBrowser(async (response, context) => {
+ const page = await context.getSelectedPage();
+ await page.goto('data:text/html,Hello MCP
');
+ await getNetworkRequest.handler(
+ {params: {url: 'data:text/html,Hello MCP
'}},
+ response,
+ context,
+ );
+ assert(!response.includeNetworkRequests);
+ });
+ });
});
});
diff --git a/tests/tools/screenshot.test.ts b/tests/tools/screenshot.test.ts
index e6cdda17..d369f2ca 100644
--- a/tests/tools/screenshot.test.ts
+++ b/tests/tools/screenshot.test.ts
@@ -4,6 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
+import {rm, stat, mkdir, chmod, writeFile} from 'node:fs/promises';
+import {tmpdir} from 'node:os';
+import {join} from 'node:path';
import {describe, it} from 'node:test';
import {screenshot} from '../../src/tools/screenshot.js';
@@ -39,6 +42,18 @@ describe('screenshot', () => {
);
});
});
+ it('with webp', async () => {
+ await withBrowser(async (response, context) => {
+ await screenshot.handler({params: {format: 'webp'}}, response, context);
+
+ assert.equal(response.images.length, 1);
+ assert.equal(response.images[0].mimeType, 'image/webp');
+ assert.equal(
+ response.responseLines.at(0),
+ "Took a screenshot of the current page's viewport.",
+ );
+ });
+ });
it('with full page', async () => {
await withBrowser(async (response, context) => {
const fixture = screenshots.viewportOverflow;
@@ -108,5 +123,111 @@ describe('screenshot', () => {
);
});
});
+
+ it('with filePath', async () => {
+ await withBrowser(async (response, context) => {
+ const filePath = join(tmpdir(), 'test-screenshot.png');
+ try {
+ const fixture = screenshots.basic;
+ const page = context.getSelectedPage();
+ await page.setContent(fixture.html);
+ await screenshot.handler(
+ {params: {format: 'png', filePath}},
+ response,
+ context,
+ );
+
+ assert.equal(response.images.length, 0);
+ assert.equal(
+ response.responseLines.at(0),
+ "Took a screenshot of the current page's viewport.",
+ );
+ assert.equal(
+ response.responseLines.at(1),
+ `Saved screenshot to ${filePath}.`,
+ );
+
+ const stats = await stat(filePath);
+ assert.ok(stats.isFile());
+ assert.ok(stats.size > 0);
+ } finally {
+ await rm(filePath, {force: true});
+ }
+ });
+ });
+
+ it('with unwritable filePath', async () => {
+ if (process.platform === 'win32') {
+ const filePath = join(
+ tmpdir(),
+ 'readonly-file-for-screenshot-test.png',
+ );
+ // Create the file and make it read-only.
+ await writeFile(filePath, '');
+ await chmod(filePath, 0o400);
+
+ try {
+ await withBrowser(async (response, context) => {
+ const fixture = screenshots.basic;
+ const page = context.getSelectedPage();
+ await page.setContent(fixture.html);
+ await assert.rejects(
+ screenshot.handler(
+ {params: {format: 'png', filePath}},
+ response,
+ context,
+ ),
+ );
+ });
+ } finally {
+ // Make the file writable again so it can be deleted.
+ await chmod(filePath, 0o600);
+ await rm(filePath, {force: true});
+ }
+ } else {
+ const dir = join(tmpdir(), 'readonly-dir-for-screenshot-test');
+ await mkdir(dir, {recursive: true});
+ await chmod(dir, 0o500);
+ const filePath = join(dir, 'test-screenshot.png');
+
+ try {
+ await withBrowser(async (response, context) => {
+ const fixture = screenshots.basic;
+ const page = context.getSelectedPage();
+ await page.setContent(fixture.html);
+ await assert.rejects(
+ screenshot.handler(
+ {params: {format: 'png', filePath}},
+ response,
+ context,
+ ),
+ );
+ });
+ } finally {
+ await chmod(dir, 0o700);
+ await rm(dir, {recursive: true, force: true});
+ }
+ }
+ });
+
+ it('with malformed filePath', async () => {
+ await withBrowser(async (response, context) => {
+ // Use a platform-specific invalid character.
+ // On Windows, characters like '<', '>', ':', '"', '/', '\', '|', '?', '*' are invalid.
+ // On POSIX, the null byte is invalid.
+ const invalidChar = process.platform === 'win32' ? '>' : '\0';
+ const filePath = `malformed${invalidChar}path.png`;
+ const fixture = screenshots.basic;
+ const page = context.getSelectedPage();
+ await page.setContent(fixture.html);
+ await assert.rejects(
+ screenshot.handler(
+ {params: {format: 'png', filePath}},
+ response,
+ context,
+ ),
+ );
+ });
+ });
});
});
diff --git a/tests/tools/snapshot.test.ts b/tests/tools/snapshot.test.ts
index a44a0bf2..31857ff5 100644
--- a/tests/tools/snapshot.test.ts
+++ b/tests/tools/snapshot.test.ts
@@ -95,5 +95,32 @@ describe('snapshot', () => {
assert.ok(response.includeSnapshot);
});
});
+
+ it('should work with iframe content', async () => {
+ await withBrowser(async (response, context) => {
+ const page = await context.getSelectedPage();
+
+ await page.setContent(
+ html`Top level
+ `,
+ );
+
+ await waitFor.handler(
+ {
+ params: {
+ text: 'Hello iframe',
+ },
+ },
+ response,
+ context,
+ );
+
+ assert.equal(
+ response.responseLines[0],
+ 'Element with text "Hello iframe" found.',
+ );
+ assert.ok(response.includeSnapshot);
+ });
+ });
});
});