diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 26c7305e7..131985a3d 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -1,16 +1,16 @@
{
- "name": "chrome-devtools-plugins",
+ "name": "brave-devtools-plugins",
"version": "1.0.0",
- "description": "Bundled plugins for actuating and debugging the Chrome browser.",
+ "description": "Bundled plugins for actuating and debugging the Brave browser.",
"owner": {
- "name": "Chrome DevTools Team",
- "email": "devtools-dev@chromium.org"
+ "name": "381sm016",
+ "email": ""
},
"plugins": [
{
- "name": "chrome-devtools-mcp",
+ "name": "brave-devtools-mcp",
"source": "./",
- "description": "Reliable automation, in-depth debugging, and performance analysis in Chrome using Chrome DevTools and Puppeteer"
+ "description": "Reliable automation, in-depth debugging, and performance analysis in Brave using DevTools and Puppeteer"
}
]
}
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 1033f4171..3d71a88de 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,11 +1,11 @@
{
- "name": "chrome-devtools-mcp",
+ "name": "brave-devtools-mcp",
"version": "latest",
- "description": "Reliable automation, in-depth debugging, and performance analysis in Chrome using Chrome DevTools and Puppeteer",
+ "description": "Reliable automation, in-depth debugging, and performance analysis in Brave using DevTools and Puppeteer",
"mcpServers": {
- "chrome-devtools": {
+ "brave-devtools": {
"command": "npx",
- "args": ["chrome-devtools-mcp@latest"]
+ "args": ["brave-devtools-mcp@latest"]
}
}
}
diff --git a/README.md b/README.md
index 56e0d72a6..9735f8a05 100644
--- a/README.md
+++ b/README.md
@@ -1,52 +1,20 @@
-# Chrome DevTools MCP
+# Brave DevTools MCP
-[](https://npmjs.org/package/chrome-devtools-mcp)
+Modified version of `chrome-devtools-mcp` without telemetry data and adapted to work with brave. Plug and play as it is just a Chromium browser.
-`chrome-devtools-mcp` lets your coding agent (such as Gemini, Claude, Cursor or Copilot)
-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) | [Design Principles](./docs/design-principles.md)
-
-## Key features
-
-- **Get performance insights**: Uses [Chrome
- DevTools](https://github.com/ChromeDevTools/devtools-frontend) to record
- traces and extract actionable performance insights.
-- **Advanced browser debugging**: Analyze network requests, take screenshots and
- check browser console messages (with source-mapped stack traces).
-- **Reliable automation**. Uses
- [puppeteer](https://github.com/puppeteer/puppeteer) to automate actions in
- Chrome and automatically wait for action results.
+Fork of [chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp), adapted for Brave Browser.
## Disclaimers
-`chrome-devtools-mcp` exposes content of the browser instance to the MCP clients
+`brave-devtools-mcp` exposes content of the browser instance to the MCP clients
allowing them to inspect, debug, and modify any data in the browser or DevTools.
Avoid sharing sensitive or personal information that you don't want to share with
MCP clients.
-## **Usage statistics**
-
-Google collects usage statistics (such as tool invocation success rates, latency, and environment information) to improve the reliability and performance of Chrome DevTools MCP.
-
-Data collection is **enabled by default**. You can opt-out by passing the `--no-usage-statistics` flag when starting the server:
-
-```json
-"args": ["-y", "chrome-devtools-mcp@latest", "--no-usage-statistics"]
-```
-
-Google handles this data in accordance with the [Google Privacy Policy](https://policies.google.com/privacy).
-
-Google's collection of usage statistics for Chrome DevTools MCP is independent from the Chrome browser's usage statistics. Opting out of Chrome metrics does not automatically opt you out of this tool, and vice-versa.
-
-Collection is disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.
-
## Requirements
- [Node.js](https://nodejs.org/) v20.19 or a newer [latest maintenance LTS](https://github.com/nodejs/Release#release-schedule) version.
-- [Chrome](https://www.google.com/chrome/) current stable version or newer.
+- [Brave Browser](https://brave.com/) current stable version or newer.
- [npm](https://www.npmjs.com/).
## Getting started
@@ -56,290 +24,113 @@ Add the following config to your MCP client:
```json
{
"mcpServers": {
- "chrome-devtools": {
+ "brave-devtools": {
"command": "npx",
- "args": ["-y", "chrome-devtools-mcp@latest"]
+ "args": ["-y", "brave-devtools-mcp@latest"]
}
}
}
```
-> [!NOTE]
-> Using `chrome-devtools-mcp@latest` ensures that your MCP client will always use the latest version of the Chrome DevTools MCP server.
+> [!NOTE]
+> Using `brave-devtools-mcp@latest` ensures that your MCP client will always use the latest version of the Brave DevTools MCP server.
### 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
-```
-
-
-
-
- Antigravity
-
-To use the Chrome DevTools MCP server follow the instructions from Antigravity's docs to install a custom MCP server. Add the following config to the MCP servers config:
-
-```bash
-{
- "mcpServers": {
- "chrome-devtools": {
- "command": "npx",
- "args": [
- "chrome-devtools-mcp@latest",
- "--browser-url=http://127.0.0.1:9222",
- "-y"
- ]
- }
- }
-}
-```
-
-This will make the Chrome DevTools MCP server automatically connect to the browser that Antigravity is using. If you are not using port 9222, make sure to adjust accordingly.
-
-Chrome DevTools MCP will not start the browser instance automatically using this approach as as the Chrome DevTools MCP server runs in Antigravity's built-in browser. If the browser is not already running, you have to start it first by clicking the Chrome icon at the top right corner.
-
-
-
Claude Code
- Use the Claude Code CLI to add the Chrome DevTools MCP server (guide):
-
-```bash
-claude mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest
-```
-
-
-
-
- Cline
- Follow https://docs.cline.bot/mcp/configuring-mcp-servers and use the config provided above.
-
-
-
- Codex
- Follow the configure MCP guide
- using the standard config from above. You can also install the Chrome DevTools MCP server using the Codex CLI:
+ Use the Claude Code CLI to add the Brave DevTools MCP server (guide):
```bash
-codex mcp add chrome-devtools -- npx chrome-devtools-mcp@latest
-```
-
-**On Windows 11**
-
-Configure the Chrome install location and increase the startup timeout by updating `.codex/config.toml` and adding the following `env` and `startup_timeout_ms` parameters:
-
-```
-[mcp_servers.chrome-devtools]
-command = "cmd"
-args = [
- "/c",
- "npx",
- "-y",
- "chrome-devtools-mcp@latest",
-]
-env = { SystemRoot="C:\\Windows", PROGRAMFILES="C:\\Program Files" }
-startup_timeout_ms = 20_000
+claude mcp add brave-devtools --scope user npx brave-devtools-mcp@latest
```
- 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 `CTRL+S` to save the configuration:
+ Cursor
-- **Server name:** `chrome-devtools`
-- **Server Type:** `[1] Local`
-- **Command:** `npx -y chrome-devtools-mcp@latest`
+Go to `Cursor Settings` -> `MCP` -> `New MCP Server`. Use the config provided above.
Copilot / VS Code
-**Click the button to install:**
-
-[
](https://vscode.dev/redirect/mcp/install?name=io.github.ChromeDevTools%2Fchrome-devtools-mcp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22chrome-devtools-mcp%22%5D%2C%22env%22%3A%7B%7D%7D)
-
-[
](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522io.github.ChromeDevTools%252Fchrome-devtools-mcp%2522%252C%2522config%2522%253A%257B%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522chrome-devtools-mcp%2522%255D%252C%2522env%2522%253A%257B%257D%257D%257D)
-
-**Or install manually:**
-
Follow the MCP install guide,
-with the standard config from above. You can also install the Chrome DevTools MCP server using the VS Code CLI:
-
-```bash
-code --add-mcp '{"name":"io.github.ChromeDevTools/chrome-devtools-mcp","command":"npx","args":["-y","chrome-devtools-mcp"],"env":{}}'
-```
+with the standard config from above.
- Cursor
-
-**Click the button to install:**
-
-[
](https://cursor.com/en/install-mcp?name=chrome-devtools&config=eyJjb21tYW5kIjoibnB4IC15IGNocm9tZS1kZXZ0b29scy1tY3BAbGF0ZXN0In0%3D)
-
-**Or install manually:**
-
-Go to `Cursor Settings` -> `MCP` -> `New MCP Server`. Use the config provided above.
-
+ Windsurf
+ Follow the configure MCP guide
+ using the standard config from above.
- Factory CLI
-Use the Factory CLI to add the Chrome DevTools MCP server (guide):
-
-```bash
-droid mcp add chrome-devtools "npx -y chrome-devtools-mcp@latest"
-```
-
+ Cline
+ Follow https://docs.cline.bot/mcp/configuring-mcp-servers and use the config provided above.
Gemini CLI
-Install the Chrome DevTools MCP server using the Gemini CLI.
+Install the Brave DevTools MCP server using the Gemini CLI.
**Project wide:**
```bash
-# Either MCP only:
-gemini mcp add chrome-devtools npx chrome-devtools-mcp@latest
-# Or as a Gemini extension (MCP+Skills):
-gemini extensions install --auto-update https://github.com/ChromeDevTools/chrome-devtools-mcp
+gemini mcp add brave-devtools npx brave-devtools-mcp@latest
```
**Globally:**
```bash
-gemini mcp add -s user chrome-devtools npx chrome-devtools-mcp@latest
+gemini mcp add -s user brave-devtools npx brave-devtools-mcp@latest
```
-Alternatively, follow the MCP guide and use the standard config from above.
-
- Gemini Code Assist
- Follow the configure MCP guide
- using the standard config from above.
-
-
-
- JetBrains AI Assistant & Junie
-
-Go to `Settings | Tools | AI Assistant | Model Context Protocol (MCP)` -> `Add`. Use the config provided above.
-The same way chrome-devtools-mcp can be configured for JetBrains Junie in `Settings | Tools | Junie | MCP Settings` -> `Add`. Use the config provided above.
-
-
-
-
- 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.
-
-
-
-
- OpenCode
-
-Add the following configuration to your `opencode.json` file. If you don't have one, create it at `~/.config/opencode/opencode.json` (guide):
+ Codex
+ Follow the configure MCP guide
+ using the standard config from above. You can also install using the Codex CLI:
-```json
-{
- "$schema": "https://opencode.ai/config.json",
- "mcp": {
- "chrome-devtools": {
- "type": "local",
- "command": ["npx", "-y", "chrome-devtools-mcp@latest"]
- }
- }
-}
+```bash
+codex mcp add brave-devtools -- npx brave-devtools-mcp@latest
```
-
-
-
- Qoder
-
-In **Qoder Settings**, go to `MCP Server` > `+ Add` > Use the configuration snippet provided above.
-
-Alternatively, follow the MCP guide and use the standard config from above.
-
-
-
-
- Qoder CLI
+**On Windows 11**
-Install the Chrome DevTools MCP server using the Qoder CLI (guide):
+Configure the Brave install location and increase the startup timeout by updating `.codex/config.toml` and adding the following `env` and `startup_timeout_ms` parameters:
-**Project wide:**
-
-```bash
-qodercli mcp add chrome-devtools -- npx chrome-devtools-mcp@latest
```
-
-**Globally:**
-
-```bash
-qodercli mcp add -s user chrome-devtools -- npx chrome-devtools-mcp@latest
+[mcp_servers.brave-devtools]
+command = "cmd"
+args = [
+ "/c",
+ "npx",
+ "-y",
+ "brave-devtools-mcp@latest",
+]
+env = { SystemRoot="C:\\Windows", PROGRAMFILES="C:\\Program Files" }
+startup_timeout_ms = 20_000
```
-
- 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)
-
-
-
- Warp
-
-Go to `Settings | AI | Manage MCP Servers` -> `+ Add` to [add an MCP Server](https://docs.warp.dev/knowledge-and-collaboration/mcp#adding-an-mcp-server). Use the config provided above.
-
-
-
-
- Windsurf
- Follow the configure MCP guide
- using the standard config from above.
-
-
### Your first prompt
Enter the following prompt in your MCP Client to check if everything is working:
```
-Check the performance of https://developers.chrome.com
+Check the performance of https://search.brave.com
```
Your MCP client should open the browser and record a performance trace.
-> [!NOTE]
-> The MCP server will start the browser automatically once the MCP client uses a tool that requires a running browser instance. Connecting to the Chrome DevTools MCP server on its own will not automatically start the browser.
+> [!NOTE]
+> The MCP server will start the browser automatically once the MCP client uses a tool that requires a running browser instance. Connecting to the Brave DevTools MCP server on its own will not automatically start the browser.
## Tools
@@ -384,21 +175,21 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
## Configuration
-The Chrome DevTools MCP server supports the following configuration option:
+The Brave DevTools MCP server supports the following configuration options:
- **`--autoConnect`/ `--auto-connect`**
- If specified, automatically connects to a browser (Chrome 144+) running in the user data directory identified by the channel param. Requires the remoted debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.
+ If specified, automatically connects to a running Brave instance using the user data directory identified by the channel param. Requires the remote debugging server to be started in the Brave instance via brave://inspect/#remote-debugging.
- **Type:** boolean
- **Default:** `false`
- **`--browserUrl`/ `--browser-url`, `-u`**
- Connect to a running, debuggable Chrome instance (e.g. `http://127.0.0.1:9222`). For more details see: https://github.com/ChromeDevTools/chrome-devtools-mcp#connecting-to-a-running-chrome-instance.
+ Connect to a running, debuggable Brave instance (e.g. `http://127.0.0.1:9222`).
- **Type:** string
- **`--wsEndpoint`/ `--ws-endpoint`, `-w`**
- WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/). Alternative to --browserUrl.
+ WebSocket endpoint to connect to a running Brave instance (e.g., ws://127.0.0.1:9222/devtools/browser/). Alternative to --browserUrl.
- **Type:** string
- **`--wsHeaders`/ `--ws-headers`**
@@ -411,7 +202,7 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Default:** `false`
- **`--executablePath`/ `--executable-path`, `-e`**
- Path to custom Chrome executable.
+ Path to custom Brave executable.
- **Type:** string
- **`--isolated`**
@@ -419,36 +210,36 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Type:** boolean
- **`--userDataDir`/ `--user-data-dir`**
- Path to the user data directory for Chrome. Default is $HOME/.cache/chrome-devtools-mcp/chrome-profile$CHANNEL_SUFFIX_IF_NON_STABLE
+ Path to the user data directory for Brave. Default is $HOME/.cache/brave-devtools-mcp/brave-profile$CHANNEL_SUFFIX_IF_NON_STABLE
- **Type:** string
- **`--channel`**
- Specify a different Chrome channel that should be used. The default is the stable channel version.
+ Specify a different Brave channel that should be used. The default is the stable channel version.
- **Type:** string
- - **Choices:** `stable`, `canary`, `beta`, `dev`
+ - **Choices:** `stable`, `beta`, `dev`, `nightly`
- **`--logFile`/ `--log-file`**
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 Chrome instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.
+ Initial viewport size for the Brave instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.
- **Type:** string
- **`--proxyServer`/ `--proxy-server`**
- 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.
+ Proxy server configuration for Brave passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details.
- **Type:** string
- **`--acceptInsecureCerts`/ `--accept-insecure-certs`**
If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
- **Type:** boolean
-- **`--chromeArg`/ `--chrome-arg`**
- Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
+- **`--braveArg`/ `--brave-arg`**
+ Additional arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.
- **Type:** array
-- **`--ignoreDefaultChromeArg`/ `--ignore-default-chrome-arg`**
- Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
+- **`--ignoreDefaultBraveArg`/ `--ignore-default-brave-arg`**
+ Explicitly disable default arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.
- **Type:** array
- **`--categoryEmulation`/ `--category-emulation`**
@@ -466,11 +257,6 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Type:** boolean
- **Default:** `true`
-- **`--usageStatistics`/ `--usage-statistics`**
- Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.
- - **Type:** boolean
- - **Default:** `true`
-
Pass them via the `args` property in the JSON configuration. For example:
@@ -478,11 +264,11 @@ Pass them via the `args` property in the JSON configuration. For example:
```json
{
"mcpServers": {
- "chrome-devtools": {
+ "brave-devtools": {
"command": "npx",
"args": [
- "chrome-devtools-mcp@latest",
- "--channel=canary",
+ "brave-devtools-mcp@latest",
+ "--channel=nightly",
"--headless=true",
"--isolated=true"
]
@@ -493,15 +279,15 @@ Pass them via the `args` property in the JSON configuration. For example:
### Connecting via WebSocket with custom headers
-You can connect directly to a Chrome WebSocket endpoint and include custom headers (e.g., for authentication):
+You can connect directly to a Brave WebSocket endpoint and include custom headers (e.g., for authentication):
```json
{
"mcpServers": {
- "chrome-devtools": {
+ "brave-devtools": {
"command": "npx",
"args": [
- "chrome-devtools-mcp@latest",
+ "brave-devtools-mcp@latest",
"--wsEndpoint=ws://127.0.0.1:9222/devtools/browser/",
"--wsHeaders={\"Authorization\":\"Bearer YOUR_TOKEN\"}"
]
@@ -510,103 +296,95 @@ You can connect directly to a Chrome WebSocket endpoint and include custom heade
}
```
-To get the WebSocket endpoint from a running Chrome instance, visit `http://127.0.0.1:9222/json/version` and look for the `webSocketDebuggerUrl` field.
+To get the WebSocket endpoint from a running Brave instance, visit `http://127.0.0.1:9222/json/version` and look for the `webSocketDebuggerUrl` field.
-You can also run `npx chrome-devtools-mcp@latest --help` to see all available configuration options.
+You can also run `npx brave-devtools-mcp@latest --help` to see all available configuration options.
## Concepts
### User data directory
-`chrome-devtools-mcp` starts a Chrome's stable channel instance using the following user
+`brave-devtools-mcp` starts a Brave stable channel instance using the following user
data directory:
-- Linux / macOS: `$HOME/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL`
-- Windows: `%HOMEPATH%/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL`
+- Linux / macOS: `$HOME/.cache/brave-devtools-mcp/brave-profile-$CHANNEL`
+- Windows: `%HOMEPATH%/.cache/brave-devtools-mcp/brave-profile-$CHANNEL`
The user data directory is not cleared between runs and shared across
-all instances of `chrome-devtools-mcp`. Set the `isolated` option to `true`
+all instances of `brave-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
+### Connecting to a running Brave instance
-By default, the Chrome DevTools MCP server will start a new Chrome instance with a dedicated profile. This might not be ideal in all situations:
+By default, the Brave DevTools MCP server will start a new Brave instance with a dedicated profile. This might not be ideal in all situations:
- If you would like to maintain the same application state when alternating between manual site testing and agent-driven testing.
-- When the MCP needs to sign into a website. Some accounts may prevent sign-in when the browser is controlled via WebDriver (the default launch mechanism for the Chrome DevTools MCP server).
-- If you're running your LLM inside a sandboxed environment, but you would like to connect to a Chrome instance that runs outside the sandbox.
+- When the MCP needs to sign into a website. Some accounts may prevent sign-in when the browser is controlled via WebDriver (the default launch mechanism for the Brave DevTools MCP server).
+- If you're running your LLM inside a sandboxed environment, but you would like to connect to a Brave instance that runs outside the sandbox.
-In these cases, start Chrome first and let the Chrome DevTools MCP server connect to it. There are two ways to do so:
+In these cases, start Brave first and let the Brave DevTools MCP server connect to it. There are two ways to do so:
-- **Automatic connection (available in Chrome 144)**: best for sharing state between manual and agent-driven testing.
+- **Automatic connection**: best for sharing state between manual and agent-driven testing.
- **Manual connection via remote debugging port**: best when running inside a sandboxed environment.
-#### Automatically connecting to a running Chrome instance
+#### Automatically connecting to a running Brave instance
-**Step 1:** Set up remote debugging in Chrome
+**Step 1:** Set up remote debugging in Brave
-In Chrome (\>= M144), do the following to set up remote debugging:
+In Brave, do the following to set up remote debugging:
-1. Navigate to `chrome://inspect/#remote-debugging` to enable remote debugging.
+1. Navigate to `brave://inspect/#remote-debugging` to enable remote debugging.
2. Follow the dialog UI to allow or disallow incoming debugging connections.
-**Step 2:** Configure Chrome DevTools MCP server to automatically connect to a running Chrome Instance
+**Step 2:** Configure Brave DevTools MCP server to automatically connect to a running Brave instance
-To connect the `chrome-devtools-mcp` server to the running Chrome instance, use
+To connect the `brave-devtools-mcp` server to the running Brave instance, use
`--autoConnect` command line argument for the MCP server.
-The following code snippet is an example configuration for gemini-cli:
+The following code snippet is an example configuration:
```json
{
"mcpServers": {
- "chrome-devtools": {
+ "brave-devtools": {
"command": "npx",
- "args": ["chrome-devtools-mcp@latest", "--autoConnect", "--channel=beta"]
+ "args": ["brave-devtools-mcp@latest", "--autoConnect", "--channel=stable"]
}
}
}
```
-Note: you have to specify `--channel=beta` until Chrome M144 has reached the
-stable channel.
-
**Step 3:** Test your setup
-Make sure your browser is running. Open gemini-cli and run the following prompt:
+Make sure your browser is running. Open your MCP client and run the following prompt:
```none
-Check the performance of https://developers.chrome.com
+Check the performance of https://search.brave.com
```
-> [!NOTE]
-> The autoConnect option requires the user to start Chrome. If the user has multiple active profiles, the MCP server will connect to the default profile (as determined by Chrome). The MCP server has access to all open windows for the selected profile.
+> [!NOTE]
+> The autoConnect option requires the user to start Brave. If the user has multiple active profiles, the MCP server will connect to the default profile (as determined by Brave). The MCP server has access to all open windows for the selected profile.
-The Chrome DevTools MCP server will try to connect to your running Chrome
-instance. It shows a dialog asking for user permission.
-
-Clicking **Allow** results in the Chrome DevTools MCP server opening
-[developers.chrome.com](http://developers.chrome.com) and taking a performance
-trace.
+The Brave DevTools MCP server will try to connect to your running Brave instance.
#### Manual connection using port forwarding
-You can connect to a running Chrome instance by using the `--browser-url` option. This is useful if you are running the MCP server in a sandboxed environment that does not allow starting a new Chrome instance.
+You can connect to a running Brave instance by using the `--browser-url` option. This is useful if you are running the MCP server in a sandboxed environment that does not allow starting a new Brave instance.
-Here is a step-by-step guide on how to connect to a running Chrome instance:
+Here is a step-by-step guide on how to connect to a running Brave 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.
+Add the `--browser-url` option to your MCP client configuration. The value of this option should be the URL of the running Brave instance. `http://127.0.0.1:9222` is a common default.
```json
{
"mcpServers": {
- "chrome-devtools": {
+ "brave-devtools": {
"command": "npx",
"args": [
- "chrome-devtools-mcp@latest",
+ "brave-devtools-mcp@latest",
"--browser-url=http://127.0.0.1:9222"
]
}
@@ -614,58 +392,50 @@ Add the `--browser-url` option to your MCP client configuration. The value of th
}
```
-**Step 2: Start the Chrome browser**
+**Step 2: Start the Brave browser**
-> [!WARNING]
+> [!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.
+Start the Brave browser with the remote debugging port enabled. Make sure to close any running Brave 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.
**macOS**
```bash
-/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile-stable
+/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser --remote-debugging-port=9222 --user-data-dir=/tmp/brave-profile-stable
```
**Linux**
```bash
-/usr/bin/google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile-stable
+/usr/bin/brave-browser --remote-debugging-port=9222 --user-data-dir=/tmp/brave-profile-stable
```
**Windows**
```bash
-"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-profile-stable"
+"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe" --remote-debugging-port=9222 --user-data-dir="%TEMP%\brave-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:
+After configuring the MCP client and starting the Brave browser, you can test your setup by running a simple prompt in your MCP client:
```
-Check the performance of https://developers.chrome.com
+Check the performance of https://search.brave.com
```
-Your MCP client should connect to the running Chrome instance and receive a performance report.
-
-If you hit VM-to-host port forwarding issues, see the “Remote debugging between virtual machine (VM) and host fails” section in [`docs/troubleshooting.md`](./docs/troubleshooting.md#remote-debugging-between-virtual-machine-vm-and-host-fails).
-
-For more details on remote debugging, see the [Chrome DevTools documentation](https://developer.chrome.com/docs/devtools/remote-debugging/).
-
-### Debugging Chrome on Android
+Your MCP client should connect to the running Brave instance and receive a performance report.
-Please consult [these instructions](./docs/debugging-android.md).
+If you hit VM-to-host port forwarding issues, see the "Remote debugging between virtual machine (VM) and host fails" section in [`docs/troubleshooting.md`](./docs/troubleshooting.md#remote-debugging-between-virtual-machine-vm-and-host-fails).
## Known limitations
### Operating system sandboxes
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
-`--browser-url` to connect to a Chrome instance that you start manually outside
+containers. If sandboxes are enabled, `brave-devtools-mcp` is not able to start
+Brave that requires permissions to create its own sandboxes. As a workaround,
+either disable sandboxing for `brave-devtools-mcp` in your MCP client or use
+`--browser-url` to connect to a Brave instance that you start manually outside
of the MCP client sandbox.
diff --git a/gemini-extension.json b/gemini-extension.json
index 10c45cb9d..b7838d2e2 100644
--- a/gemini-extension.json
+++ b/gemini-extension.json
@@ -1,10 +1,10 @@
{
- "name": "chrome-devtools-mcp",
+ "name": "brave-devtools-mcp",
"version": "latest",
"mcpServers": {
- "chrome-devtools": {
+ "brave-devtools": {
"command": "npx",
- "args": ["chrome-devtools-mcp@latest"]
+ "args": ["brave-devtools-mcp@latest"]
}
}
}
diff --git a/package-lock.json b/package-lock.json
index 73b209cef..d23d1b756 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,15 @@
{
- "name": "chrome-devtools-mcp",
+ "name": "brave-devtools-mcp",
"version": "0.15.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "chrome-devtools-mcp",
+ "name": "brave-devtools-mcp",
"version": "0.15.1",
"license": "Apache-2.0",
"bin": {
- "chrome-devtools-mcp": "build/src/index.js"
+ "brave-devtools-mcp": "build/src/index.js"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
diff --git a/package.json b/package.json
index 85bc39dc8..cc1c1c280 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "chrome-devtools-mcp",
+ "name": "brave-devtools-mcp",
"version": "0.15.1",
- "description": "MCP server for Chrome DevTools",
+ "description": "MCP server for Brave DevTools",
"type": "module",
"bin": "./build/src/index.js",
"main": "index.js",
@@ -22,7 +22,7 @@
"test:update-snapshots": "npm run build && node scripts/test.mjs --test-update-snapshots",
"prepare": "node --experimental-strip-types scripts/prepare.ts",
"verify-server-json-version": "node --experimental-strip-types scripts/verify-server-json-version.ts",
- "eval": "npm run build && CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS=true node --experimental-strip-types scripts/eval_gemini.ts",
+ "eval": "npm run build && node --experimental-strip-types scripts/eval_gemini.ts",
"count-tokens": "node --experimental-strip-types scripts/count_tokens.ts"
},
"files": [
@@ -31,14 +31,14 @@
"LICENSE",
"!*.tsbuildinfo"
],
- "repository": "ChromeDevTools/chrome-devtools-mcp",
- "author": "Google LLC",
+ "repository": "381sm016/brave-devtools-mcp",
+ "author": "381sm016",
"license": "Apache-2.0",
"bugs": {
- "url": "https://github.com/ChromeDevTools/chrome-devtools-mcp/issues"
+ "url": "https://github.com/381sm016/brave-devtools-mcp/issues"
},
- "homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp#readme",
- "mcpName": "io.github.ChromeDevTools/chrome-devtools-mcp",
+ "homepage": "https://github.com/381sm016/brave-devtools-mcp#readme",
+ "mcpName": "io.github.381sm016/brave-devtools-mcp",
"devDependencies": {
"@eslint/js": "^9.35.0",
"@google/genai": "^1.37.0",
diff --git a/puppeteer.config.cjs b/puppeteer.config.cjs
index d162fc0fd..b17c4fa1b 100644
--- a/puppeteer.config.cjs
+++ b/puppeteer.config.cjs
@@ -9,7 +9,7 @@
*/
module.exports = {
chrome: {
- skipDownload: false,
+ skipDownload: true,
},
['chrome-headless-shell']: {
skipDownload: true,
diff --git a/scripts/test.mjs b/scripts/test.mjs
index 80ccd7dbc..114eb71ec 100644
--- a/scripts/test.mjs
+++ b/scripts/test.mjs
@@ -67,7 +67,6 @@ async function runTests(attempt) {
stdio: 'inherit',
env: {
...process.env,
- CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: true,
},
});
diff --git a/server.json b/server.json
index 74757bec8..56cf3c234 100644
--- a/server.json
+++ b/server.json
@@ -1,10 +1,10 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
- "name": "io.github.ChromeDevTools/chrome-devtools-mcp",
- "title": "Chrome DevTools MCP",
- "description": "MCP server for Chrome DevTools",
+ "name": "io.github.381sm016/brave-devtools-mcp",
+ "title": "Brave DevTools MCP",
+ "description": "MCP server for Brave DevTools",
"repository": {
- "url": "https://github.com/ChromeDevTools/chrome-devtools-mcp",
+ "url": "https://github.com/381sm016/brave-devtools-mcp",
"source": "github"
},
"version": "0.15.1",
@@ -12,7 +12,7 @@
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
- "identifier": "chrome-devtools-mcp",
+ "identifier": "brave-devtools-mcp",
"version": "0.15.1",
"transport": {
"type": "stdio"
diff --git a/skills/chrome-devtools/SKILL.md b/skills/brave-devtools/SKILL.md
similarity index 74%
rename from skills/chrome-devtools/SKILL.md
rename to skills/brave-devtools/SKILL.md
index 551c03be8..07d9042d6 100644
--- a/skills/chrome-devtools/SKILL.md
+++ b/skills/brave-devtools/SKILL.md
@@ -1,11 +1,11 @@
---
-name: chrome-devtools
-description: Uses Chrome DevTools via MCP for efficient debugging, troubleshooting and browser automation. Use when debugging web pages, automating browser interactions, analyzing performance, or inspecting network requests.
+name: brave-devtools
+description: Uses Brave DevTools via MCP for efficient debugging, troubleshooting and browser automation. Use when debugging web pages, automating browser interactions, analyzing performance, or inspecting network requests.
---
## Core Concepts
-**Browser lifecycle**: Browser starts automatically on first tool call using a persistent Chrome profile. Configure via CLI args in the MCP server configuration: `npx chrome-devtools-mcp@latest --help`.
+**Browser lifecycle**: Browser starts automatically on first tool call using a persistent Brave profile. Configure via CLI args in the MCP server configuration: `npx brave-devtools-mcp@latest --help`.
**Page selection**: Tools operate on the currently selected page. Use `list_pages` to see available pages, then `select_page` to switch context.
@@ -38,7 +38,6 @@ You can send multiple tool calls in parallel, but maintain correct order: naviga
## Troubleshooting
-If `chrome-devtools-mcp` is insufficient, guide users to use Chrome DevTools UI:
+If `brave-devtools-mcp` is insufficient, guide users to use Brave DevTools UI:
- https://developer.chrome.com/docs/devtools
-- https://developer.chrome.com/docs/devtools/ai-assistance
diff --git a/src/McpContext.ts b/src/McpContext.ts
index fa4bb9e83..3a8ca698e 100644
--- a/src/McpContext.ts
+++ b/src/McpContext.ts
@@ -675,7 +675,7 @@ export class McpContext implements Context {
): Promise<{filename: string}> {
try {
const dir = await fs.mkdtemp(
- path.join(os.tmpdir(), 'chrome-devtools-mcp-'),
+ path.join(os.tmpdir(), 'brave-devtools-mcp-'),
);
const filename = path.join(
diff --git a/src/browser.ts b/src/browser.ts
index 64db15681..6e2b1b31f 100644
--- a/src/browser.ts
+++ b/src/browser.ts
@@ -11,7 +11,6 @@ import path from 'node:path';
import {logger} from './logger.js';
import type {
Browser,
- ChromeReleaseChannel,
LaunchOptions,
Target,
} from './third_party/index.js';
@@ -24,18 +23,20 @@ function makeTargetFilter() {
'chrome://',
'chrome-extension://',
'chrome-untrusted://',
+ 'brave://',
]);
return function targetFilter(target: Target): boolean {
- if (target.url() === 'chrome://newtab/') {
+ const url = target.url();
+ if (url === 'chrome://newtab/' || url === 'brave://newtab/') {
return true;
}
// Could be the only page opened in the browser.
- if (target.url().startsWith('chrome://inspect')) {
+ if (url.startsWith('chrome://inspect') || url.startsWith('brave://inspect')) {
return true;
}
for (const prefix of ignoredPrefixes) {
- if (target.url().startsWith(prefix)) {
+ if (url.startsWith(prefix)) {
return false;
}
}
@@ -43,6 +44,95 @@ function makeTargetFilter() {
};
}
+export type Channel = 'stable' | 'beta' | 'dev' | 'nightly';
+
+export function resolveBraveExecutablePath(channel: Channel): string {
+ const platform = os.platform();
+
+ if (platform === 'win32') {
+ const channelSuffix =
+ channel === 'stable' ? '' :
+ channel === 'beta' ? '-Beta' :
+ channel === 'dev' ? '-Dev' :
+ '-Nightly';
+ const folderName = `Brave-Browser${channelSuffix}`;
+ const programFiles = process.env['PROGRAMFILES'] ?? 'C:\\Program Files';
+ const localAppData = process.env['LOCALAPPDATA'] ?? '';
+
+ const candidates = [
+ path.join(programFiles, 'BraveSoftware', folderName, 'Application', 'brave.exe'),
+ path.join(localAppData, 'BraveSoftware', folderName, 'Application', 'brave.exe'),
+ ];
+ for (const candidate of candidates) {
+ if (fs.existsSync(candidate)) {
+ return candidate;
+ }
+ }
+ throw new Error(
+ `Could not find Brave (${channel}) executable. Checked:\n${candidates.join('\n')}`,
+ );
+ }
+
+ if (platform === 'darwin') {
+ const channelSuffix =
+ channel === 'stable' ? '' :
+ channel === 'beta' ? ' Beta' :
+ channel === 'dev' ? ' Dev' :
+ ' Nightly';
+ const appName = `Brave Browser${channelSuffix}`;
+ const candidate = `/Applications/${appName}.app/Contents/MacOS/${appName}`;
+ if (fs.existsSync(candidate)) {
+ return candidate;
+ }
+ throw new Error(
+ `Could not find Brave (${channel}) executable at ${candidate}`,
+ );
+ }
+
+ // Linux
+ const channelSuffix =
+ channel === 'stable' ? '' :
+ channel === 'beta' ? '-beta' :
+ channel === 'dev' ? '-dev' :
+ '-nightly';
+ const binaryName = `brave-browser${channelSuffix}`;
+ const candidates = [
+ `/usr/bin/${binaryName}`,
+ `/usr/local/bin/${binaryName}`,
+ `/snap/bin/${binaryName}`,
+ ];
+ for (const candidate of candidates) {
+ if (fs.existsSync(candidate)) {
+ return candidate;
+ }
+ }
+ throw new Error(
+ `Could not find Brave (${channel}) executable. Checked:\n${candidates.join('\n')}`,
+ );
+}
+
+export function resolveBraveUserDataDir(channel: Channel): string {
+ const platform = os.platform();
+
+ const channelFolder =
+ channel === 'stable' ? 'Brave-Browser' :
+ channel === 'beta' ? 'Brave-Browser-Beta' :
+ channel === 'dev' ? 'Brave-Browser-Dev' :
+ 'Brave-Browser-Nightly';
+
+ if (platform === 'win32') {
+ const localAppData = process.env['LOCALAPPDATA'] ?? path.join(os.homedir(), 'AppData', 'Local');
+ return path.join(localAppData, 'BraveSoftware', channelFolder, 'User Data');
+ }
+
+ if (platform === 'darwin') {
+ return path.join(os.homedir(), 'Library', 'Application Support', 'BraveSoftware', channelFolder);
+ }
+
+ // Linux
+ return path.join(os.homedir(), '.config', 'BraveSoftware', channelFolder);
+}
+
export async function ensureBrowserConnected(options: {
browserURL?: string;
wsEndpoint?: string;
@@ -70,9 +160,8 @@ export async function ensureBrowserConnected(options: {
} else if (options.browserURL) {
connectOptions.browserURL = options.browserURL;
} else if (channel || options.userDataDir) {
- const userDataDir = options.userDataDir;
+ const userDataDir = options.userDataDir ?? (channel ? resolveBraveUserDataDir(channel) : undefined);
if (userDataDir) {
- // TODO: re-expose this logic via Puppeteer.
const portPath = path.join(userDataDir, 'DevToolsActivePort');
try {
const fileContent = await fs.promises.readFile(portPath, 'utf8');
@@ -95,19 +184,14 @@ export async function ensureBrowserConnected(options: {
connectOptions.browserWSEndpoint = browserWSEndpoint;
} catch (error) {
throw new Error(
- `Could not connect to Chrome in ${userDataDir}. Check if Chrome is running and remote debugging is enabled.`,
+ `Could not connect to Brave in ${userDataDir}. Check if Brave is running and remote debugging is enabled.`,
{
cause: error,
},
);
}
} else {
- if (!channel) {
- throw new Error('Channel must be provided if userDataDir is missing');
- }
- connectOptions.channel = (
- channel === 'stable' ? 'chrome' : `chrome-${channel}`
- ) as ChromeReleaseChannel;
+ throw new Error('Channel must be provided if userDataDir is missing');
}
} else {
throw new Error(
@@ -120,7 +204,7 @@ export async function ensureBrowserConnected(options: {
browser = await puppeteer.connect(connectOptions);
} catch (err) {
throw new Error(
- 'Could not connect to Chrome. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.',
+ 'Could not connect to Brave. Check if Brave is running and remote debugging is enabled by going to brave://inspect/#remote-debugging.',
{
cause: err,
},
@@ -142,25 +226,27 @@ interface McpLaunchOptions {
width: number;
height: number;
};
- chromeArgs?: string[];
- ignoreDefaultChromeArgs?: string[];
+ braveArgs?: string[];
+ ignoreDefaultBraveArgs?: string[];
devtools: boolean;
enableExtensions?: boolean;
}
export async function launch(options: McpLaunchOptions): Promise {
- const {channel, executablePath, headless, isolated} = options;
+ const {channel, headless, isolated} = options;
const profileDirName =
channel && channel !== 'stable'
- ? `chrome-profile-${channel}`
- : 'chrome-profile';
+ ? `brave-profile-${channel}`
+ : 'brave-profile';
+
+ const executablePath = options.executablePath ?? resolveBraveExecutablePath(channel ?? 'stable');
let userDataDir = options.userDataDir;
if (!isolated && !userDataDir) {
userDataDir = path.join(
os.homedir(),
'.cache',
- 'chrome-devtools-mcp',
+ 'brave-devtools-mcp',
profileDirName,
);
await fs.promises.mkdir(userDataDir, {
@@ -169,31 +255,23 @@ export async function launch(options: McpLaunchOptions): Promise {
}
const args: LaunchOptions['args'] = [
- ...(options.chromeArgs ?? []),
+ ...(options.braveArgs ?? []),
'--hide-crash-restore-bubble',
];
const ignoreDefaultArgs: LaunchOptions['ignoreDefaultArgs'] =
- options.ignoreDefaultChromeArgs ?? false;
+ options.ignoreDefaultBraveArgs ?? false;
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'
- ? (`chrome-${channel}` as ChromeReleaseChannel)
- : 'chrome';
- }
try {
const browser = await puppeteer.launch({
- channel: puppeteerChannel,
- targetFilter: makeTargetFilter(),
executablePath,
+ targetFilter: makeTargetFilter(),
defaultViewport: null,
userDataDir,
pipe: true,
@@ -205,8 +283,6 @@ export async function launch(options: McpLaunchOptions): Promise {
enableExtensions: options.enableExtensions,
});
if (options.logFile) {
- // FIXME: we are probably subscribing too late to catch startup logs. We
- // should expose the process earlier or expose the getRecentLogs() getter.
browser.process()?.stderr?.pipe(options.logFile);
browser.process()?.stdout?.pipe(options.logFile);
}
@@ -243,5 +319,3 @@ export async function ensureBrowserLaunched(
browser = await launch(options);
return browser;
}
-
-export type Channel = 'stable' | 'canary' | 'beta' | 'dev';
diff --git a/src/cli.ts b/src/cli.ts
index c06b9063d..235fd50dd 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -11,7 +11,7 @@ export const cliOptions = {
autoConnect: {
type: 'boolean',
description:
- 'If specified, automatically connects to a browser (Chrome 144+) running in the user data directory identified by the channel param. Requires the remoted debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.',
+ 'If specified, automatically connects to a running Brave instance using the user data directory identified by the channel param. Requires the remote debugging server to be started in the Brave instance via brave://inspect/#remote-debugging.',
conflicts: ['isolated', 'executablePath'],
default: false,
coerce: (value: boolean | undefined) => {
@@ -24,7 +24,7 @@ export const cliOptions = {
browserUrl: {
type: 'string',
description:
- 'Connect to a running, debuggable Chrome instance (e.g. `http://127.0.0.1:9222`). For more details see: https://github.com/ChromeDevTools/chrome-devtools-mcp#connecting-to-a-running-chrome-instance.',
+ 'Connect to a running, debuggable Brave instance (e.g. `http://127.0.0.1:9222`).',
alias: 'u',
conflicts: 'wsEndpoint',
coerce: (url: string | undefined) => {
@@ -42,7 +42,7 @@ export const cliOptions = {
wsEndpoint: {
type: 'string',
description:
- 'WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/). Alternative to --browserUrl.',
+ 'WebSocket endpoint to connect to a running Brave instance (e.g., ws://127.0.0.1:9222/devtools/browser/). Alternative to --browserUrl.',
alias: 'w',
conflicts: 'browserUrl',
coerce: (url: string | undefined) => {
@@ -94,7 +94,7 @@ export const cliOptions = {
},
executablePath: {
type: 'string',
- description: 'Path to custom Chrome executable.',
+ description: 'Path to custom Brave executable.',
conflicts: ['browserUrl', 'wsEndpoint'],
alias: 'e',
},
@@ -106,14 +106,14 @@ export const cliOptions = {
userDataDir: {
type: 'string',
description:
- 'Path to the user data directory for Chrome. Default is $HOME/.cache/chrome-devtools-mcp/chrome-profile$CHANNEL_SUFFIX_IF_NON_STABLE',
+ 'Path to the user data directory for Brave. Default is $HOME/.cache/brave-devtools-mcp/brave-profile$CHANNEL_SUFFIX_IF_NON_STABLE',
conflicts: ['browserUrl', 'wsEndpoint', 'isolated'],
},
channel: {
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,
+ 'Specify a different Brave channel that should be used. The default is the stable channel version.',
+ choices: ['stable', 'beta', 'dev', 'nightly'] as const,
conflicts: ['browserUrl', 'wsEndpoint', 'executablePath'],
},
logFile: {
@@ -124,7 +124,7 @@ export const cliOptions = {
viewport: {
type: 'string',
describe:
- 'Initial viewport size for the Chrome instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.',
+ 'Initial viewport size for the Brave instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.',
coerce: (arg: string | undefined) => {
if (arg === undefined) {
return;
@@ -141,7 +141,7 @@ export const cliOptions = {
},
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.`,
+ description: `Proxy server configuration for Brave passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details.`,
},
acceptInsecureCerts: {
type: 'boolean',
@@ -173,15 +173,15 @@ export const cliOptions = {
describe: 'Whether to enable interoperability tools',
hidden: true,
},
- chromeArg: {
+ braveArg: {
type: 'array',
describe:
- 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
+ 'Additional arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.',
},
- ignoreDefaultChromeArg: {
+ ignoreDefaultBraveArg: {
type: 'array',
describe:
- 'Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
+ 'Explicitly disable default arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.',
},
categoryEmulation: {
type: 'boolean',
@@ -204,32 +204,11 @@ export const cliOptions = {
hidden: true,
describe: 'Set to false to exclude tools related to extensions.',
},
- usageStatistics: {
- type: 'boolean',
- default: true,
- describe:
- 'Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.',
- },
- clearcutEndpoint: {
- type: 'string',
- hidden: true,
- describe: 'Endpoint for Clearcut telemetry.',
- },
- clearcutForceFlushIntervalMs: {
- type: 'number',
- hidden: true,
- describe: 'Force flush interval in milliseconds (for testing).',
- },
- clearcutIncludePidHeader: {
- type: 'boolean',
- hidden: true,
- describe: 'Include watchdog PID in Clearcut request headers (for testing).',
- },
} satisfies Record;
export function parseArguments(version: string, argv = process.argv) {
const yargsInstance = yargs(hideBin(argv))
- .scriptName('npx chrome-devtools-mcp@latest')
+ .scriptName('npx brave-devtools-mcp@latest')
.options(cliOptions)
.check(args => {
// We can't set default in the options else
@@ -257,22 +236,22 @@ export function parseArguments(version: string, argv = process.argv) {
`$0 --wsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123 --wsHeaders '{"Authorization":"Bearer token"}'`,
'Connect via WebSocket with custom headers',
],
- ['$0 --channel beta', 'Use Chrome Beta installed on this system'],
- ['$0 --channel canary', 'Use Chrome Canary installed on this system'],
- ['$0 --channel dev', 'Use Chrome Dev installed on this system'],
- ['$0 --channel stable', 'Use stable Chrome installed on this system'],
+ ['$0 --channel beta', 'Use Brave Beta installed on this system'],
+ ['$0 --channel nightly', 'Use Brave Nightly installed on this system'],
+ ['$0 --channel dev', 'Use Brave Dev installed on this system'],
+ ['$0 --channel stable', 'Use stable Brave 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',
+ 'Launch Brave with the initial viewport size of 1280x720px',
],
[
- `$0 --chrome-arg='--no-sandbox' --chrome-arg='--disable-setuid-sandbox'`,
- 'Launch Chrome without sandboxes. Use with caution.',
+ `$0 --brave-arg='--no-sandbox' --brave-arg='--disable-setuid-sandbox'`,
+ 'Launch Brave without sandboxes. Use with caution.',
],
[
- `$0 --ignore-default-chrome-arg='--disable-extensions'`,
+ `$0 --ignore-default-brave-arg='--disable-extensions'`,
'Disable the default arguments provided by Puppeteer. Use with caution.',
],
['$0 --no-category-emulation', 'Disable tools in the emulation category'],
@@ -287,15 +266,11 @@ export function parseArguments(version: string, argv = process.argv) {
],
[
'$0 --auto-connect',
- 'Connect to a stable Chrome instance (Chrome 144+) running instead of launching a new instance',
- ],
- [
- '$0 --auto-connect --channel=canary',
- 'Connect to a canary Chrome instance (Chrome 144+) running instead of launching a new instance',
+ 'Connect to a stable Brave instance running instead of launching a new instance',
],
[
- '$0 --no-usage-statistics',
- 'Do not send usage statistics https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics.',
+ '$0 --auto-connect --channel=nightly',
+ 'Connect to a nightly Brave instance running instead of launching a new instance',
],
]);
diff --git a/src/index.ts b/src/index.ts
index f20c4659a..e2226da3f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,21 +12,21 @@ const [major, minor] = version.substring(1).split('.').map(Number);
if (major === 20 && minor < 19) {
console.error(
- `ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 20.19.0 LTS or a newer LTS.`,
+ `ERROR: \`brave-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 20.19.0 LTS or a newer LTS.`,
);
process.exit(1);
}
if (major === 22 && minor < 12) {
console.error(
- `ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 22.12.0 LTS or a newer LTS.`,
+ `ERROR: \`brave-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 22.12.0 LTS or a newer LTS.`,
);
process.exit(1);
}
if (major < 20) {
console.error(
- `ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 20.19.0 LTS or a newer LTS.`,
+ `ERROR: \`brave-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 20.19.0 LTS or a newer LTS.`,
);
process.exit(1);
}
diff --git a/src/main.ts b/src/main.ts
index 6a02fd325..260b01527 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -10,15 +10,12 @@ import process from 'node:process';
import type {Channel} from './browser.js';
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
-import {cliOptions, parseArguments} from './cli.js';
+import {parseArguments} from './cli.js';
import {loadIssueDescriptions} from './issue-descriptions.js';
import {logger, saveLogsToFile} from './logger.js';
import {McpContext} from './McpContext.js';
import {McpResponse} from './McpResponse.js';
import {Mutex} from './Mutex.js';
-import {ClearcutLogger} from './telemetry/clearcut-logger.js';
-import {computeFlagUsage} from './telemetry/flag-utils.js';
-import {bucketizeLatency} from './telemetry/metric-utils.js';
import {
McpServer,
StdioServerTransport,
@@ -37,36 +34,16 @@ const VERSION = '0.15.1';
export const args = parseArguments(VERSION);
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
-if (
- process.env['CI'] ||
- process.env['CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS']
-) {
- console.error(
- "turning off usage statistics. process.env['CI'] || process.env['CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS'] is set.",
- );
- args.usageStatistics = false;
-}
-
-let clearcutLogger: ClearcutLogger | undefined;
-if (args.usageStatistics) {
- clearcutLogger = new ClearcutLogger({
- logFile: args.logFile,
- appVersion: VERSION,
- clearcutEndpoint: args.clearcutEndpoint,
- clearcutForceFlushIntervalMs: args.clearcutForceFlushIntervalMs,
- clearcutIncludePidHeader: args.clearcutIncludePidHeader,
- });
-}
process.on('unhandledRejection', (reason, promise) => {
logger('Unhandled promise rejection', promise, reason);
});
-logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
+logger(`Starting Brave DevTools MCP Server v${VERSION}`);
const server = new McpServer(
{
- name: 'chrome_devtools',
- title: 'Chrome DevTools MCP server',
+ name: 'brave_devtools',
+ title: 'Brave DevTools MCP server',
version: VERSION,
},
{capabilities: {logging: {}}},
@@ -77,12 +54,12 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {
let context: McpContext;
async function getContext(): Promise {
- const chromeArgs: string[] = (args.chromeArg ?? []).map(String);
- const ignoreDefaultChromeArgs: string[] = (
- args.ignoreDefaultChromeArg ?? []
+ const braveArgs: string[] = (args.braveArg ?? []).map(String);
+ const ignoreDefaultBraveArgs: string[] = (
+ args.ignoreDefaultBraveArg ?? []
).map(String);
if (args.proxyServer) {
- chromeArgs.push(`--proxy-server=${args.proxyServer}`);
+ braveArgs.push(`--proxy-server=${args.proxyServer}`);
}
const devtools = args.experimentalDevtools ?? false;
const browser =
@@ -104,8 +81,8 @@ async function getContext(): Promise {
userDataDir: args.userDataDir,
logFile,
viewport: args.viewport,
- chromeArgs,
- ignoreDefaultChromeArgs,
+ braveArgs,
+ ignoreDefaultBraveArgs,
acceptInsecureCerts: args.acceptInsecureCerts,
devtools,
enableExtensions: args.categoryExtensions,
@@ -122,18 +99,10 @@ async function getContext(): Promise {
const logDisclaimers = () => {
console.error(
- `chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
+ `brave-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
debug, and modify any data in the browser or DevTools.
Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`,
);
-
- if (args.usageStatistics) {
- console.error(
- `
-Google collects usage statistics to improve Chrome DevTools MCP. To opt-out, run with --no-usage-statistics.
-For more details, visit: https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics`,
- );
- }
};
const toolMutex = new Mutex();
@@ -184,8 +153,6 @@ function registerTool(tool: ToolDefinition): void {
},
async (params): Promise => {
const guard = await toolMutex.acquire();
- const startTime = Date.now();
- let success = false;
try {
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
const context = await getContext();
@@ -208,7 +175,6 @@ function registerTool(tool: ToolDefinition): void {
} = {
content,
};
- success = true;
if (args.experimentalStructuredContent) {
result.structuredContent = structuredContent as Record<
string,
@@ -232,11 +198,6 @@ function registerTool(tool: ToolDefinition): void {
isError: true,
};
} finally {
- void clearcutLogger?.logToolInvocation({
- toolName: tool.name,
- success,
- latencyMs: bucketizeLatency(Date.now() - startTime),
- });
guard.dispose();
}
},
@@ -250,7 +211,5 @@ for (const tool of tools) {
await loadIssueDescriptions();
const transport = new StdioServerTransport();
await server.connect(transport);
-logger('Chrome DevTools MCP Server connected');
+logger('Brave DevTools MCP Server connected');
logDisclaimers();
-void clearcutLogger?.logDailyActiveIfNeeded();
-void clearcutLogger?.logServerStart(computeFlagUsage(args, cliOptions));
diff --git a/src/telemetry/clearcut-logger.ts b/src/telemetry/clearcut-logger.ts
deleted file mode 100644
index 6a1d6351d..000000000
--- a/src/telemetry/clearcut-logger.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import process from 'node:process';
-
-import {logger} from '../logger.js';
-
-import type {LocalState, Persistence} from './persistence.js';
-import {FilePersistence} from './persistence.js';
-import {type FlagUsage, WatchdogMessageType, OsType} from './types.js';
-import {WatchdogClient} from './watchdog-client.js';
-
-const MS_PER_DAY = 24 * 60 * 60 * 1000;
-
-function detectOsType(): OsType {
- switch (process.platform) {
- case 'win32':
- return OsType.OS_TYPE_WINDOWS;
- case 'darwin':
- return OsType.OS_TYPE_MACOS;
- case 'linux':
- return OsType.OS_TYPE_LINUX;
- default:
- return OsType.OS_TYPE_UNSPECIFIED;
- }
-}
-
-export class ClearcutLogger {
- #persistence: Persistence;
- #watchdog: WatchdogClient;
-
- constructor(options: {
- appVersion: string;
- logFile?: string;
- persistence?: Persistence;
- watchdogClient?: WatchdogClient;
- clearcutEndpoint?: string;
- clearcutForceFlushIntervalMs?: number;
- clearcutIncludePidHeader?: boolean;
- }) {
- this.#persistence = options.persistence ?? new FilePersistence();
- this.#watchdog =
- options.watchdogClient ??
- new WatchdogClient({
- parentPid: process.pid,
- appVersion: options.appVersion,
- osType: detectOsType(),
- logFile: options.logFile,
- clearcutEndpoint: options.clearcutEndpoint,
- clearcutForceFlushIntervalMs: options.clearcutForceFlushIntervalMs,
- clearcutIncludePidHeader: options.clearcutIncludePidHeader,
- });
- }
-
- async logToolInvocation(args: {
- toolName: string;
- success: boolean;
- latencyMs: number;
- }): Promise {
- this.#watchdog.send({
- type: WatchdogMessageType.LOG_EVENT,
- payload: {
- tool_invocation: {
- tool_name: args.toolName,
- success: args.success,
- latency_ms: args.latencyMs,
- },
- },
- });
- }
-
- async logServerStart(flagUsage: FlagUsage): Promise {
- this.#watchdog.send({
- type: WatchdogMessageType.LOG_EVENT,
- payload: {
- server_start: {
- flag_usage: flagUsage,
- },
- },
- });
- }
-
- async logDailyActiveIfNeeded(): Promise {
- try {
- const state = await this.#persistence.loadState();
-
- if (this.#shouldLogDailyActive(state)) {
- let daysSince = -1;
- if (state.lastActive) {
- const lastActiveDate = new Date(state.lastActive);
- const now = new Date();
- const diffTime = Math.abs(now.getTime() - lastActiveDate.getTime());
- daysSince = Math.ceil(diffTime / MS_PER_DAY);
- }
-
- this.#watchdog.send({
- type: WatchdogMessageType.LOG_EVENT,
- payload: {
- daily_active: {
- days_since_last_active: daysSince,
- },
- },
- });
-
- state.lastActive = new Date().toISOString();
- await this.#persistence.saveState(state);
- }
- } catch (err) {
- logger('Error in logDailyActiveIfNeeded:', err);
- }
- }
-
- #shouldLogDailyActive(state: LocalState): boolean {
- if (!state.lastActive) {
- return true;
- }
- const lastActiveDate = new Date(state.lastActive);
- const now = new Date();
-
- // Compare UTC dates
- const isSameDay =
- lastActiveDate.getUTCFullYear() === now.getUTCFullYear() &&
- lastActiveDate.getUTCMonth() === now.getUTCMonth() &&
- lastActiveDate.getUTCDate() === now.getUTCDate();
-
- return !isSameDay;
- }
-}
diff --git a/src/telemetry/flag-utils.ts b/src/telemetry/flag-utils.ts
deleted file mode 100644
index 525ac4817..000000000
--- a/src/telemetry/flag-utils.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import type {cliOptions} from '../cli.js';
-import {toSnakeCase} from '../utils/string.js';
-
-import type {FlagUsage} from './types.js';
-
-type CliOptions = typeof cliOptions;
-
-/**
- * Computes telemetry flag usage from parsed arguments and CLI options.
- *
- * Iterates over the defined CLI options to construct a payload:
- * - Flag names are converted to snake_case (e.g. `browserUrl` -> `browser_url`).
- * - A flag is logged as `{flag_name}_present` if:
- * - It has no default value, OR
- * - The provided value differs from the default value.
- * - Boolean flags are logged with their literal value.
- * - String flags with defined `choices` (Enums) are logged as their uppercase value.
- */
-export function computeFlagUsage(
- args: Record,
- options: CliOptions,
-): FlagUsage {
- const usage: FlagUsage = {};
-
- for (const [flagName, config] of Object.entries(options)) {
- const value = args[flagName];
- const snakeCaseName = toSnakeCase(flagName);
-
- // If there isn't a default value provided for the flag,
- // we're going to log whether it's present on the args user
- // provided or not. If there is a default value, we only log presence
- // if the value differs from the default, implying explicit user intent.
- if (!('default' in config) || value !== config.default) {
- usage[`${snakeCaseName}_present`] = value !== undefined && value !== null;
- }
-
- if (config.type === 'boolean' && typeof value === 'boolean') {
- // For boolean options, we're going to log the value directly.
- usage[snakeCaseName] = value;
- } else if (
- config.type === 'string' &&
- typeof value === 'string' &&
- 'choices' in config &&
- config.choices
- ) {
- // For enums, log the value as uppercase
- // We're going to have an enum for such flags with choices represented
- // as an `enum` where the keys of the enum will map to the uppercase `choice`.
- usage[snakeCaseName] = `${snakeCaseName}_${value}`.toUpperCase();
- }
- }
-
- return usage;
-}
diff --git a/src/telemetry/metric-utils.ts b/src/telemetry/metric-utils.ts
deleted file mode 100644
index 55e834f41..000000000
--- a/src/telemetry/metric-utils.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-const LATENCY_BUCKETS = [50, 100, 250, 500, 1000, 2500, 5000, 10000];
-
-export function bucketizeLatency(latencyMs: number): number {
- for (const bucket of LATENCY_BUCKETS) {
- if (latencyMs <= bucket) {
- return bucket;
- }
- }
- return LATENCY_BUCKETS[LATENCY_BUCKETS.length - 1];
-}
diff --git a/src/telemetry/persistence.ts b/src/telemetry/persistence.ts
deleted file mode 100644
index 5b133649b..000000000
--- a/src/telemetry/persistence.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import fs from 'node:fs/promises';
-import os from 'node:os';
-import path from 'node:path';
-import process from 'node:process';
-
-import {logger} from '../logger.js';
-
-export interface LocalState {
- lastActive: string; // ISO 8601 UTC date string
-}
-
-const STATE_FILE_NAME = 'telemetry_state.json';
-function getDataFolder(): string {
- const homedir = os.homedir();
- const {env} = process;
- const name = 'chrome-devtools-mcp';
-
- if (process.platform === 'darwin') {
- return path.join(homedir, 'Library', 'Application Support', name);
- }
-
- if (process.platform === 'win32') {
- const localAppData =
- env.LOCALAPPDATA || path.join(homedir, 'AppData', 'Local');
- return path.join(localAppData, name, 'Data');
- }
-
- return path.join(
- env.XDG_DATA_HOME || path.join(homedir, '.local', 'share'),
- name,
- );
-}
-
-export interface Persistence {
- loadState(): Promise;
- saveState(state: LocalState): Promise;
-}
-
-export class FilePersistence implements Persistence {
- #dataFolder: string;
-
- constructor(dataFolderOverride?: string) {
- this.#dataFolder = dataFolderOverride ?? getDataFolder();
- }
-
- async loadState(): Promise {
- try {
- const filePath = path.join(this.#dataFolder, STATE_FILE_NAME);
- const content = await fs.readFile(filePath, 'utf-8');
- return JSON.parse(content) as LocalState;
- } catch {
- return {
- lastActive: '',
- };
- }
- }
-
- async saveState(state: LocalState): Promise {
- const filePath = path.join(this.#dataFolder, STATE_FILE_NAME);
- try {
- await fs.mkdir(this.#dataFolder, {recursive: true});
- await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
- } catch (error) {
- // Ignore errors during state saving to avoid crashing the server
- logger(`Failed to save telemetry state to ${filePath}:`, error);
- }
- }
-}
diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts
deleted file mode 100644
index 24d18b94f..000000000
--- a/src/telemetry/types.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-// Protobuf message interfaces
-export interface ChromeDevToolsMcpExtension {
- os_type?: OsType;
- mcp_client?: McpClient;
- app_version?: string;
- session_id?: string;
- tool_invocation?: ToolInvocation;
- server_start?: ServerStart;
- daily_active?: DailyActive;
- server_shutdown?: ServerShutdown;
-}
-
-export type ServerShutdown = Record;
-
-export interface ToolInvocation {
- tool_name: string;
- success: boolean;
- latency_ms: number;
-}
-
-export interface ServerStart {
- flag_usage?: FlagUsage;
-}
-
-export interface DailyActive {
- days_since_last_active: number;
-}
-
-export type FlagUsage = Record;
-
-// Clearcut API interfaces
-export interface LogRequest {
- log_source: number;
- request_time_ms: string;
- client_info: {
- client_type: number;
- };
- log_event: Array<{
- event_time_ms: string;
- source_extension_json: string;
- }>;
-}
-
-export interface LogResponse {
- /**
- * If present, the client must wait this many milliseconds before
- * issuing the next HTTP request.
- */
- next_request_wait_millis?: number;
-}
-
-// Enums
-export enum OsType {
- OS_TYPE_UNSPECIFIED = 0,
- OS_TYPE_WINDOWS = 1,
- OS_TYPE_MACOS = 2,
- OS_TYPE_LINUX = 3,
-}
-
-export enum ChromeChannel {
- CHROME_CHANNEL_UNSPECIFIED = 0,
- CHROME_CHANNEL_CANARY = 1,
- CHROME_CHANNEL_DEV = 2,
- CHROME_CHANNEL_BETA = 3,
- CHROME_CHANNEL_STABLE = 4,
-}
-
-export enum McpClient {
- MCP_CLIENT_UNSPECIFIED = 0,
- MCP_CLIENT_CLAUDE_CODE = 1,
- MCP_CLIENT_GEMINI_CLI = 2,
-}
-
-// IPC types for messages between the main process and the
-// telemetry watchdog process.
-export enum WatchdogMessageType {
- LOG_EVENT = 'log-event',
-}
-
-export interface WatchdogMessage {
- type: WatchdogMessageType.LOG_EVENT;
- payload: ChromeDevToolsMcpExtension;
-}
diff --git a/src/telemetry/watchdog-client.ts b/src/telemetry/watchdog-client.ts
deleted file mode 100644
index 36090541f..000000000
--- a/src/telemetry/watchdog-client.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {spawn, type ChildProcess} from 'node:child_process';
-import {fileURLToPath} from 'node:url';
-
-import {logger} from '../logger.js';
-
-import type {WatchdogMessage, OsType} from './types.js';
-
-export class WatchdogClient {
- #childProcess: ChildProcess;
-
- constructor(
- config: {
- parentPid: number;
- appVersion: string;
- osType: OsType;
- logFile?: string;
- clearcutEndpoint?: string;
- clearcutForceFlushIntervalMs?: number;
- clearcutIncludePidHeader?: boolean;
- },
- options?: {spawn?: typeof spawn},
- ) {
- const watchdogPath = fileURLToPath(
- new URL('./watchdog/main.js', import.meta.url),
- );
-
- const args = [
- watchdogPath,
- `--parent-pid=${config.parentPid}`,
- `--app-version=${config.appVersion}`,
- `--os-type=${config.osType}`,
- ];
-
- if (config.logFile) {
- args.push(`--log-file=${config.logFile}`);
- }
- if (config.clearcutEndpoint) {
- args.push(`--clearcut-endpoint=${config.clearcutEndpoint}`);
- }
- if (config.clearcutForceFlushIntervalMs) {
- args.push(
- `--clearcut-force-flush-interval-ms=${config.clearcutForceFlushIntervalMs}`,
- );
- }
- if (config.clearcutIncludePidHeader) {
- args.push('--clearcut-include-pid-header');
- }
-
- const spawner = options?.spawn ?? spawn;
- this.#childProcess = spawner(process.execPath, args, {
- stdio: ['pipe', 'ignore', 'ignore'],
- detached: true,
- });
- this.#childProcess.unref();
- this.#childProcess.on('error', err => {
- logger('Watchdog process error:', err);
- });
- this.#childProcess.on('exit', (code, signal) => {
- logger(`Watchdog exited with code ${code} and signal ${signal}`);
- });
- }
-
- send(message: WatchdogMessage): void {
- if (
- this.#childProcess.stdin &&
- !this.#childProcess.stdin.destroyed &&
- this.#childProcess.pid
- ) {
- try {
- const line = JSON.stringify(message) + '\n';
- this.#childProcess.stdin.write(line);
- } catch (err) {
- logger('Failed to write to watchdog stdin', err);
- }
- } else {
- logger('Watchdog stdin not available, dropping message');
- }
- }
-}
diff --git a/src/telemetry/watchdog/clearcut-sender.ts b/src/telemetry/watchdog/clearcut-sender.ts
deleted file mode 100644
index 981764b93..000000000
--- a/src/telemetry/watchdog/clearcut-sender.ts
+++ /dev/null
@@ -1,255 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import crypto from 'node:crypto';
-
-import {logger} from '../../logger.js';
-import type {
- ChromeDevToolsMcpExtension,
- LogRequest,
- LogResponse,
- OsType,
-} from '../types.js';
-
-export interface ClearcutSenderConfig {
- appVersion: string;
- osType: OsType;
- clearcutEndpoint?: string;
- forceFlushIntervalMs?: number;
- includePidHeader?: boolean;
-}
-
-const MAX_BUFFER_SIZE = 1000;
-const DEFAULT_CLEARCUT_ENDPOINT =
- 'https://play.googleapis.com/log?format=json_proto';
-const DEFAULT_FLUSH_INTERVAL_MS = 15 * 60 * 1000;
-
-const LOG_SOURCE = 2839;
-const CLIENT_TYPE = 47;
-const MIN_RATE_LIMIT_WAIT_MS = 30_000;
-const REQUEST_TIMEOUT_MS = 30_000;
-const SHUTDOWN_TIMEOUT_MS = 5_000;
-const SESSION_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
-
-interface BufferedEvent {
- event: ChromeDevToolsMcpExtension;
- timestamp: number;
-}
-
-export class ClearcutSender {
- #appVersion: string;
- #osType: OsType;
- #clearcutEndpoint: string;
- #flushIntervalMs: number;
- #includePidHeader: boolean;
- #sessionId: string;
- #sessionCreated: number;
- #buffer: BufferedEvent[] = [];
- #flushTimer: ReturnType | null = null;
- #isFlushing = false;
- #timerStarted = false;
-
- constructor(config: ClearcutSenderConfig) {
- this.#appVersion = config.appVersion;
- this.#osType = config.osType;
- this.#clearcutEndpoint =
- config.clearcutEndpoint ?? DEFAULT_CLEARCUT_ENDPOINT;
- this.#flushIntervalMs =
- config.forceFlushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
- this.#includePidHeader = config.includePidHeader ?? false;
- this.#sessionId = crypto.randomUUID();
- this.#sessionCreated = Date.now();
- }
-
- enqueueEvent(event: ChromeDevToolsMcpExtension): void {
- if (Date.now() - this.#sessionCreated > SESSION_ROTATION_INTERVAL_MS) {
- this.#sessionId = crypto.randomUUID();
- this.#sessionCreated = Date.now();
- }
-
- logger('Enqueing telemetry event', JSON.stringify(event, null, 2));
-
- this.#addToBuffer({
- ...event,
- session_id: this.#sessionId,
- app_version: this.#appVersion,
- os_type: this.#osType,
- });
-
- if (!this.#timerStarted) {
- this.#timerStarted = true;
- this.#scheduleFlush(this.#flushIntervalMs);
- }
- }
-
- async sendShutdownEvent(): Promise {
- if (this.#flushTimer) {
- clearTimeout(this.#flushTimer);
- this.#flushTimer = null;
- }
-
- const shutdownEvent: ChromeDevToolsMcpExtension = {
- server_shutdown: {},
- };
- this.enqueueEvent(shutdownEvent);
-
- try {
- await Promise.race([
- this.#finalFlush(),
- new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS)),
- ]);
- logger('Final flush completed');
- } catch (error) {
- logger('Final flush failed:', error);
- }
- }
-
- async #flush(): Promise {
- if (this.#isFlushing) {
- return;
- }
-
- if (this.#buffer.length === 0) {
- this.#scheduleFlush(this.#flushIntervalMs);
- return;
- }
-
- this.#isFlushing = true;
- let nextDelayMs = this.#flushIntervalMs;
-
- // Optimistically remove events from buffer before sending.
- // This prevents race conditions where a simultaneous #finalFlush would include these same events.
- const eventsToSend = [...this.#buffer];
- this.#buffer = [];
-
- try {
- const result = await this.#sendBatch(eventsToSend);
-
- if (result.success) {
- if (result.nextRequestWaitMs !== undefined) {
- nextDelayMs = Math.max(
- result.nextRequestWaitMs,
- MIN_RATE_LIMIT_WAIT_MS,
- );
- }
- } else if (result.isPermanentError) {
- logger(
- 'Permanent error, dropped batch of',
- eventsToSend.length,
- 'events',
- );
- } else {
- // Transient error: Requeue events at the front of the buffer
- // to maintain order and retry them later.
- this.#buffer = [...eventsToSend, ...this.#buffer];
- }
- } catch (error) {
- // Safety catch for unexpected errors, requeue events
- this.#buffer = [...eventsToSend, ...this.#buffer];
- logger('Flush failed unexpectedly:', error);
- } finally {
- this.#isFlushing = false;
- this.#scheduleFlush(nextDelayMs);
- }
- }
-
- #addToBuffer(event: ChromeDevToolsMcpExtension): void {
- if (this.#buffer.length >= MAX_BUFFER_SIZE) {
- this.#buffer.shift();
- logger('Telemetry buffer overflow: dropped oldest event');
- }
- this.#buffer.push({
- event,
- timestamp: Date.now(),
- });
- }
-
- #scheduleFlush(delayMs: number): void {
- if (this.#flushTimer) {
- clearTimeout(this.#flushTimer);
- }
- this.#flushTimer = setTimeout(() => {
- this.#flush().catch(err => {
- logger('Flush error:', err);
- });
- }, delayMs);
- }
-
- async #sendBatch(events: BufferedEvent[]): Promise<{
- success: boolean;
- isPermanentError?: boolean;
- nextRequestWaitMs?: number;
- }> {
- const requestBody: LogRequest = {
- log_source: LOG_SOURCE,
- request_time_ms: Date.now().toString(),
- client_info: {
- client_type: CLIENT_TYPE,
- },
- log_event: events.map(({event, timestamp}) => ({
- event_time_ms: timestamp.toString(),
- source_extension_json: JSON.stringify(event),
- })),
- };
-
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
- try {
- const response = await fetch(this.#clearcutEndpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- // Used in E2E tests to confirm that the watchdog process is killed
- ...(this.#includePidHeader
- ? {'X-Watchdog-Pid': process.pid.toString()}
- : {}),
- },
- body: JSON.stringify(requestBody),
- signal: controller.signal,
- });
-
- clearTimeout(timeoutId);
- if (response.ok) {
- const data = (await response.json()) as LogResponse;
- return {
- success: true,
- nextRequestWaitMs: data.next_request_wait_millis,
- };
- }
-
- const status = response.status;
- if (status >= 500 || status === 429) {
- return {success: false};
- }
-
- logger('Telemetry permanent error:', status);
- return {success: false, isPermanentError: true};
- } catch {
- clearTimeout(timeoutId);
- return {success: false};
- }
- }
-
- async #finalFlush(): Promise {
- if (this.#buffer.length === 0) {
- return;
- }
- const eventsToSend = [...this.#buffer];
- await this.#sendBatch(eventsToSend);
- }
-
- stopForTesting(): void {
- if (this.#flushTimer) {
- clearTimeout(this.#flushTimer);
- this.#flushTimer = null;
- }
- this.#timerStarted = false;
- }
-
- get bufferSizeForTesting(): number {
- return this.#buffer.length;
- }
-}
diff --git a/src/telemetry/watchdog/main.ts b/src/telemetry/watchdog/main.ts
deleted file mode 100644
index 60536b583..000000000
--- a/src/telemetry/watchdog/main.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import type {WriteStream} from 'node:fs';
-import process from 'node:process';
-import readline from 'node:readline';
-import {parseArgs} from 'node:util';
-
-import {logger, flushLogs, saveLogsToFile} from '../../logger.js';
-import type {OsType} from '../types.js';
-import {WatchdogMessageType} from '../types.js';
-
-import {ClearcutSender} from './clearcut-sender.js';
-
-interface WatchdogArgs {
- // Required arguments
- parentPid: number;
- appVersion: string;
- osType: OsType;
- // Optional arguments
- logFile?: string;
- clearcutEndpoint?: string;
- clearcutForceFlushIntervalMs?: number;
- clearcutIncludePidHeader?: boolean;
-}
-
-function parseWatchdogArgs(): WatchdogArgs {
- const {values} = parseArgs({
- options: {
- 'parent-pid': {type: 'string'},
- 'app-version': {type: 'string'},
- 'os-type': {type: 'string'},
- 'log-file': {type: 'string'},
- 'clearcut-endpoint': {type: 'string'},
- 'clearcut-force-flush-interval-ms': {type: 'string'},
- 'clearcut-include-pid-header': {type: 'boolean'},
- },
- strict: true,
- });
- // Verify required arguments
- const parentPid = parseInt(values['parent-pid'] ?? '', 10);
- const appVersion = values['app-version'];
- const osType = parseInt(values['os-type'] ?? '', 10);
- if (isNaN(parentPid) || !appVersion || isNaN(osType)) {
- console.error(
- 'Invalid arguments provided for watchdog process: ',
- JSON.stringify({parentPid, appVersion, osType}),
- );
- process.exit(1);
- }
-
- // Parse Optional Arguments
- const logFile = values['log-file'];
- const clearcutEndpoint = values['clearcut-endpoint'];
- const clearcutIncludePidHeader = values['clearcut-include-pid-header'];
- let clearcutForceFlushIntervalMs: number | undefined;
- if (values['clearcut-force-flush-interval-ms']) {
- const parsed = parseInt(values['clearcut-force-flush-interval-ms'], 10);
- if (!isNaN(parsed)) {
- clearcutForceFlushIntervalMs = parsed;
- }
- }
-
- return {
- parentPid,
- appVersion,
- osType,
- logFile,
- clearcutEndpoint,
- clearcutForceFlushIntervalMs,
- clearcutIncludePidHeader,
- };
-}
-
-function main() {
- const {
- parentPid,
- appVersion,
- osType,
- logFile,
- clearcutEndpoint,
- clearcutForceFlushIntervalMs,
- clearcutIncludePidHeader,
- } = parseWatchdogArgs();
- let logStream: WriteStream | undefined;
- if (logFile) {
- logStream = saveLogsToFile(logFile);
- }
-
- const exit = (code: number) => {
- if (!logStream) {
- process.exit(code);
- }
-
- void flushLogs(logStream).finally(() => {
- process.exit(code);
- });
- };
-
- logger(
- 'Watchdog started',
- JSON.stringify(
- {
- pid: process.pid,
- parentPid,
- version: appVersion,
- osType,
- },
- null,
- 2,
- ),
- );
-
- const sender = new ClearcutSender({
- appVersion,
- osType: osType,
- clearcutEndpoint,
- forceFlushIntervalMs: clearcutForceFlushIntervalMs,
- includePidHeader: clearcutIncludePidHeader,
- });
-
- let isShuttingDown = false;
- function onParentDeath(reason: string) {
- if (isShuttingDown) {
- return;
- }
-
- isShuttingDown = true;
- logger(`Parent death detected (${reason}). Sending shutdown event...`);
- sender
- .sendShutdownEvent()
- .then(() => {
- logger('Shutdown event sent. Exiting.');
- exit(0);
- })
- .catch(err => {
- logger('Failed to send shutdown event', err);
- exit(1);
- });
- }
-
- process.stdin.on('end', () => onParentDeath('stdin end'));
- process.stdin.on('close', () => onParentDeath('stdin close'));
- process.on('disconnect', () => onParentDeath('ipc disconnect'));
-
- const rl = readline.createInterface({
- input: process.stdin,
- terminal: false,
- });
-
- rl.on('line', line => {
- try {
- if (!line.trim()) {
- return;
- }
-
- const msg = JSON.parse(line);
- if (msg.type === WatchdogMessageType.LOG_EVENT && msg.payload) {
- sender.enqueueEvent(msg.payload);
- }
- } catch (err) {
- logger('Failed to parse IPC message', err);
- }
- });
-}
-
-try {
- main();
-} catch (err) {
- console.error('Watchdog fatal error:', err);
- process.exit(1);
-}
diff --git a/src/third_party/index.ts b/src/third_party/index.ts
index 55c094954..9e54c212d 100644
--- a/src/third_party/index.ts
+++ b/src/third_party/index.ts
@@ -32,10 +32,8 @@ export {default as puppeteer} from 'puppeteer-core';
export type * from 'puppeteer-core';
export type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js';
export {
- resolveDefaultUserDataDir,
detectBrowserPlatform,
Browser as BrowserEnum,
- type ChromeReleaseChannel as BrowsersChromeReleaseChannel,
} from '@puppeteer/browsers';
export * as DevTools from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js';
diff --git a/tests/browser.test.ts b/tests/browser.test.ts
index aad6eec9c..be25f8350 100644
--- a/tests/browser.test.ts
+++ b/tests/browser.test.ts
@@ -82,7 +82,7 @@ describe('browser', () => {
userDataDir: folderPath,
executablePath: executablePath(),
devtools: false,
- chromeArgs: ['--remote-debugging-port=0'],
+ braveArgs: ['--remote-debugging-port=0'],
});
try {
const connectedBrowser = await ensureBrowserConnected({
diff --git a/tests/cli.test.ts b/tests/cli.test.ts
index 57b1dbb1f..e20d1abf2 100644
--- a/tests/cli.test.ts
+++ b/tests/cli.test.ts
@@ -21,8 +21,6 @@ describe('cli args parsing', () => {
categoryNetwork: true,
'auto-connect': undefined,
autoConnect: undefined,
- 'usage-statistics': true,
- usageStatistics: true,
};
it('parses with default args', async () => {
@@ -31,7 +29,7 @@ describe('cli args parsing', () => {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
channel: 'stable',
});
});
@@ -47,7 +45,7 @@ describe('cli args parsing', () => {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
'browser-url': 'http://localhost:3000',
browserUrl: 'http://localhost:3000',
u: 'http://localhost:3000',
@@ -59,16 +57,16 @@ describe('cli args parsing', () => {
'node',
'main.js',
'--user-data-dir',
- '/tmp/chrome-profile',
+ '/tmp/brave-profile',
]);
assert.deepStrictEqual(args, {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
channel: 'stable',
- 'user-data-dir': '/tmp/chrome-profile',
- userDataDir: '/tmp/chrome-profile',
+ 'user-data-dir': '/tmp/brave-profile',
+ userDataDir: '/tmp/brave-profile',
});
});
@@ -83,7 +81,7 @@ describe('cli args parsing', () => {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
'browser-url': undefined,
browserUrl: undefined,
u: undefined,
@@ -96,16 +94,16 @@ describe('cli args parsing', () => {
'node',
'main.js',
'--executablePath',
- '/tmp/test 123/chrome',
+ '/tmp/test 123/brave',
]);
assert.deepStrictEqual(args, {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
- 'executable-path': '/tmp/test 123/chrome',
- e: '/tmp/test 123/chrome',
- executablePath: '/tmp/test 123/chrome',
+ $0: 'npx brave-devtools-mcp@latest',
+ 'executable-path': '/tmp/test 123/brave',
+ e: '/tmp/test 123/brave',
+ executablePath: '/tmp/test 123/brave',
});
});
@@ -120,7 +118,7 @@ describe('cli args parsing', () => {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
channel: 'stable',
viewport: {
width: 888,
@@ -129,42 +127,42 @@ describe('cli args parsing', () => {
});
});
- it('parses chrome args', async () => {
+ it('parses brave args', async () => {
const args = parseArguments('1.0.0', [
'node',
'main.js',
- `--chrome-arg='--no-sandbox'`,
- `--chrome-arg='--disable-setuid-sandbox'`,
+ `--brave-arg='--no-sandbox'`,
+ `--brave-arg='--disable-setuid-sandbox'`,
]);
assert.deepStrictEqual(args, {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
channel: 'stable',
- 'chrome-arg': ['--no-sandbox', '--disable-setuid-sandbox'],
- chromeArg: ['--no-sandbox', '--disable-setuid-sandbox'],
+ 'brave-arg': ['--no-sandbox', '--disable-setuid-sandbox'],
+ braveArg: ['--no-sandbox', '--disable-setuid-sandbox'],
});
});
- it('parses ignore chrome args', async () => {
+ it('parses ignore brave args', async () => {
const args = parseArguments('1.0.0', [
'node',
'main.js',
- `--ignore-default-chrome-arg='--disable-extensions'`,
- `--ignore-default-chrome-arg='--disable-cancel-all-touches'`,
+ `--ignore-default-brave-arg='--disable-extensions'`,
+ `--ignore-default-brave-arg='--disable-cancel-all-touches'`,
]);
assert.deepStrictEqual(args, {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
channel: 'stable',
- 'ignore-default-chrome-arg': [
+ 'ignore-default-brave-arg': [
'--disable-extensions',
'--disable-cancel-all-touches',
],
- ignoreDefaultChromeArg: [
+ ignoreDefaultBraveArg: [
'--disable-extensions',
'--disable-cancel-all-touches',
],
@@ -182,7 +180,7 @@ describe('cli args parsing', () => {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
'ws-endpoint': 'ws://127.0.0.1:9222/devtools/browser/abc123',
wsEndpoint: 'ws://127.0.0.1:9222/devtools/browser/abc123',
w: 'ws://127.0.0.1:9222/devtools/browser/abc123',
@@ -200,7 +198,7 @@ describe('cli args parsing', () => {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
'ws-endpoint': 'wss://example.com:9222/devtools/browser/abc123',
wsEndpoint: 'wss://example.com:9222/devtools/browser/abc123',
w: 'wss://example.com:9222/devtools/browser/abc123',
@@ -232,7 +230,7 @@ describe('cli args parsing', () => {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
channel: 'stable',
'category-emulation': false,
categoryEmulation: false,
@@ -244,32 +242,10 @@ describe('cli args parsing', () => {
...defaultArgs,
_: [],
headless: false,
- $0: 'npx chrome-devtools-mcp@latest',
+ $0: 'npx brave-devtools-mcp@latest',
channel: 'stable',
'auto-connect': true,
autoConnect: true,
});
});
-
- it('parses usage statistics flag', async () => {
- // Test default (should be true).
- const defaultArgs = parseArguments('1.0.0', ['node', 'main.js']);
- assert.strictEqual(defaultArgs.usageStatistics, true);
-
- // Test enabling it
- const enabledArgs = parseArguments('1.0.0', [
- 'node',
- 'main.js',
- '--usage-statistics',
- ]);
- assert.strictEqual(enabledArgs.usageStatistics, true);
-
- // Test disabling it
- const disabledArgs = parseArguments('1.0.0', [
- 'node',
- 'main.js',
- '--no-usage-statistics',
- ]);
- assert.strictEqual(disabledArgs.usageStatistics, false);
- });
});
diff --git a/tests/e2e/telemetry.test.ts b/tests/e2e/telemetry.test.ts
deleted file mode 100644
index 10051a676..000000000
--- a/tests/e2e/telemetry.test.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import assert from 'node:assert';
-import {spawn, type ChildProcess, type SpawnOptions} from 'node:child_process';
-import http from 'node:http';
-import type {AddressInfo} from 'node:net';
-import path from 'node:path';
-import {describe, it} from 'node:test';
-
-import type {ChromeDevToolsMcpExtension} from '../../src/telemetry/types';
-
-const SERVER_PATH = path.resolve('build/src/main.js');
-
-interface MockServerContext {
- server: http.Server;
- port: number;
- events: ChromeDevToolsMcpExtension[];
- watchdogPid?: number;
- waitForEvent: (
- predicate: (event: ChromeDevToolsMcpExtension) => boolean,
- ) => Promise;
-}
-
-async function startMockServer(): Promise {
- const events: ChromeDevToolsMcpExtension[] = [];
- let waitingResolvers: Array<{
- predicate: (event: ChromeDevToolsMcpExtension) => boolean;
- resolve: (event: ChromeDevToolsMcpExtension) => void;
- }> = [];
- let watchdogPid: number | undefined;
-
- const server = http.createServer((req, res) => {
- if (req.method === 'POST') {
- const pidHeader = req.headers['x-watchdog-pid'];
- if (pidHeader && !Array.isArray(pidHeader)) {
- watchdogPid = parseInt(pidHeader, 10);
- }
-
- let body = '';
- req.on('data', chunk => {
- body += chunk.toString();
- });
- req.on('end', () => {
- try {
- const parsed = JSON.parse(body);
- // Extract internal log events
- if (parsed.log_event) {
- for (const logEvent of parsed.log_event) {
- if (logEvent.source_extension_json) {
- const ext = JSON.parse(
- logEvent.source_extension_json,
- ) as ChromeDevToolsMcpExtension;
- events.push(ext);
-
- // Check if any waiters are satisfied
- waitingResolvers = waitingResolvers.filter(
- ({predicate, resolve}) => {
- if (predicate(ext)) {
- resolve(ext);
- return false;
- }
- return true;
- },
- );
- }
- }
- }
- } catch (err) {
- console.error('Failed to parse mock server request', err);
- }
- res.writeHead(200, {'Content-Type': 'application/json'});
- res.end(JSON.stringify({next_request_wait_millis: 100}));
- });
- } else {
- res.writeHead(404);
- res.end();
- }
- });
-
- await new Promise(resolve => {
- server.listen(0, '127.0.0.1', () => resolve());
- });
-
- const address = server.address() as AddressInfo;
- return {
- server,
- port: address.port,
- events,
- get watchdogPid() {
- return watchdogPid;
- },
- waitForEvent: predicate => {
- const existing = events.find(predicate);
- if (existing) {
- return Promise.resolve(existing);
- }
-
- return new Promise(resolve => {
- waitingResolvers.push({predicate, resolve});
- });
- },
- };
-}
-
-interface TestContext {
- process?: ChildProcess;
- mockServer?: MockServerContext;
-}
-
-function isProcessAlive(pid: number): boolean {
- try {
- process.kill(pid, 0);
- return true;
- } catch {
- return false;
- }
-}
-
-async function waitForProcessExit(
- pid: number,
- timeoutMs = 5000,
-): Promise {
- const startTime = Date.now();
- while (Date.now() - startTime < timeoutMs) {
- if (!isProcessAlive(pid)) {
- return;
- }
- await new Promise(resolve => setTimeout(resolve, 50));
- }
- throw new Error(`Timeout waiting for process ${pid} to exit`);
-}
-
-function cleanupTest(ctx: TestContext): void {
- // Kill Main Process
- if (ctx.process && ctx.process.exitCode === null) {
- try {
- ctx.process.kill('SIGKILL');
- } catch {
- // ignore
- }
- }
- // Kill Watchdog Process
- if (ctx.mockServer?.watchdogPid) {
- try {
- process.kill(ctx.mockServer.watchdogPid, 'SIGKILL');
- } catch {
- // ignore
- }
- }
- // Stop Mock Server
- if (ctx.mockServer) {
- ctx.mockServer.server.close();
- }
-}
-
-describe('Telemetry E2E', () => {
- async function runTelemetryTest(
- killFn: (ctx: TestContext) => void,
- spawnOptions?: SpawnOptions,
- ): Promise {
- const mockContext = await startMockServer();
- const ctx: TestContext = {
- mockServer: mockContext,
- };
-
- try {
- ctx.process = spawn(
- process.execPath,
- [
- SERVER_PATH,
- '--usage-statistics',
- '--headless',
- `--clearcutEndpoint=http://127.0.0.1:${mockContext.port}`,
- '--clearcutForceFlushIntervalMs=10',
- '--clearcutIncludePidHeader',
- ],
- {
- stdio: ['pipe', 'pipe', 'pipe'],
- env: {
- ...process.env,
- CI: undefined,
- CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: undefined,
- },
- ...spawnOptions,
- },
- );
-
- const startEvent = await Promise.race([
- mockContext.waitForEvent(e => e.server_start !== undefined),
- new Promise((_, reject) =>
- setTimeout(
- () => reject(new Error('Timeout waiting for server_start')),
- 10000,
- ),
- ),
- ]);
- assert.ok(startEvent, 'server_start event not received');
-
- // Now that we received an event, we should have the Watchdog PID
- const watchdogPid = mockContext.watchdogPid;
- assert.ok(watchdogPid, 'Watchdog PID not captured from headers');
-
- // Assert Watchdog is actually running
- assert.strictEqual(
- isProcessAlive(watchdogPid),
- true,
- 'Watchdog process should be running',
- );
-
- // Trigger shutdown
- killFn(ctx);
-
- // Verify shutdown event
- const shutdownEvent = await Promise.race([
- mockContext.waitForEvent(e => e.server_shutdown !== undefined),
- new Promise((_, reject) =>
- setTimeout(
- () => reject(new Error('Timeout waiting for server_shutdown')),
- 10000,
- ),
- ),
- ]);
- assert.ok(shutdownEvent, 'server_shutdown event not received');
-
- // Wait for Watchdog to exit naturally
- await waitForProcessExit(watchdogPid);
- } finally {
- cleanupTest(ctx);
- }
- }
-
- it('handles SIGKILL', () =>
- runTelemetryTest(ctx => {
- ctx.process!.kill('SIGKILL');
- }));
-
- it('handles SIGTERM', () =>
- runTelemetryTest(ctx => {
- ctx.process!.kill('SIGTERM');
- }));
-
- it(
- 'handles POSIX process group SIGTERM',
- {skip: process.platform === 'win32'},
- () =>
- runTelemetryTest(
- ctx => {
- process.kill(-ctx.process!.pid!, 'SIGTERM');
- },
- {detached: true},
- ),
- );
-});
diff --git a/tests/telemetry/clearcut-logger.test.ts b/tests/telemetry/clearcut-logger.test.ts
deleted file mode 100644
index 2c4912ba4..000000000
--- a/tests/telemetry/clearcut-logger.test.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import assert from 'node:assert';
-import {describe, it, afterEach, beforeEach} from 'node:test';
-
-import sinon from 'sinon';
-
-import {ClearcutLogger} from '../../src/telemetry/clearcut-logger.js';
-import type {Persistence} from '../../src/telemetry/persistence.js';
-import {FilePersistence} from '../../src/telemetry/persistence.js';
-import {WatchdogMessageType} from '../../src/telemetry/types.js';
-import {WatchdogClient} from '../../src/telemetry/watchdog-client.js';
-
-describe('ClearcutLogger', () => {
- let mockPersistence: sinon.SinonStubbedInstance;
- let mockWatchdogClient: sinon.SinonStubbedInstance;
-
- beforeEach(() => {
- mockPersistence = sinon.createStubInstance(FilePersistence, {
- loadState: Promise.resolve({
- lastActive: '',
- }),
- });
- mockWatchdogClient = sinon.createStubInstance(WatchdogClient);
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- describe('logToolInvocation', () => {
- it('sends correct payload', async () => {
- const logger = new ClearcutLogger({
- persistence: mockPersistence,
- appVersion: '1.0.0',
- watchdogClient: mockWatchdogClient,
- });
- await logger.logToolInvocation({
- toolName: 'test_tool',
- success: true,
- latencyMs: 123,
- });
-
- assert(mockWatchdogClient.send.calledOnce);
- const msg = mockWatchdogClient.send.firstCall.args[0];
- assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
- assert.strictEqual(msg.payload.tool_invocation?.tool_name, 'test_tool');
- assert.strictEqual(msg.payload.tool_invocation?.success, true);
- assert.strictEqual(msg.payload.tool_invocation?.latency_ms, 123);
- });
- });
-
- describe('logServerStart', () => {
- it('logs flag usage', async () => {
- const logger = new ClearcutLogger({
- persistence: mockPersistence,
- appVersion: '1.0.0',
- watchdogClient: mockWatchdogClient,
- });
-
- await logger.logServerStart({headless: true});
-
- assert(mockWatchdogClient.send.calledOnce);
- const msg = mockWatchdogClient.send.firstCall.args[0];
- assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
- assert.strictEqual(msg.payload.server_start?.flag_usage?.headless, true);
- });
- });
-
- describe('logDailyActiveIfNeeded', () => {
- it('logs daily active if needed (lastActive > 24h ago)', async () => {
- const yesterday = new Date();
- yesterday.setDate(yesterday.getDate() - 1);
-
- mockPersistence.loadState.resolves({
- lastActive: yesterday.toISOString(),
- });
-
- const logger = new ClearcutLogger({
- persistence: mockPersistence,
- appVersion: '1.0.0',
- watchdogClient: mockWatchdogClient,
- });
-
- await logger.logDailyActiveIfNeeded();
-
- assert(mockWatchdogClient.send.calledOnce);
- const msg = mockWatchdogClient.send.firstCall.args[0];
- assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
- assert.ok(msg.payload.daily_active);
-
- assert(mockPersistence.saveState.called);
- });
-
- it('does not log daily active if not needed (today)', async () => {
- mockPersistence.loadState.resolves({
- lastActive: new Date().toISOString(),
- });
-
- const logger = new ClearcutLogger({
- persistence: mockPersistence,
- appVersion: '1.0.0',
- watchdogClient: mockWatchdogClient,
- });
-
- await logger.logDailyActiveIfNeeded();
-
- assert(mockWatchdogClient.send.notCalled);
- assert(mockPersistence.saveState.notCalled);
- });
-
- it('logs daily active with -1 if lastActive is missing', async () => {
- mockPersistence.loadState.resolves({
- lastActive: '',
- });
-
- const logger = new ClearcutLogger({
- persistence: mockPersistence,
- appVersion: '1.0.0',
- watchdogClient: mockWatchdogClient,
- });
-
- await logger.logDailyActiveIfNeeded();
-
- assert(mockWatchdogClient.send.calledOnce);
- const msg = mockWatchdogClient.send.firstCall.args[0];
- assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
- assert.strictEqual(msg.payload.daily_active?.days_since_last_active, -1);
- assert(mockPersistence.saveState.called);
- });
- });
-});
diff --git a/tests/telemetry/flag-utils.test.ts b/tests/telemetry/flag-utils.test.ts
deleted file mode 100644
index f7105ce14..000000000
--- a/tests/telemetry/flag-utils.test.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import assert from 'node:assert/strict';
-import {describe, it} from 'node:test';
-
-import type {cliOptions} from '../../src/cli.js';
-import {computeFlagUsage} from '../../src/telemetry/flag-utils.js';
-
-describe('computeFlagUsage', () => {
- const mockOptions = {
- boolFlag: {
- type: 'boolean' as const,
- description: 'A boolean flag',
- },
- stringFlag: {
- type: 'string' as const,
- description: 'A string flag',
- },
- enumFlag: {
- type: 'string' as const,
- description: 'An enum flag',
- choices: ['a', 'b'],
- },
- flagWithDefault: {
- type: 'boolean' as const,
- description: 'A flag with a default value',
- default: false,
- },
- } as unknown as typeof cliOptions;
-
- it('logs boolean flags directly with snake_case keys', () => {
- const args = {boolFlag: true};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.bool_flag, true);
- });
-
- it('logs boolean flags as false when false', () => {
- const args = {boolFlag: false};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.bool_flag, false);
- });
-
- it('logs enum flags as uppercase strings prefixed by snake case flag name', () => {
- const args = {enumFlag: 'a'};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.enum_flag, 'ENUM_FLAG_A');
- });
-
- it('logs other flags as present with snake_case keys', () => {
- const args = {stringFlag: 'value'};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.string_flag, undefined);
- assert.equal(usage.string_flag_present, true);
- });
-
- it('handles undefined/null values', () => {
- const args = {stringFlag: undefined};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.string_flag_present, false);
- });
-
- describe('defaults behavior', () => {
- it('logs presence when default exists and user provides different value', () => {
- // Case 1: Default exists, and a value is provided by the user.
- // default is false, user provides true.
- const args = {flagWithDefault: true};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.flag_with_default, true);
- assert.equal(usage.flag_with_default_present, true);
- });
-
- it('does not log presence when default exists and user provides no value', () => {
- // Case 2a: Default exists, and a value is not provided by the user.
- // Argument parsing would populate with default.
- const args = {flagWithDefault: false};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.flag_with_default, false);
- assert.equal(usage.flag_with_default_present, undefined);
- });
-
- it('does not log presence when default exists and user explicitly provides the default value', () => {
- // Case 2b: User explicitly provides 'false', which matches default.
- const args = {flagWithDefault: false};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.flag_with_default, false);
- assert.equal(usage.flag_with_default_present, undefined);
- });
-
- it('logs presence when no default exists and user provides value', () => {
- // Case 3: No default, user provides value.
- const args = {stringFlag: 'value'};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.string_flag_present, true);
- });
-
- it('logs non-presence when no default exists and user provides no value', () => {
- // Case 4: No default, user provides nothing.
- const args = {};
- const usage = computeFlagUsage(args, mockOptions);
- assert.equal(usage.string_flag_present, false);
- });
- });
-});
diff --git a/tests/telemetry/metric-utils.test.ts b/tests/telemetry/metric-utils.test.ts
deleted file mode 100644
index 789594f3c..000000000
--- a/tests/telemetry/metric-utils.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import assert from 'node:assert';
-import {describe, it} from 'node:test';
-
-import {bucketizeLatency} from '../../src/telemetry/metric-utils.js';
-
-describe('bucketizeLatency', () => {
- it('should bucketize values correctly', () => {
- assert.strictEqual(bucketizeLatency(0), 50);
- assert.strictEqual(bucketizeLatency(25), 50);
- assert.strictEqual(bucketizeLatency(50), 50);
-
- assert.strictEqual(bucketizeLatency(51), 100);
- assert.strictEqual(bucketizeLatency(100), 100);
-
- assert.strictEqual(bucketizeLatency(101), 250);
- assert.strictEqual(bucketizeLatency(250), 250);
-
- assert.strictEqual(bucketizeLatency(499), 500);
- assert.strictEqual(bucketizeLatency(500), 500);
-
- assert.strictEqual(bucketizeLatency(900), 1000);
- assert.strictEqual(bucketizeLatency(1000), 1000);
-
- assert.strictEqual(bucketizeLatency(2000), 2500);
- assert.strictEqual(bucketizeLatency(2500), 2500);
-
- assert.strictEqual(bucketizeLatency(4000), 5000);
- assert.strictEqual(bucketizeLatency(5000), 5000);
-
- assert.strictEqual(bucketizeLatency(6000), 10000);
- assert.strictEqual(bucketizeLatency(10000), 10000);
-
- assert.strictEqual(bucketizeLatency(10001), 10000);
- assert.strictEqual(bucketizeLatency(99999), 10000);
- });
-});
diff --git a/tests/telemetry/persistence.test.ts b/tests/telemetry/persistence.test.ts
deleted file mode 100644
index 9bacada9a..000000000
--- a/tests/telemetry/persistence.test.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import assert from 'node:assert';
-import crypto from 'node:crypto';
-import fs from 'node:fs/promises';
-import os from 'node:os';
-import path from 'node:path';
-import {describe, it, afterEach, beforeEach} from 'node:test';
-
-import * as persistence from '../../src/telemetry/persistence.js';
-
-describe('FilePersistence', () => {
- let tmpDir: string;
-
- beforeEach(async () => {
- tmpDir = path.join(
- await fs.realpath(os.tmpdir()),
- `telemetry-test-${crypto.randomUUID()}`,
- );
- await fs.mkdir(tmpDir, {recursive: true});
- });
-
- afterEach(async () => {
- await fs.rm(tmpDir, {recursive: true, force: true});
- });
-
- describe('loadState', () => {
- it('returns default state if file does not exist', async () => {
- const filePersistence = new persistence.FilePersistence(tmpDir);
- const state = await filePersistence.loadState();
- assert.deepStrictEqual(state, {
- lastActive: '',
- });
- });
-
- it('returns stored state if file exists', async () => {
- const expectedState = {
- lastActive: '2023-01-01T00:00:00.000Z',
- };
- await fs.writeFile(
- path.join(tmpDir, 'telemetry_state.json'),
- JSON.stringify(expectedState),
- );
-
- const filePersistence = new persistence.FilePersistence(tmpDir);
- const state = await filePersistence.loadState();
- assert.deepStrictEqual(state, expectedState);
- });
- });
-
- describe('saveState', () => {
- it('saves state to file', async () => {
- const state = {
- lastActive: '2023-01-01T00:00:00.000Z',
- };
- const filePersistence = new persistence.FilePersistence(tmpDir);
- await filePersistence.saveState(state);
-
- const content = await fs.readFile(
- path.join(tmpDir, 'telemetry_state.json'),
- 'utf-8',
- );
- assert.deepStrictEqual(JSON.parse(content), state);
- });
- });
-});
diff --git a/tests/telemetry/watchdog-client.test.ts b/tests/telemetry/watchdog-client.test.ts
deleted file mode 100644
index 258d3cac6..000000000
--- a/tests/telemetry/watchdog-client.test.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import assert from 'node:assert';
-import {ChildProcess} from 'node:child_process';
-import {Writable} from 'node:stream';
-import {describe, it, afterEach, beforeEach} from 'node:test';
-
-import sinon from 'sinon';
-
-import {OsType, WatchdogMessageType} from '../../src/telemetry/types.js';
-import {WatchdogClient} from '../../src/telemetry/watchdog-client.js';
-
-describe('WatchdogClient', () => {
- let spawnStub: sinon.SinonStub;
- let stdinStub: sinon.SinonStubbedInstance;
- let mockChildProcess: sinon.SinonStubbedInstance;
-
- beforeEach(() => {
- stdinStub = sinon.createStubInstance(Writable);
- mockChildProcess = sinon.createStubInstance(ChildProcess);
- spawnStub = sinon.stub().returns(mockChildProcess);
-
- Object.defineProperty(mockChildProcess, 'stdin', {
- value: stdinStub,
- writable: true,
- });
- Object.defineProperty(mockChildProcess, 'pid', {
- value: 12345,
- writable: true,
- });
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it('spawns watchdog process with correct arguments', () => {
- new WatchdogClient(
- {
- parentPid: 100,
- appVersion: '1.2.3',
- osType: OsType.OS_TYPE_MACOS,
- },
- {spawn: spawnStub},
- );
-
- assert.ok(spawnStub.calledOnce, 'Expected `spawn` to be called');
- const args = spawnStub.firstCall.args;
- const cmdArgs = args[1];
-
- assert.match(
- cmdArgs[0],
- /watchdog[/\\]main\.js$/,
- 'First argument should be path to watchdog/main.js',
- );
- assert.ok(
- cmdArgs.includes('--parent-pid=100'),
- 'Arguments should include parent PID',
- );
- assert.ok(
- cmdArgs.includes('--app-version=1.2.3'),
- 'Arguments should include app version',
- );
- assert.ok(
- cmdArgs.includes('--os-type=2'),
- 'Arguments should include OS type',
- );
- assert.strictEqual(
- spawnStub.firstCall.args[2].detached,
- true,
- 'Process should be spawned as detached',
- );
- });
-
- it('passes log-file argument if provided', () => {
- new WatchdogClient(
- {
- parentPid: 100,
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_LINUX,
- logFile: '/tmp/test.log',
- },
- {spawn: spawnStub},
- );
-
- const cmdArgs = spawnStub.firstCall.args[1];
- assert.ok(
- cmdArgs.includes('--log-file=/tmp/test.log'),
- 'Arguments should include log file path',
- );
- });
-
- it('sends IPC messages via stdin', () => {
- const client = new WatchdogClient(
- {
- parentPid: 100,
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_LINUX,
- },
- {spawn: spawnStub},
- );
-
- const msg = {type: WatchdogMessageType.LOG_EVENT, payload: {}};
- client.send(msg);
-
- assert.ok(
- stdinStub.write.calledOnce,
- 'Expected `stdin.write` to be called',
- );
-
- const writtenData = stdinStub.write.firstCall.args[0];
- assert.strictEqual(
- writtenData.trim(),
- JSON.stringify(msg),
- 'Written data should match expected JSON message',
- );
- });
-
- it('handles write errors gracefully', () => {
- const client = new WatchdogClient(
- {
- parentPid: 100,
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_LINUX,
- },
- {spawn: spawnStub},
- );
-
- stdinStub.write.throws(new Error('EPIPE'));
-
- assert.doesNotThrow(() => {
- client.send({type: WatchdogMessageType.LOG_EVENT, payload: {}});
- }, 'Client should catch and ignore write errors');
- });
-});
diff --git a/tests/telemetry/watchdog/clearcut-sender.test.ts b/tests/telemetry/watchdog/clearcut-sender.test.ts
deleted file mode 100644
index 8eca6a7f9..000000000
--- a/tests/telemetry/watchdog/clearcut-sender.test.ts
+++ /dev/null
@@ -1,451 +0,0 @@
-/**
- * @license
- * Copyright 2026 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import assert from 'node:assert';
-import crypto from 'node:crypto';
-import {describe, it, afterEach, beforeEach} from 'node:test';
-
-import sinon from 'sinon';
-
-import {OsType} from '../../../src/telemetry/types.js';
-import type {LogRequest} from '../../../src/telemetry/types.js';
-import {ClearcutSender} from '../../../src/telemetry/watchdog/clearcut-sender.js';
-
-const FLUSH_INTERVAL_MS = 15 * 1000;
-
-describe('ClearcutSender', () => {
- let randomUUIDStub: sinon.SinonStub;
- let fetchStub: sinon.SinonStub;
- let clock: sinon.SinonFakeTimers;
-
- beforeEach(() => {
- clock = sinon.useFakeTimers({
- now: Date.now(),
- toFake: ['setTimeout', 'clearTimeout', 'Date'],
- });
-
- let uuidCounter = 0;
- randomUUIDStub = sinon.stub(crypto, 'randomUUID').callsFake(() => {
- return `uuid-${++uuidCounter}` as ReturnType;
- });
- fetchStub = sinon.stub(global, 'fetch');
- fetchStub.resolves(new Response(JSON.stringify({}), {status: 200}));
- });
-
- afterEach(() => {
- randomUUIDStub.restore();
- fetchStub.restore();
- clock.restore();
- sinon.restore();
- });
-
- it('enriches events with app version, os type, and session id', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
-
- sender.enqueueEvent({mcp_client: undefined});
- assert.strictEqual(sender.bufferSizeForTesting, 1);
-
- await clock.tickAsync(FLUSH_INTERVAL_MS);
- sender.stopForTesting();
-
- assert.strictEqual(fetchStub.callCount, 1);
- const requestBody = JSON.parse(
- fetchStub.firstCall.args[1].body,
- ) as LogRequest;
- const event = JSON.parse(requestBody.log_event[0].source_extension_json);
-
- assert.strictEqual(event.session_id, 'uuid-1');
- assert.strictEqual(event.app_version, '1.0.0');
- assert.strictEqual(event.os_type, OsType.OS_TYPE_MACOS);
- });
-
- it('accumulates events in buffer without immediate send', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
-
- sender.enqueueEvent({
- tool_invocation: {tool_name: 'test1', success: true, latency_ms: 100},
- });
- sender.enqueueEvent({
- tool_invocation: {tool_name: 'test2', success: true, latency_ms: 200},
- });
-
- assert.strictEqual(sender.bufferSizeForTesting, 2);
- assert.strictEqual(fetchStub.callCount, 0);
-
- sender.stopForTesting();
- });
-
- it('sends correct LogRequest format', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
-
- sender.enqueueEvent({
- tool_invocation: {tool_name: 'test', success: true, latency_ms: 100},
- });
-
- await clock.tickAsync(FLUSH_INTERVAL_MS);
- sender.stopForTesting();
-
- const [url, options] = fetchStub.firstCall.args;
- assert.strictEqual(
- url,
- 'https://play.googleapis.com/log?format=json_proto',
- );
- assert.strictEqual(options.method, 'POST');
- assert.strictEqual(options.headers['Content-Type'], 'application/json');
-
- const body = JSON.parse(options.body) as LogRequest;
- assert.strictEqual(body.log_source, 2839);
- assert.strictEqual(body.client_info.client_type, 47);
- assert.ok(body.request_time_ms);
- });
-
- it('clears buffer on successful send', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
-
- sender.enqueueEvent({});
- sender.enqueueEvent({});
- assert.strictEqual(sender.bufferSizeForTesting, 2);
-
- await clock.tickAsync(FLUSH_INTERVAL_MS);
- sender.stopForTesting();
- assert.strictEqual(sender.bufferSizeForTesting, 0);
- });
-
- it('keeps events in buffer on transient 5xx error', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
- fetchStub.resolves(new Response('Server Error', {status: 500}));
-
- sender.enqueueEvent({});
- await clock.tickAsync(FLUSH_INTERVAL_MS);
- sender.stopForTesting();
-
- assert.strictEqual(sender.bufferSizeForTesting, 1);
- });
-
- it('keeps events in buffer on transient 429 error', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
- fetchStub.resolves(new Response('Too Many Requests', {status: 429}));
-
- sender.enqueueEvent({});
- await clock.tickAsync(FLUSH_INTERVAL_MS);
- sender.stopForTesting();
-
- assert.strictEqual(sender.bufferSizeForTesting, 1);
- });
-
- it('drops batch on permanent 4xx error', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
- fetchStub.resolves(new Response('Bad Request', {status: 400}));
-
- sender.enqueueEvent({});
- await clock.tickAsync(FLUSH_INTERVAL_MS);
- sender.stopForTesting();
-
- assert.strictEqual(sender.bufferSizeForTesting, 0);
- });
-
- it('keeps events in buffer on network error', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
- fetchStub.rejects(new Error('Network error'));
-
- sender.enqueueEvent({});
- await clock.tickAsync(FLUSH_INTERVAL_MS);
- sender.stopForTesting();
-
- assert.strictEqual(sender.bufferSizeForTesting, 1);
- });
-
- it('sendShutdownEvent sends an immediate server_shutdown event', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
-
- await sender.sendShutdownEvent();
-
- assert.strictEqual(fetchStub.callCount, 1);
- const requestBody = JSON.parse(
- fetchStub.firstCall.args[1].body,
- ) as LogRequest;
- const event = JSON.parse(requestBody.log_event[0].source_extension_json);
-
- assert.ok(event.server_shutdown);
- });
-
- it('shutdown includes buffered events', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
-
- sender.enqueueEvent({
- tool_invocation: {tool_name: 'test', success: true, latency_ms: 100},
- });
- await sender.sendShutdownEvent();
-
- const requestBody = JSON.parse(
- fetchStub.firstCall.args[1].body,
- ) as LogRequest;
- assert.strictEqual(requestBody.log_event.length, 2);
- });
-
- it('correctly handles buffer overflow during queued flush', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
-
- sender.enqueueEvent({
- tool_invocation: {tool_name: 'initial', success: true, latency_ms: 100},
- });
- let resolveRequest: (value: Response) => void;
-
- fetchStub.onFirstCall().returns(
- new Promise(resolve => {
- resolveRequest = resolve;
- }),
- );
-
- clock.tick(FLUSH_INTERVAL_MS);
-
- for (let i = 0; i < 1100; i++) {
- sender.enqueueEvent({
- tool_invocation: {
- tool_name: `overflow-${i}`,
- success: true,
- latency_ms: 100,
- },
- });
- }
-
- assert.strictEqual(sender.bufferSizeForTesting, 1000);
-
- resolveRequest!(new Response(JSON.stringify({}), {status: 200}));
-
- assert.strictEqual(sender.bufferSizeForTesting, 1000);
-
- sender.stopForTesting();
- });
-
- it('does not duplicate events when shutdown occurs during an active flush', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
- sender.enqueueEvent({
- tool_invocation: {
- tool_name: 'test-event',
- success: true,
- latency_ms: 100,
- },
- });
-
- let resolveFirstRequest: (value: Response) => void;
- fetchStub.onFirstCall().returns(
- new Promise(resolve => {
- resolveFirstRequest = resolve;
- }),
- );
-
- clock.tick(FLUSH_INTERVAL_MS);
-
- const shutdownPromise = sender.sendShutdownEvent();
-
- resolveFirstRequest!(new Response(JSON.stringify({}), {status: 200}));
- await shutdownPromise;
-
- assert.strictEqual(fetchStub.callCount, 2);
- const firstBody = JSON.parse(fetchStub.args[0][1].body) as LogRequest;
- const secondBody = JSON.parse(fetchStub.args[1][1].body) as LogRequest;
-
- const firstEvents = firstBody.log_event.map(e =>
- JSON.parse(e.source_extension_json),
- );
- const secondEvents = secondBody.log_event.map(e =>
- JSON.parse(e.source_extension_json),
- );
-
- assert.strictEqual(firstEvents.length, 1);
- assert.strictEqual(firstEvents[0].tool_invocation?.tool_name, 'test-event');
-
- assert.strictEqual(
- secondEvents.length,
- 1,
- 'Shutdown request should only contain shutdown event',
- );
- assert.ok(
- secondEvents[0].server_shutdown,
- 'Shutdown request should contain server_shutdown',
- );
-
- sender.stopForTesting();
- });
-
- it('rotates session id after 24 hours', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
-
- sender.enqueueEvent({
- tool_invocation: {tool_name: 'test1', success: true, latency_ms: 10},
- });
- await clock.tickAsync(FLUSH_INTERVAL_MS);
-
- const firstCallBody = JSON.parse(
- fetchStub.firstCall.args[1].body,
- ) as LogRequest;
- const firstEvent = JSON.parse(
- firstCallBody.log_event[0].source_extension_json,
- );
- const firstSessionId = firstEvent.session_id;
-
- const SESSION_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
- await clock.tickAsync(
- SESSION_ROTATION_INTERVAL_MS - FLUSH_INTERVAL_MS + 1000,
- );
-
- sender.enqueueEvent({
- tool_invocation: {tool_name: 'test2', success: true, latency_ms: 10},
- });
- await clock.tickAsync(FLUSH_INTERVAL_MS);
-
- const secondCallBody = JSON.parse(
- fetchStub.secondCall.args[1].body,
- ) as LogRequest;
- const secondEvent = JSON.parse(
- secondCallBody.log_event[0].source_extension_json,
- );
- const secondSessionId = secondEvent.session_id;
-
- assert.notStrictEqual(firstSessionId, secondSessionId);
-
- sender.stopForTesting();
- });
-
- it('respects next_request_wait_millis from server', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
-
- fetchStub.resolves(
- new Response(
- JSON.stringify({
- next_request_wait_millis: 45000,
- }),
- {status: 200},
- ),
- );
-
- sender.enqueueEvent({});
- await clock.tickAsync(FLUSH_INTERVAL_MS);
-
- fetchStub.resetHistory();
-
- sender.enqueueEvent({});
-
- await clock.tickAsync(44000);
- assert.strictEqual(
- fetchStub.callCount,
- 0,
- 'Should not flush before wait time',
- );
-
- await clock.tickAsync(1000);
- assert.strictEqual(fetchStub.callCount, 1, 'Should flush after wait time');
-
- sender.stopForTesting();
- });
-
- it('aborts request after timeout', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
- const REQUEST_TIMEOUT_MS = 30000;
-
- let fetchSignal: AbortSignal | undefined;
- fetchStub.callsFake((_url, options) => {
- fetchSignal = options.signal;
- return new Promise(() => {
- // Hangs forever
- });
- });
-
- sender.enqueueEvent({});
-
- await clock.tickAsync(FLUSH_INTERVAL_MS);
- await clock.tickAsync(REQUEST_TIMEOUT_MS);
-
- assert.ok(fetchSignal, 'Fetch should have been called with a signal');
- assert.strictEqual(
- fetchSignal.aborted,
- true,
- 'Signal should be aborted after timeout',
- );
-
- sender.stopForTesting();
- });
-
- it('resolves sendShutdownEvent after timeout if flush hangs', async () => {
- const sender = new ClearcutSender({
- appVersion: '1.0.0',
- osType: OsType.OS_TYPE_MACOS,
- forceFlushIntervalMs: FLUSH_INTERVAL_MS,
- });
- fetchStub.returns(
- new Promise(() => {
- // Hangs forever
- }),
- );
-
- const shutdownPromise = sender.sendShutdownEvent();
-
- await clock.tickAsync(5000);
-
- await shutdownPromise;
- });
-});