diff --git a/.github/ISSUE_TEMPLATE/01-bug.yml b/.github/ISSUE_TEMPLATE/01-bug.yml new file mode 100644 index 00000000..02010b90 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bug.yml @@ -0,0 +1,72 @@ +name: Bug report +description: File a bug report for chrome-devtools-mcp +title: '' +type: 'Bug' +body: + - id: description + type: textarea + attributes: + label: Description of the bug + description: > + A clear and concise description of what the bug is. + placeholder: + validations: + required: true + + - id: reproduce + type: textarea + attributes: + label: Reproduction + description: > + Steps to reproduce the behavior: + placeholder: | + 1. Use tool '...' + 2. Then use tool '...' + + - id: expectation + type: textarea + attributes: + label: Expectation + description: A clear and concise description of what you expected to happen. + + - id: mcp-configuration + type: textarea + attributes: + label: MCP configuration + + - id: node-version + type: input + attributes: + label: Node version + description: > + Please verify you have the minimal supported version listed in the README.md + + - id: chrome-version + type: input + attributes: + label: Chrome version + + - id: coding-agent-version + type: input + attributes: + label: Coding agent version + + - id: model-version + type: input + attributes: + label: Model version + + - id: chat-log + type: input + attributes: + label: Chat log + + - id: operating-system + type: dropdown + attributes: + label: Operating system + description: What supported operating system are you running? + options: + - Windows + - macOS + - Linux diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 96e9aee3..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Chrome version:** -**Coding agent version:** -**Model version:** - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Chat log** -The full log of the chat with your coding agent. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 18e7d6a4..63c22745 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,7 @@ updates: dependency-type: production exclude-patterns: - 'puppeteer*' + - 'chrome-devtools-frontend' patterns: - '*' dev-dependencies: @@ -23,6 +24,9 @@ updates: puppeteer: patterns: - 'puppeteer*' + chrome-devtools-frontend: + patterns: + - 'chrome-devtools-frontend' - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/publish-to-npm-on-tag.yml b/.github/workflows/publish-to-npm-on-tag.yml index 69112d3b..d50cdd97 100644 --- a/.github/workflows/publish-to-npm-on-tag.yml +++ b/.github/workflows/publish-to-npm-on-tag.yml @@ -4,6 +4,16 @@ on: push: tags: - 'chrome-devtools-mcp-v*' + workflow_dispatch: + inputs: + npm-publish: + description: 'Try to publish to NPM' + default: false + type: boolean + mcp-publish: + description: 'Try to publish to MCP registry' + default: true + type: boolean permissions: id-token: write # Required for OIDC @@ -12,6 +22,7 @@ permissions: jobs: publish-to-npm: runs-on: ubuntu-latest + if: ${{ (github.event_name != 'workflow_dispatch') || (inputs.npm-publish && always()) }} steps: - name: Check out repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -42,6 +53,7 @@ jobs: publish-to-mcp-registry: runs-on: ubuntu-latest needs: publish-to-npm + if: ${{ (github.event_name != 'workflow_dispatch' && needs.publish-to-npm.result == 'success') || (inputs.mcp-publish && always()) }} steps: - name: Check out repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -65,12 +77,11 @@ jobs: - name: Build run: npm run build - - name: Bump - run: npm run sync-server-json-version - - name: Install MCP Publisher run: | - curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v1.1.0/mcp-publisher_1.1.0_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher + export VERSION="1.2.1" + export OS=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') + curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v${VERSION}/mcp-publisher_${VERSION}_${OS}.tar.gz" | tar xz mcp-publisher - name: Login to MCP Registry run: ./mcp-publisher login github-oidc diff --git a/.release-please-manifest.json b/.release-please-manifest.json index bcd05228..64f3cdd6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.0" + ".": "0.8.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index d708adcb..1a42f71e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## [0.8.0](https://github.com/ChromeDevTools/chrome-devtools-mcp/compare/chrome-devtools-mcp-v0.7.1...chrome-devtools-mcp-v0.8.0) (2025-10-10) + + +### Features + +* support passing args to Chrome ([#338](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/338)) ([e1b5363](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/e1b536365363e1e1a3aa7661dd84290c794510ad)) + +## [0.7.1](https://github.com/ChromeDevTools/chrome-devtools-mcp/compare/chrome-devtools-mcp-v0.7.0...chrome-devtools-mcp-v0.7.1) (2025-10-10) + + +### Bug Fixes + +* document that console and requests are since the last nav ([#335](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/335)) ([9ad7cbb](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/9ad7cbb2de3d285e46e5f3e7c098b0a7535c7e7a)) + +## [0.7.0](https://github.com/ChromeDevTools/chrome-devtools-mcp/compare/chrome-devtools-mcp-v0.6.1...chrome-devtools-mcp-v0.7.0) (2025-10-10) + + +### Features + +* Add offline network emulation support to emulate_network command ([#326](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/326)) ([139ce60](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/139ce607814bf25ba541a7264ce96a04b2fac871)) +* add request and response body ([#267](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/267)) ([dd3c143](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/dd3c14336ee44d057d06231a5bfd5c5bcf661029)) + + +### Bug Fixes + +* ordering of information in performance trace summary ([#334](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/334)) ([2d4484a](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/2d4484a123968754b4840d112b9c1ca59fb29997)) +* publishing to MCP registry ([#313](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/313)) ([1faec78](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/1faec78f84569a03f63585fb84df35992bcfe81a)) +* use default ProtocolTimeout ([#315](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/315)) ([a525f19](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/a525f199458afb266db4540bf0fa8007323f3301)) + +## [0.6.1](https://github.com/ChromeDevTools/chrome-devtools-mcp/compare/chrome-devtools-mcp-v0.6.0...chrome-devtools-mcp-v0.6.1) (2025-10-07) + + +### Bug Fixes + +* change default screen size in headless ([#299](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/299)) ([357db65](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/357db65d18f87b1299a0f6212b7ec982ef187171)) +* **cli:** tolerate empty browser URLs ([#298](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/298)) ([098a904](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/098a904b363f3ad81595ed58c25d34dd7d82bcd8)) +* guard performance_stop_trace when tracing inactive ([#295](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/295)) ([8200194](https://github.com/ChromeDevTools/chrome-devtools-mcp/commit/8200194c8037cc30b8ab815e5ee0d0b2b000bea6)) + ## [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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cad5d3ef..cea89123 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,8 @@ for PR and commit titles. ## Installation +Check that you are using node version specified in .nvmrc, then run following commands: + ```sh git clone https://github.com/ChromeDevTools/chrome-devtools-mcp.git cd chrome-devtools-mcp diff --git a/README.md b/README.md index fa62a49c..8d9af355 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,16 @@ Add the following config to your MCP client: ### MCP Client configuration +
+ Amp + Follow https://ampcode.com/manual#mcp and use the config provided above. You can also install the Chrome DevTools MCP server using the CLI: + +```bash +amp mcp add chrome-devtools -- npx chrome-devtools-mcp@latest +``` + +
+
Claude Code Use the Claude Code CLI to add the Chrome DevTools MCP server (guide): @@ -111,7 +121,7 @@ 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: +Configure the following fields and press `CTRL+S` to save the configuration: - **Server name:** `chrome-devtools` - **Server Type:** `[1] Local` @@ -177,6 +187,15 @@ The same way chrome-devtools-mcp can be configured for JetBrains Junie in `Setti
+
+ Kiro + +In **Kiro Settings**, go to `Configure MCP` > `Open Workspace or User MCP Config` > Use the configuration snippet provided above. + +Or, from the IDE **Activity Bar** > `Kiro` > `MCP Servers` > `Click Open MCP Config`. Use the configuration snippet provided above. + +
+
Visual Studio @@ -280,7 +299,7 @@ The Chrome DevTools MCP server supports the following configuration option: - **Type:** string - **`--viewport`** - Initial viewport size for the Chrome instances started by the server. For example, `1280x720` + Initial viewport size for the Chrome instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px. - **Type:** string - **`--proxyServer`** @@ -291,6 +310,10 @@ The Chrome DevTools MCP server supports the following configuration option: If enabled, ignores errors relative to self-signed and expired certificates. Use with caution. - **Type:** boolean +- **`--chromeArg`** + Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp. + - **Type:** array + Pass them via the `args` property in the JSON configuration. For example: @@ -320,7 +343,7 @@ You can also run `npx chrome-devtools-mcp@latest --help` to see all available co `chrome-devtools-mcp` starts a Chrome's stable channel instance using the following user data directory: -- Linux / MacOS: `$HOME/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL` +- Linux / macOS: `$HOME/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL` - Windows: `%HOMEPATH%/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL` The user data directory is not cleared between runs and shared across @@ -328,6 +351,69 @@ all instances of `chrome-devtools-mcp`. Set the `isolated` option to `true` to use a temporary user data dir instead which will be cleared automatically after the browser is closed. +### Connecting to a running Chrome instance + +You can connect to a running Chrome instance by using the `--browser-url` option. This is useful if you want to use your existing Chrome profile or if you are running the MCP server in a sandboxed environment that does not allow starting a new Chrome instance. + +Here is a step-by-step guide on how to connect to a running Chrome Stable instance: + +**Step 1: Configure the MCP client** + +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. + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": [ + "chrome-devtools-mcp@latest", + "--browser-url=http://127.0.0.1:9222" + ] + } + } +} +``` + +**Step 2: Start the Chrome browser** + +> [!WARNING] +> Enabling the remote debugging port opens up a debugging port on the running browser instance. Any application on your machine can connect to this port and control the browser. Make sure that you are not browsing any sensitive websites while the debugging port is open. + +Start the Chrome browser with the remote debugging port enabled. Make sure to close any running Chrome instances before starting a new one with the debugging port enabled. The port number you choose must be the same as the one you specified in the `--browser-url` option in your MCP client configuration. + +For security reasons, [Chrome requires you to use a non-default user data directory](https://developer.chrome.com/blog/remote-debugging-port) when enabling the remote debugging port. You can specify a custom directory using the `--user-data-dir` flag. This ensures that your regular browsing profile and data are not exposed to the debugging session. + +**macOS** + +```bash +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile-stable +``` + +**Linux** + +```bash +/usr/bin/google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile-stable +``` + +**Windows** + +```bash +"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-profile-stable" +``` + +**Step 3: Test your setup** + +After configuring the MCP client and starting the Chrome browser, you can test your setup by running a simple prompt in your MCP client: + +``` +Check the performance of https://developers.chrome.com +``` + +Your MCP client should connect to the running Chrome instance and receive a performance report. + +For more details on remote debugging, see the [Chrome DevTools documentation](https://developer.chrome.com/docs/devtools/remote-debugging/). + ## Known limitations ### Operating system sandboxes @@ -336,5 +422,5 @@ Some MCP clients allow sandboxing the MCP server using macOS Seatbelt or Linux containers. If sandboxes are enabled, `chrome-devtools-mcp` is not able to start Chrome that requires permissions to create its own sandboxes. As a workaround, either disable sandboxing for `chrome-devtools-mcp` in your MCP client or use -`--connect-url` to connect to a Chrome instance that you start manually outside +`--browser-url` to connect to a Chrome instance that you start manually outside of the MCP client sandbox. diff --git a/docs/tool-reference.md b/docs/tool-reference.md index ad9410a2..be12f101 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -200,11 +200,11 @@ ### `emulate_network` -**Description:** Emulates network conditions such as throttling on the selected page. +**Description:** Emulates network conditions such as throttling or offline mode on the selected page. **Parameters:** -- **throttlingOption** (enum: "No emulation", "Slow 3G", "Fast 3G", "Slow 4G", "Fast 4G") **(required)**: The network throttling option to emulate. Available throttling options are: No emulation, Slow 3G, Fast 3G, Slow 4G, Fast 4G. Set to "No emulation" to disable. +- **throttlingOption** (enum: "No emulation", "Offline", "Slow 3G", "Fast 3G", "Slow 4G", "Fast 4G") **(required)**: The network throttling option to emulate. Available throttling options are: No emulation, Offline, Slow 3G, Fast 3G, Slow 4G, Fast 4G. Set to "No emulation" to disable. Set to "Offline" to simulate offline network conditions. --- @@ -264,7 +264,7 @@ ### `list_network_requests` -**Description:** List all requests for the currently selected page +**Description:** List all requests for the currently selected page since the last navigation. **Parameters:** @@ -298,7 +298,7 @@ so returned values have to JSON-serializable. ### `list_console_messages` -**Description:** List all console messages for the currently selected page +**Description:** List all console messages for the currently selected page since the last navigation. **Parameters:** None diff --git a/package-lock.json b/package-lock.json index 0248f049..5eb6888e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,20 @@ { "name": "chrome-devtools-mcp", - "version": "0.6.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chrome-devtools-mcp", - "version": "0.6.0", + "version": "0.8.0", "license": "Apache-2.0", "dependencies": { - "@modelcontextprotocol/sdk": "1.19.1", - "core-js": "3.45.1", + "@modelcontextprotocol/sdk": "1.20.0", + "core-js": "3.46.0", "debug": "4.4.3", - "puppeteer-core": "24.23.0", - "yargs": "18.0.0" + "puppeteer-core": "^24.24.0", + "yargs": "18.0.0", + "zod": "^3.25.76" }, "bin": { "chrome-devtools-mcp": "build/src/index.js" @@ -34,7 +35,7 @@ "eslint-plugin-import": "^2.32.0", "globals": "^16.4.0", "prettier": "^3.6.2", - "puppeteer": "24.23.0", + "puppeteer": "24.24.0", "sinon": "^21.0.0", "typescript": "^5.9.2", "typescript-eslint": "^8.43.0" @@ -371,9 +372,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.19.1.tgz", - "integrity": "sha512-3Y2h3MZKjec1eAqSTBclATlX+AbC6n1LgfVzRMJLt3v6w0RCYgwLrjbxPDbhsYHt6Wdqc/aCceNJYgj448ELQQ==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.0.tgz", + "integrity": "sha512-kOQ4+fHuT4KbR2iq2IjeV32HiihueuOf1vJkq18z08CLZ1UQrTc8BXJpVfxZkq45+inLLD+D4xx4nBjUelJa4Q==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -445,9 +446,9 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.10.10", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.10.tgz", - "integrity": "sha512-3ZG500+ZeLql8rE0hjfhkycJjDj0pI/btEh3L9IkWUYcOrgP0xCNRq3HbtbqOPbvDhFaAWD88pDFtlLv8ns8gA==", + "version": "2.10.11", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.11.tgz", + "integrity": "sha512-kp3ORGce+oC3qUMJ+g5NH9W4Q7mMG7gV2I+alv0bCbfkZ36B2V/xKCg9uYavSgjmsElhwBneahWjJP7A6fuKLw==", "license": "Apache-2.0", "dependencies": { "debug": "^4.4.3", @@ -455,7 +456,7 @@ "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.2", - "tar-fs": "^3.1.0", + "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { @@ -691,9 +692,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", - "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "version": "24.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", + "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -745,17 +746,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "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==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", + "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.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", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/type-utils": "8.46.0", + "@typescript-eslint/utils": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -775,16 +776,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz", - "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", + "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", "dependencies": { - "@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", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4" }, "engines": { @@ -800,14 +801,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "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==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", + "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.45.0", - "@typescript-eslint/types": "^8.45.0", + "@typescript-eslint/tsconfig-utils": "^8.46.0", + "@typescript-eslint/types": "^8.46.0", "debug": "^4.3.4" }, "engines": { @@ -822,14 +823,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "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==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", + "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.45.0", - "@typescript-eslint/visitor-keys": "8.45.0" + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -840,9 +841,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "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==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", + "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", "dev": true, "license": "MIT", "engines": { @@ -857,15 +858,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz", - "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz", + "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.45.0", - "@typescript-eslint/typescript-estree": "8.45.0", - "@typescript-eslint/utils": "8.45.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/utils": "8.46.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -882,9 +883,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz", - "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", + "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", "dev": true, "license": "MIT", "engines": { @@ -896,16 +897,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "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==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", + "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", "dev": true, "license": "MIT", "dependencies": { - "@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", + "@typescript-eslint/project-service": "8.46.0", + "@typescript-eslint/tsconfig-utils": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -925,16 +926,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz", - "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz", + "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.45.0", - "@typescript-eslint/types": "8.45.0", - "@typescript-eslint/typescript-estree": "8.45.0" + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -949,13 +950,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "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==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", + "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/types": "8.46.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1501,9 +1502,9 @@ } }, "node_modules/b4a": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.2.tgz", - "integrity": "sha512-DyUOdz+E8R6+sruDpQNOaV0y/dBbV6X/8ZkxrDcR0Ifc3BgKlpgG0VAtfOozA0eMtJO5GGe9FsZhueLs00pTww==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -1522,15 +1523,23 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", - "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", - "license": "Apache-2.0" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz", + "integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } }, "node_modules/bare-fs": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.4.tgz", - "integrity": "sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.9.tgz", + "integrity": "sha512-sh8UV8OvXBZa3Yg5rhF1LNH3U4DfHniexdqyUXelC1thQUxO9TCF37yvd1/7Ir+cgeSg/6YrXyH67xvRr7yaOg==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1898,9 +1907,9 @@ } }, "node_modules/core-js": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", - "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz", + "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -4710,18 +4719,18 @@ } }, "node_modules/puppeteer": { - "version": "24.23.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.23.0.tgz", - "integrity": "sha512-BVR1Lg8sJGKXY79JARdIssFWK2F6e1j+RyuJP66w4CUmpaXjENicmA3nNpUXA8lcTdDjAndtP+oNdni3T/qQqA==", + "version": "24.24.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.24.0.tgz", + "integrity": "sha512-jRn6T8rSrQZXIplXICpH2zYJ2XrIFY7Ug0+TxRTuwY8ZTL7+MKDvFH0aLG7Xx3ts4twzxIKZmiYo+qg7whNpZw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.10", + "@puppeteer/browsers": "2.10.11", "chromium-bidi": "9.1.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1508733", - "puppeteer-core": "24.23.0", + "puppeteer-core": "24.24.0", "typed-query-selector": "^2.12.0" }, "bin": { @@ -4732,12 +4741,12 @@ } }, "node_modules/puppeteer-core": { - "version": "24.23.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.23.0.tgz", - "integrity": "sha512-yl25C59gb14sOdIiSnJ08XiPP+O2RjuyZmEG+RjYmCXO7au0jcLf7fRiyii96dXGUBW7Zwei/mVKfxMx/POeFw==", + "version": "24.24.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.24.0.tgz", + "integrity": "sha512-RR5AeQ6dIbSepDe9PTtfgK1fgD7TuA9qqyGxPbFCyGfvfkbR7MiqNYdE7AhbTaFIqG3hFBtWwbVKVZF8oEqj7Q==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.10", + "@puppeteer/browsers": "2.10.11", "chromium-bidi": "9.1.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1508733", @@ -5748,16 +5757,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.45.0.tgz", - "integrity": "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz", + "integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "@typescript-eslint/eslint-plugin": "8.46.0", + "@typescript-eslint/parser": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/utils": "8.46.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6194,19 +6203,17 @@ } }, "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", - "license": "MIT", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "license": "ISC", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "peerDependencies": { "zod": "^3.24.1" } diff --git a/package.json b/package.json index 63f7ceee..57feb412 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chrome-devtools-mcp", - "version": "0.6.0", + "version": "0.8.0", "description": "MCP server for Chrome DevTools", "type": "module", "bin": "./build/src/index.js", @@ -19,8 +19,7 @@ "test:only": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"", "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\"", "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\"", - "prepare": "node --experimental-strip-types scripts/prepare.ts", - "sync-server-json-version": "node --experimental-strip-types scripts/sync-server-json-version.ts && npm run format" + "prepare": "node --experimental-strip-types scripts/prepare.ts" }, "files": [ "build/src", @@ -37,11 +36,12 @@ "homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp#readme", "mcpName": "io.github.ChromeDevTools/chrome-devtools-mcp", "dependencies": { - "@modelcontextprotocol/sdk": "1.19.1", - "core-js": "3.45.1", + "@modelcontextprotocol/sdk": "1.20.0", + "core-js": "3.46.0", "debug": "4.4.3", - "puppeteer-core": "24.23.0", - "yargs": "18.0.0" + "puppeteer-core": "^24.24.0", + "yargs": "18.0.0", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/js": "^9.35.0", @@ -59,7 +59,7 @@ "eslint-plugin-import": "^2.32.0", "globals": "^16.4.0", "prettier": "^3.6.2", - "puppeteer": "24.23.0", + "puppeteer": "24.24.0", "sinon": "^21.0.0", "typescript": "^5.9.2", "typescript-eslint": "^8.43.0" diff --git a/release-please-config.json b/release-please-config.json index 826a6b2f..1cfd0ac1 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,5 +1,22 @@ { "packages": { - ".": {} + ".": { + "extra-files": [ + { + "type": "generic", + "path": "src/main.ts" + }, + { + "type": "json", + "path": "server.json", + "jsonpath": "version" + }, + { + "type": "json", + "path": "server.json", + "jsonpath": "packages[0].version" + } + ] + } } } diff --git a/scripts/sync-server-json-version.ts b/scripts/sync-server-json-version.ts deleted file mode 100644 index 27fe176e..00000000 --- a/scripts/sync-server-json-version.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -import fs from 'node:fs'; - -const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf-8')); -const serverJson = JSON.parse(fs.readFileSync('./server.json', 'utf-8')); - -serverJson.version = packageJson.version; -for (const pkg of serverJson.packages) { - pkg.version = packageJson.version; -} - -fs.writeFileSync('./server.json', JSON.stringify(serverJson, null, 2)); diff --git a/server.json b/server.json index 11ce929d..1f1e7f6b 100644 --- a/server.json +++ b/server.json @@ -1,19 +1,18 @@ { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.ChromeDevTools/chrome-devtools-mcp", "description": "MCP server for Chrome DevTools", - "status": "active", "repository": { "url": "https://github.com/ChromeDevTools/chrome-devtools-mcp", "source": "github" }, - "version": "0.2.5", + "version": "0.6.0", "packages": [ { "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "chrome-devtools-mcp", - "version": "0.2.5", + "version": "0.6.0", "transport": { "type": "stdio" }, diff --git a/src/McpContext.ts b/src/McpContext.ts index d1037935..5b29ce09 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -272,6 +272,10 @@ export class McpContext implements Context { return page.getDefaultNavigationTimeout(); } + getAXNodeByUid(uid: string) { + return this.#textSnapshot?.idToNode.get(uid); + } + async getElementByUid(uid: string): Promise> { if (!this.#textSnapshot?.idToNode.size) { throw new Error( @@ -326,19 +330,37 @@ export class McpContext implements Context { // will be used for the tree serialization and mapping ids back to nodes. let idCounter = 0; const idToNode = new Map(); - const assignIds = (node: SerializedAXNode): TextSnapshotNode => { + const assignIds = async ( + node: SerializedAXNode, + ): Promise => { const nodeWithId: TextSnapshotNode = { ...node, id: `${snapshotId}_${idCounter++}`, - children: node.children - ? node.children.map(child => assignIds(child)) - : [], + children: [], }; + + // The AXNode for an option doesn't contain its `value`. + // Therefore, set text content of the option as value. + if (node.role === 'option') { + const handle = await node.elementHandle(); + if (handle) { + const textContentHandle = await handle.getProperty('textContent'); + const optionText = await textContentHandle.jsonValue(); + if (optionText) { + nodeWithId.value = optionText.toString(); + } + } + } + + nodeWithId.children = node.children + ? await Promise.all(node.children.map(child => assignIds(child))) + : []; + idToNode.set(nodeWithId.id, nodeWithId); return nodeWithId; }; - const rootNodeWithId = assignIds(rootNode); + const rootNodeWithId = await assignIds(rootNode); this.#textSnapshot = { root: rootNodeWithId, snapshotId: String(snapshotId), diff --git a/src/McpResponse.ts b/src/McpResponse.ts index fa3c69ae..bf7603bf 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -12,6 +12,8 @@ import type {ResourceType} from 'puppeteer-core'; import {formatConsoleEvent} from './formatters/consoleFormatter.js'; import { getFormattedHeaderValue, + getFormattedResponseBody, + getFormattedRequestBody, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js'; @@ -21,10 +23,16 @@ import {handleDialog} from './tools/pages.js'; import type {ImageContentData, Response} from './tools/ToolDefinition.js'; import {paginate, type PaginationOptions} from './utils/pagination.js'; +interface NetworkRequestData { + networkRequestUrl: string; + requestBody?: string; + responseBody?: string; +} + export class McpResponse implements Response { #includePages = false; #includeSnapshot = false; - #attachedNetworkRequestUrl?: string; + #attachedNetworkRequestData?: NetworkRequestData; #includeConsoleData = false; #textResponseLines: string[] = []; #formattedConsoleData?: string[]; @@ -74,7 +82,9 @@ export class McpResponse implements Response { } attachNetworkRequest(url: string): void { - this.#attachedNetworkRequestUrl = url; + this.#attachedNetworkRequestData = { + networkRequestUrl: url, + }; } get includePages(): boolean { @@ -89,7 +99,7 @@ export class McpResponse implements Response { return this.#includeConsoleData; } get attachedNetworkRequestUrl(): string | undefined { - return this.#attachedNetworkRequestUrl; + return this.#attachedNetworkRequestData?.networkRequestUrl; } get networkRequestsPageIdx(): number | undefined { return this.#networkRequestsOptions?.pagination?.pageIdx; @@ -127,6 +137,22 @@ export class McpResponse implements Response { } let formattedConsoleMessages: string[]; + + if (this.#attachedNetworkRequestData?.networkRequestUrl) { + const request = context.getNetworkRequestByUrl( + this.#attachedNetworkRequestData.networkRequestUrl, + ); + + this.#attachedNetworkRequestData.requestBody = + await getFormattedRequestBody(request); + + const response = request.response(); + if (response) { + this.#attachedNetworkRequestData.responseBody = + await getFormattedResponseBody(response); + } + } + if (this.#includeConsoleData) { const consoleMessages = context.getConsoleData(); if (consoleMessages) { @@ -274,10 +300,11 @@ Call ${handleDialog.name} to handle it before continuing.`); #getIncludeNetworkRequestsData(context: McpContext): string[] { const response: string[] = []; - const url = this.#attachedNetworkRequestUrl; + const url = this.#attachedNetworkRequestData?.networkRequestUrl; if (!url) { return response; } + const httpRequest = context.getNetworkRequestByUrl(url); response.push(`## Request ${httpRequest.url()}`); response.push(`Status: ${getStatusFromRequest(httpRequest)}`); @@ -286,6 +313,11 @@ Call ${handleDialog.name} to handle it before continuing.`); response.push(line); } + if (this.#attachedNetworkRequestData?.requestBody) { + response.push(`### Request Body`); + response.push(this.#attachedNetworkRequestData.requestBody); + } + const httpResponse = httpRequest.response(); if (httpResponse) { response.push(`### Response Headers`); @@ -294,6 +326,11 @@ Call ${handleDialog.name} to handle it before continuing.`); } } + if (this.#attachedNetworkRequestData?.responseBody) { + response.push(`### Response Body`); + response.push(this.#attachedNetworkRequestData.responseBody); + } + const httpFailure = httpRequest.failure(); if (httpFailure) { response.push(`### Request failed with`); diff --git a/src/browser.ts b/src/browser.ts index d5a17e17..76fd14f4 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -11,7 +11,6 @@ import path from 'node:path'; import type { Browser, ChromeReleaseChannel, - ConnectOptions, LaunchOptions, Target, } from 'puppeteer-core'; @@ -19,39 +18,41 @@ import puppeteer from 'puppeteer-core'; let browser: Browser | undefined; -const ignoredPrefixes = new Set([ - 'chrome://', - 'chrome-extension://', - 'chrome-untrusted://', - 'devtools://', -]); +function makeTargetFilter(devtools: boolean) { + const ignoredPrefixes = new Set([ + 'chrome://', + 'chrome-extension://', + 'chrome-untrusted://', + ]); -function targetFilter(target: Target): boolean { - if (target.url() === 'chrome://newtab/') { - return true; + if (!devtools) { + ignoredPrefixes.add('devtools://'); } - for (const prefix of ignoredPrefixes) { - if (target.url().startsWith(prefix)) { - return false; + return function targetFilter(target: Target): boolean { + if (target.url() === 'chrome://newtab/') { + return true; } - } - return true; + for (const prefix of ignoredPrefixes) { + if (target.url().startsWith(prefix)) { + return false; + } + } + return true; + }; } -const connectOptions: ConnectOptions = { - targetFilter, - // We do not expect any single CDP command to take more than 10sec. - protocolTimeout: 10_000, -}; - -export async function ensureBrowserConnected(browserURL: string) { +export async function ensureBrowserConnected(options: { + browserURL: string; + devtools: boolean; +}) { if (browser?.connected) { return browser; } browser = await puppeteer.connect({ - ...connectOptions, - browserURL, + targetFilter: makeTargetFilter(options.devtools), + browserURL: options.browserURL, defaultViewport: null, + handleDevToolsAsPage: options.devtools, }); return browser; } @@ -59,7 +60,6 @@ export async function ensureBrowserConnected(browserURL: string) { interface McpLaunchOptions { acceptInsecureCerts?: boolean; executablePath?: string; - customDevTools?: string; channel?: Channel; userDataDir?: string; headless: boolean; @@ -70,10 +70,11 @@ interface McpLaunchOptions { height: number; }; args?: string[]; + devtools: boolean; } export async function launch(options: McpLaunchOptions): Promise { - const {channel, executablePath, customDevTools, headless, isolated} = options; + const {channel, executablePath, headless, isolated} = options; const profileDirName = channel && channel !== 'stable' ? `chrome-profile-${channel}` @@ -96,10 +97,13 @@ export async function launch(options: McpLaunchOptions): Promise { ...(options.args ?? []), '--hide-crash-restore-bubble', ]; - if (customDevTools) { - args.push(`--custom-devtools-frontend=file://${customDevTools}`); + if (headless) { + args.push('--screen-info={3840x2160}'); } let puppeteerChannel: ChromeReleaseChannel | undefined; + if (options.devtools) { + args.push('--auto-open-devtools-for-tabs'); + } if (!executablePath) { puppeteerChannel = channel && channel !== 'stable' @@ -109,8 +113,8 @@ export async function launch(options: McpLaunchOptions): Promise { try { const browser = await puppeteer.launch({ - ...connectOptions, channel: puppeteerChannel, + targetFilter: makeTargetFilter(options.devtools), executablePath, defaultViewport: null, userDataDir, @@ -118,6 +122,7 @@ export async function launch(options: McpLaunchOptions): Promise { headless, args, acceptInsecureCerts: options.acceptInsecureCerts, + handleDevToolsAsPage: options.devtools, }); if (options.logFile) { // FIXME: we are probably subscribing too late to catch startup logs. We diff --git a/src/cli.ts b/src/cli.ts index 23719fb1..513fff33 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,7 +14,10 @@ export const cliOptions = { 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', - coerce: (url: string) => { + coerce: (url: string | undefined) => { + if (!url) { + return; + } try { new URL(url); } catch { @@ -40,13 +43,6 @@ export const cliOptions = { 'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed.', default: false, }, - customDevtools: { - type: 'string', - description: 'Path to custom DevTools.', - hidden: true, - conflicts: 'browserUrl', - alias: 'd', - }, channel: { type: 'string', description: @@ -62,7 +58,7 @@ export const cliOptions = { viewport: { type: 'string', describe: - 'Initial viewport size for the Chrome instances started by the server. For example, `1280x720`', + 'Initial viewport size for the Chrome instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.', coerce: (arg: string | undefined) => { if (arg === undefined) { return; @@ -85,6 +81,16 @@ export const cliOptions = { type: 'boolean', description: `If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.`, }, + experimentalDevtools: { + type: 'boolean', + describe: 'Whether to enable automation over DevTools targets', + hidden: true, + }, + chromeArg: { + type: 'array', + describe: + 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.', + }, } satisfies Record; export function parseArguments(version: string, argv = process.argv) { @@ -114,6 +120,10 @@ export function parseArguments(version: string, argv = process.argv) { '$0 --viewport 1280x720', 'Launch Chrome with the initial viewport size of 1280x720px', ], + [ + `$0 --chrome-arg='--no-sandbox' --chrome-arg='--disable-setuid-sandbox'`, + 'Launch Chrome without sandboxes. Use with caution.', + ], ]); return yargsInstance diff --git a/src/formatters/networkFormatter.ts b/src/formatters/networkFormatter.ts index f74e954f..7796f01a 100644 --- a/src/formatters/networkFormatter.ts +++ b/src/formatters/networkFormatter.ts @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {HTTPRequest} from 'puppeteer-core'; +import {isUtf8} from 'node:buffer'; + +import type {HTTPRequest, HTTPResponse} from 'puppeteer-core'; + +const BODY_CONTEXT_SIZE_LIMIT = 10000; export function getShortDescriptionForRequest(request: HTTPRequest): string { return `${request.url()} ${request.method()} ${getStatusFromRequest(request)}`; @@ -37,3 +41,61 @@ export function getFormattedHeaderValue( } return response; } + +export async function getFormattedResponseBody( + httpResponse: HTTPResponse, + sizeLimit = BODY_CONTEXT_SIZE_LIMIT, +): Promise { + try { + const responseBuffer = await httpResponse.buffer(); + + if (isUtf8(responseBuffer)) { + const responseAsTest = responseBuffer.toString('utf-8'); + + if (responseAsTest.length === 0) { + return ``; + } + + return `${getSizeLimitedString(responseAsTest, sizeLimit)}`; + } + + return ``; + } catch { + // buffer() call might fail with CDP exception, in this case we don't print anything in the context + return; + } +} + +export async function getFormattedRequestBody( + httpRequest: HTTPRequest, + sizeLimit: number = BODY_CONTEXT_SIZE_LIMIT, +): Promise { + if (httpRequest.hasPostData()) { + const data = httpRequest.postData(); + + if (data) { + return `${getSizeLimitedString(data, sizeLimit)}`; + } + + try { + const fetchData = await httpRequest.fetchPostData(); + + if (fetchData) { + return `${getSizeLimitedString(fetchData, sizeLimit)}`; + } + } catch { + // fetchPostData() call might fail with CDP exception, in this case we don't print anything in the context + return; + } + } + + return; +} + +function getSizeLimitedString(text: string, sizeLimit: number) { + if (text.length > sizeLimit) { + return `${text.substring(0, sizeLimit) + '... '}`; + } + + return `${text}`; +} diff --git a/src/main.ts b/src/main.ts index 2663d3c1..d35e1f14 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,10 +6,6 @@ import './polyfill.js'; -import assert from 'node:assert'; -import fs from 'node:fs'; -import path from 'node:path'; - import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'; @@ -33,33 +29,21 @@ import * as scriptTools from './tools/script.js'; import * as snapshotTools from './tools/snapshot.js'; import type {ToolDefinition} from './tools/ToolDefinition.js'; -function readPackageJson(): {version?: string} { - const currentDir = import.meta.dirname; - const packageJsonPath = path.join(currentDir, '..', '..', 'package.json'); - if (!fs.existsSync(packageJsonPath)) { - return {}; - } - try { - const json = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); - assert.strict(json['name'], 'chrome-devtools-mcp'); - return json; - } catch { - return {}; - } -} - -const version = readPackageJson().version ?? 'unknown'; +// If moved update release-please config +// x-release-please-start-version +const VERSION = '0.8.0'; +// x-release-please-end -export const args = parseArguments(version); +export const args = parseArguments(VERSION); const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined; -logger(`Starting Chrome DevTools MCP Server v${version}`); +logger(`Starting Chrome DevTools MCP Server v${VERSION}`); const server = new McpServer( { name: 'chrome_devtools', title: 'Chrome DevTools MCP server', - version, + version: VERSION, }, {capabilities: {logging: {}}}, ); @@ -69,22 +53,26 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => { let context: McpContext; async function getContext(): Promise { - const extraArgs: string[] = []; + const extraArgs: string[] = (args.chromeArg ?? []).map(String); if (args.proxyServer) { extraArgs.push(`--proxy-server=${args.proxyServer}`); } + const devtools = args.experimentalDevtools ?? false; const browser = args.browserUrl - ? await ensureBrowserConnected(args.browserUrl) + ? await ensureBrowserConnected({ + browserURL: args.browserUrl, + devtools, + }) : 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, + devtools, }); if (context?.browser !== browser) { @@ -143,6 +131,9 @@ function registerTool(tool: ToolDefinition): void { isError: true, }; } + } catch (err) { + logger(`${tool.name} error: ${err.message}`); + throw err; } finally { guard.dispose(); } @@ -160,9 +151,14 @@ const tools = [ ...Object.values(screenshotTools), ...Object.values(scriptTools), ...Object.values(snapshotTools), -]; +] as ToolDefinition[]; + +tools.sort((a, b) => { + return a.name.localeCompare(b.name); +}); + for (const tool of tools) { - registerTool(tool as unknown as ToolDefinition); + registerTool(tool); } const transport = new StdioServerTransport(); diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index fe2fae7b..56fdb53a 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -7,6 +7,7 @@ import type {Dialog, ElementHandle, Page} from 'puppeteer-core'; import z from 'zod'; +import type {TextSnapshotNode} from '../McpContext.js'; import type {TraceResult} from '../trace-processing/parse.js'; import type {ToolCategories} from './categories.js'; @@ -68,6 +69,7 @@ export type Context = Readonly<{ closePage(pageIdx: number): Promise; setSelectedPageIdx(idx: number): void; getElementByUid(uid: string): Promise>; + getAXNodeByUid(uid: string): TextSnapshotNode | undefined; setNetworkConditions(conditions: string | null): void; setCpuThrottlingRate(rate: number): void; saveTemporaryFile( diff --git a/src/tools/console.ts b/src/tools/console.ts index 9a3ff114..4fb752f2 100644 --- a/src/tools/console.ts +++ b/src/tools/console.ts @@ -9,7 +9,8 @@ import {defineTool} from './ToolDefinition.js'; export const consoleTool = defineTool({ name: 'list_console_messages', - description: 'List all console messages for the currently selected page', + description: + 'List all console messages for the currently selected page since the last navigation.', annotations: { category: ToolCategories.DEBUGGING, readOnlyHint: true, diff --git a/src/tools/emulation.ts b/src/tools/emulation.ts index 9228c59b..92b949e9 100644 --- a/src/tools/emulation.ts +++ b/src/tools/emulation.ts @@ -12,12 +12,13 @@ import {defineTool} from './ToolDefinition.js'; const throttlingOptions: [string, ...string[]] = [ 'No emulation', + 'Offline', ...Object.keys(PredefinedNetworkConditions), ]; export const emulateNetwork = defineTool({ name: 'emulate_network', - description: `Emulates network conditions such as throttling on the selected page.`, + description: `Emulates network conditions such as throttling or offline mode on the selected page.`, annotations: { category: ToolCategories.EMULATION, readOnlyHint: false, @@ -26,7 +27,7 @@ export const emulateNetwork = defineTool({ throttlingOption: z .enum(throttlingOptions) .describe( - `The network throttling option to emulate. Available throttling options are: ${throttlingOptions.join(', ')}. Set to "No emulation" to disable.`, + `The network throttling option to emulate. Available throttling options are: ${throttlingOptions.join(', ')}. Set to "No emulation" to disable. Set to "Offline" to simulate offline network conditions.`, ), }, handler: async (request, _response, context) => { @@ -39,6 +40,17 @@ export const emulateNetwork = defineTool({ return; } + if (conditions === 'Offline') { + await page.emulateNetworkConditions({ + offline: true, + download: 0, + upload: 0, + latency: 0, + }); + context.setNetworkConditions('Offline'); + return; + } + if (conditions in PredefinedNetworkConditions) { const networkCondition = PredefinedNetworkConditions[ diff --git a/src/tools/input.ts b/src/tools/input.ts index eda04e80..02bb8a0f 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -7,6 +7,8 @@ import type {ElementHandle} from 'puppeteer-core'; import z from 'zod'; +import type {McpContext, TextSnapshotNode} from '../McpContext.js'; + import {ToolCategories} from './categories.js'; import {defineTool} from './ToolDefinition.js'; @@ -78,6 +80,61 @@ export const hover = defineTool({ }, }); +// The AXNode for an option doesn't contain its `value`. We set text content of the option as value. +// If the form is a combobox, we need to find the correct option by its text value. +// To do that, loop through the children while checking which child's text matches the requested value (requested value is actually the text content). +// When the correct option is found, use the element handle to get the real value. +async function selectOption( + handle: ElementHandle, + aXNode: TextSnapshotNode, + value: string, +) { + let optionFound = false; + for (const child of aXNode.children) { + if (child.role === 'option' && child.name === value && child.value) { + optionFound = true; + const childHandle = await child.elementHandle(); + if (childHandle) { + try { + const childValueHandle = await childHandle.getProperty('value'); + try { + const childValue = await childValueHandle.jsonValue(); + if (childValue) { + await handle.asLocator().fill(childValue.toString()); + } + } finally { + void childValueHandle.dispose(); + } + break; + } finally { + void childHandle.dispose(); + } + } + } + } + if (!optionFound) { + throw new Error(`Could not find option with text "${value}"`); + } +} + +async function fillFormElement( + uid: string, + value: string, + context: McpContext, +) { + const handle = await context.getElementByUid(uid); + try { + const aXNode = context.getAXNodeByUid(uid); + if (aXNode && aXNode.role === 'combobox') { + await selectOption(handle, aXNode, value); + } else { + await handle.asLocator().fill(value); + } + } finally { + void handle.dispose(); + } +} + export const fill = defineTool({ name: 'fill', description: `Type text into a input, text area or select an option from a `, + ); + await context.createTextSnapshot(); + await fill.handler( + { + params: { + uid: '1_1', + value: 'two', + }, + }, + response, + context, + ); + assert.strictEqual( + response.responseLines[0], + 'Successfully filled out the element', + ); + assert.ok(response.includeSnapshot); + const selectedValue = await page.evaluate( + () => document.querySelector('select')!.value, + ); + assert.strictEqual(selectedValue, 'v2'); + }); + }); }); describe('drags', () => { diff --git a/tests/tools/performance.test.js.snapshot b/tests/tools/performance.test.js.snapshot index 071909f4..ba704151 100644 --- a/tests/tools/performance.test.js.snapshot +++ b/tests/tools/performance.test.js.snapshot @@ -50,7 +50,51 @@ No buffer was provided. exports[`performance > performance_stop_trace > returns the high level summary of the performance trace 1`] = ` The performance trace has been stopped. -Here is a high level summary of the trace and the Insights that were found: +## Summary of Performance trace findings: +URL: https://web.dev/ +Bounds: {min: 122410994891, max: 122416385853} +CPU throttling: none +Network throttling: none +Metrics (lab / observed): + - LCP: 129 ms, event: (eventKey: r-6063, ts: 122411126100), nodeId: 7 + - LCP breakdown: + - TTFB: 8 ms, bounds: {min: 122410996889, max: 122411004828} + - Load delay: 33 ms, bounds: {min: 122411004828, max: 122411037986} + - Load duration: 15 ms, bounds: {min: 122411037986, max: 122411052690} + - Render delay: 73 ms, bounds: {min: 122411052690, max: 122411126100} + - CLS: 0.00 +Metrics (field / real users): n/a – no data for this page in CrUX +Available insights: + - insight name: LCPBreakdown + description: Each [subpart has specific improvement strategies](https://web.dev/articles/optimize-lcp#lcp-breakdown). Ideally, most of the LCP time should be spent on loading the resources, not within delays. + relevant trace bounds: {min: 122410996889, max: 122411126100} + example question: Help me optimize my LCP score + example question: Which LCP phase was most problematic? + example question: What can I do to reduce the LCP time for this page load? + - insight name: LCPDiscovery + description: Optimize LCP by making the LCP image [discoverable](https://web.dev/articles/optimize-lcp#1_eliminate_resource_load_delay) from the HTML immediately, and [avoiding lazy-loading](https://web.dev/articles/lcp-lazy-loading) + relevant trace bounds: {min: 122411004828, max: 122411055039} + example question: Suggest fixes to reduce my LCP + example question: What can I do to reduce my LCP discovery time? + example question: Why is LCP discovery time important? + - insight name: RenderBlocking + description: Requests are blocking the page's initial render, which may delay LCP. [Deferring or inlining](https://web.dev/learn/performance/understanding-the-critical-path#render-blocking_resources) can move these network requests out of the critical path. + relevant trace bounds: {min: 122411037528, max: 122411053852} + example question: Show me the most impactful render blocking requests that I should focus on + example question: How can I reduce the number of render blocking requests? + - insight name: DocumentLatency + description: Your first network request is the most important. Reduce its latency by avoiding redirects, ensuring a fast server response, and enabling text compression. + relevant trace bounds: {min: 122410998910, max: 122411043781} + estimated metric savings: FCP 0 ms, LCP 0 ms + estimated wasted bytes: 77.1 kB + example question: How do I decrease the initial loading time of my page? + example question: Did anything slow down the request for this document? + - insight name: ThirdParties + description: 3rd party code can significantly impact load performance. [Reduce and defer loading of 3rd party code](https://web.dev/articles/optimizing-content-efficiency-loading-third-party-javascript/) to prioritize your page's content. + relevant trace bounds: {min: 122411037881, max: 122416229595} + example question: Which third parties are having the largest impact on my page performance? + +## Details on call tree & network request formats: Information on performance traces may contain main thread activity represented as call frames and network requests. Each call frame is presented in the following format: @@ -105,48 +149,4 @@ Durations (all in milliseconds): - \`responseHeaders\`: A list (separated by '|') of values for specific, pre-defined response headers, enclosed in square brackets. The order of headers corresponds to an internal fixed list. If a header is not present, its value will be empty. - - -URL: https://web.dev/ -Bounds: {min: 122410994891, max: 122416385853} -CPU throttling: none -Network throttling: none -Metrics (lab / observed): - - LCP: 129 ms, event: (eventKey: r-6063, ts: 122411126100), nodeId: 7 - - LCP breakdown: - - TTFB: 8 ms, bounds: {min: 122410996889, max: 122411004828} - - Load delay: 33 ms, bounds: {min: 122411004828, max: 122411037986} - - Load duration: 15 ms, bounds: {min: 122411037986, max: 122411052690} - - Render delay: 73 ms, bounds: {min: 122411052690, max: 122411126100} - - CLS: 0.00 -Metrics (field / real users): n/a – no data for this page in CrUX -Available insights: - - insight name: LCPBreakdown - description: Each [subpart has specific improvement strategies](https://web.dev/articles/optimize-lcp#lcp-breakdown). Ideally, most of the LCP time should be spent on loading the resources, not within delays. - relevant trace bounds: {min: 122410996889, max: 122411126100} - example question: Help me optimize my LCP score - example question: Which LCP phase was most problematic? - example question: What can I do to reduce the LCP time for this page load? - - insight name: LCPDiscovery - description: Optimize LCP by making the LCP image [discoverable](https://web.dev/articles/optimize-lcp#1_eliminate_resource_load_delay) from the HTML immediately, and [avoiding lazy-loading](https://web.dev/articles/lcp-lazy-loading) - relevant trace bounds: {min: 122411004828, max: 122411055039} - example question: Suggest fixes to reduce my LCP - example question: What can I do to reduce my LCP discovery time? - example question: Why is LCP discovery time important? - - insight name: RenderBlocking - description: Requests are blocking the page's initial render, which may delay LCP. [Deferring or inlining](https://web.dev/learn/performance/understanding-the-critical-path#render-blocking_resources) can move these network requests out of the critical path. - relevant trace bounds: {min: 122411037528, max: 122411053852} - example question: Show me the most impactful render blocking requests that I should focus on - example question: How can I reduce the number of render blocking requests? - - insight name: DocumentLatency - description: Your first network request is the most important. Reduce its latency by avoiding redirects, ensuring a fast server response, and enabling text compression. - relevant trace bounds: {min: 122410998910, max: 122411043781} - estimated metric savings: FCP 0 ms, LCP 0 ms - estimated wasted bytes: 77.1 kB - example question: How do I decrease the initial loading time of my page? - example question: Did anything slow down the request for this document? - - insight name: ThirdParties - description: 3rd party code can significantly impact load performance. [Reduce and defer loading of 3rd party code](https://web.dev/articles/optimizing-content-efficiency-loading-third-party-javascript/) to prioritize your page's content. - relevant trace bounds: {min: 122411037881, max: 122416229595} - example question: Which third parties are having the largest impact on my page performance? `; diff --git a/tests/tools/performance.test.ts b/tests/tools/performance.test.ts index ec14381e..b8ac5533 100644 --- a/tests/tools/performance.test.ts +++ b/tests/tools/performance.test.ts @@ -218,7 +218,11 @@ describe('performance', () => { it('does nothing if the trace is not running and does not error', async () => { await withBrowser(async (response, context) => { context.setIsRunningPerformanceTrace(false); + const selectedPage = context.getSelectedPage(); + const stopTracingStub = sinon.stub(selectedPage.tracing, 'stop'); await stopTrace.handler({params: {}}, response, context); + sinon.assert.notCalled(stopTracingStub); + assert.strictEqual(context.isRunningPerformanceTrace(), false); }); }); diff --git a/tests/trace-processing/parse.test.js.snapshot b/tests/trace-processing/parse.test.js.snapshot index 5ce7918f..815206d5 100644 --- a/tests/trace-processing/parse.test.js.snapshot +++ b/tests/trace-processing/parse.test.js.snapshot @@ -1,4 +1,49 @@ exports[`Trace parsing > can format results of a trace 1`] = ` +## Summary of Performance trace findings: +URL: https://web.dev/ +Bounds: {min: 122410994891, max: 122416385853} +CPU throttling: none +Network throttling: none +Metrics (lab / observed): + - LCP: 129 ms, event: (eventKey: r-6063, ts: 122411126100), nodeId: 7 + - LCP breakdown: + - TTFB: 8 ms, bounds: {min: 122410996889, max: 122411004828} + - Load delay: 33 ms, bounds: {min: 122411004828, max: 122411037986} + - Load duration: 15 ms, bounds: {min: 122411037986, max: 122411052690} + - Render delay: 73 ms, bounds: {min: 122411052690, max: 122411126100} + - CLS: 0.00 +Metrics (field / real users): n/a – no data for this page in CrUX +Available insights: + - insight name: LCPBreakdown + description: Each [subpart has specific improvement strategies](https://web.dev/articles/optimize-lcp#lcp-breakdown). Ideally, most of the LCP time should be spent on loading the resources, not within delays. + relevant trace bounds: {min: 122410996889, max: 122411126100} + example question: Help me optimize my LCP score + example question: Which LCP phase was most problematic? + example question: What can I do to reduce the LCP time for this page load? + - insight name: LCPDiscovery + description: Optimize LCP by making the LCP image [discoverable](https://web.dev/articles/optimize-lcp#1_eliminate_resource_load_delay) from the HTML immediately, and [avoiding lazy-loading](https://web.dev/articles/lcp-lazy-loading) + relevant trace bounds: {min: 122411004828, max: 122411055039} + example question: Suggest fixes to reduce my LCP + example question: What can I do to reduce my LCP discovery time? + example question: Why is LCP discovery time important? + - insight name: RenderBlocking + description: Requests are blocking the page's initial render, which may delay LCP. [Deferring or inlining](https://web.dev/learn/performance/understanding-the-critical-path#render-blocking_resources) can move these network requests out of the critical path. + relevant trace bounds: {min: 122411037528, max: 122411053852} + example question: Show me the most impactful render blocking requests that I should focus on + example question: How can I reduce the number of render blocking requests? + - insight name: DocumentLatency + description: Your first network request is the most important. Reduce its latency by avoiding redirects, ensuring a fast server response, and enabling text compression. + relevant trace bounds: {min: 122410998910, max: 122411043781} + estimated metric savings: FCP 0 ms, LCP 0 ms + estimated wasted bytes: 77.1 kB + example question: How do I decrease the initial loading time of my page? + example question: Did anything slow down the request for this document? + - insight name: ThirdParties + description: 3rd party code can significantly impact load performance. [Reduce and defer loading of 3rd party code](https://web.dev/articles/optimizing-content-efficiency-loading-third-party-javascript/) to prioritize your page's content. + relevant trace bounds: {min: 122411037881, max: 122416229595} + example question: Which third parties are having the largest impact on my page performance? + +## Details on call tree & network request formats: Information on performance traces may contain main thread activity represented as call frames and network requests. Each call frame is presented in the following format: @@ -53,48 +98,4 @@ Durations (all in milliseconds): - \`responseHeaders\`: A list (separated by '|') of values for specific, pre-defined response headers, enclosed in square brackets. The order of headers corresponds to an internal fixed list. If a header is not present, its value will be empty. - - -URL: https://web.dev/ -Bounds: {min: 122410994891, max: 122416385853} -CPU throttling: none -Network throttling: none -Metrics (lab / observed): - - LCP: 129 ms, event: (eventKey: r-6063, ts: 122411126100), nodeId: 7 - - LCP breakdown: - - TTFB: 8 ms, bounds: {min: 122410996889, max: 122411004828} - - Load delay: 33 ms, bounds: {min: 122411004828, max: 122411037986} - - Load duration: 15 ms, bounds: {min: 122411037986, max: 122411052690} - - Render delay: 73 ms, bounds: {min: 122411052690, max: 122411126100} - - CLS: 0.00 -Metrics (field / real users): n/a – no data for this page in CrUX -Available insights: - - insight name: LCPBreakdown - description: Each [subpart has specific improvement strategies](https://web.dev/articles/optimize-lcp#lcp-breakdown). Ideally, most of the LCP time should be spent on loading the resources, not within delays. - relevant trace bounds: {min: 122410996889, max: 122411126100} - example question: Help me optimize my LCP score - example question: Which LCP phase was most problematic? - example question: What can I do to reduce the LCP time for this page load? - - insight name: LCPDiscovery - description: Optimize LCP by making the LCP image [discoverable](https://web.dev/articles/optimize-lcp#1_eliminate_resource_load_delay) from the HTML immediately, and [avoiding lazy-loading](https://web.dev/articles/lcp-lazy-loading) - relevant trace bounds: {min: 122411004828, max: 122411055039} - example question: Suggest fixes to reduce my LCP - example question: What can I do to reduce my LCP discovery time? - example question: Why is LCP discovery time important? - - insight name: RenderBlocking - description: Requests are blocking the page's initial render, which may delay LCP. [Deferring or inlining](https://web.dev/learn/performance/understanding-the-critical-path#render-blocking_resources) can move these network requests out of the critical path. - relevant trace bounds: {min: 122411037528, max: 122411053852} - example question: Show me the most impactful render blocking requests that I should focus on - example question: How can I reduce the number of render blocking requests? - - insight name: DocumentLatency - description: Your first network request is the most important. Reduce its latency by avoiding redirects, ensuring a fast server response, and enabling text compression. - relevant trace bounds: {min: 122410998910, max: 122411043781} - estimated metric savings: FCP 0 ms, LCP 0 ms - estimated wasted bytes: 77.1 kB - example question: How do I decrease the initial loading time of my page? - example question: Did anything slow down the request for this document? - - insight name: ThirdParties - description: 3rd party code can significantly impact load performance. [Reduce and defer loading of 3rd party code](https://web.dev/articles/optimizing-content-efficiency-loading-third-party-javascript/) to prioritize your page's content. - relevant trace bounds: {min: 122411037881, max: 122416229595} - example question: Which third parties are having the largest impact on my page performance? `; diff --git a/tests/utils.ts b/tests/utils.ts index 82b4da4e..0197e181 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -46,6 +46,9 @@ export function getMockRequest( response?: HTTPResponse; failure?: HTTPRequest['failure']; resourceType?: string; + hasPostData?: boolean; + postData?: string; + fetchPostData?: Promise; } = {}, ): HTTPRequest { return { @@ -55,6 +58,15 @@ export function getMockRequest( method() { return options.method ?? 'GET'; }, + fetchPostData() { + return options.fetchPostData ?? Promise.reject(); + }, + hasPostData() { + return options.hasPostData ?? false; + }, + postData() { + return options.postData; + }, response() { return options.response ?? null; },