@raultov/browser-control-mcp 0.0.0-stage → 0.1.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitignore +2 -0
- package/CHANGELOG.md +215 -0
- package/LICENSE +21 -0
- package/README.md +466 -2
- package/binary-install.js +348 -0
- package/binary.js +124 -0
- package/install.js +4 -0
- package/npm-shrinkwrap.json +52 -0
- package/package.json +97 -3
- package/run-browser-control-mcp.js +4 -0
package/.gitignore
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `browser-control-mcp` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
`browser-control-mcp` is the successor of `chrome-debug-mcp` 1.5.6 and starts
|
|
9
|
+
from a copy of its code: entries describe the differences from that release.
|
|
10
|
+
|
|
11
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
## [0.1.1-beta.1] - 2026-10-12
|
|
14
|
+
|
|
15
|
+
Public **BETA**: known bugs and Chrome/Firefox differences are listed in the
|
|
16
|
+
README ("Limitations and TODO"). The GitHub Release, the shell/PowerShell
|
|
17
|
+
installers, the MSI and the npm package start with this version (0.1.0 was
|
|
18
|
+
only published to crates.io).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- README: per-platform installation section (shell and PowerShell
|
|
22
|
+
installers, npm/npx, Homebrew, MSI, Cargo/binstall, manual download,
|
|
23
|
+
Docker, source) and a one-prompt install for AI agents.
|
|
24
|
+
- Releases of beta versions are published as the latest GitHub Release and
|
|
25
|
+
with the npm `latest` tag, so `releases/latest/download/...` installers
|
|
26
|
+
and `npm install -g @raultov/browser-control-mcp` get them.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- A Windows-only unit test failure (path separator in the cookie-import
|
|
30
|
+
error assertion).
|
|
31
|
+
|
|
32
|
+
## [0.1.0] - 2026-10-12
|
|
33
|
+
|
|
34
|
+
First public release, **BETA**: known bugs and Chrome/Firefox differences are
|
|
35
|
+
listed in the README ("Limitations and TODO").
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- When Chrome is not installed and a Firefox is, the launch error says so
|
|
39
|
+
and tells the agent to ask the user before using `"browser": "firefox"`.
|
|
40
|
+
- E2E smoke over stdio MCP against real, visible Chrome and Firefox
|
|
41
|
+
(`make e2e`: launched and attach modes, SIGTERM and relaunch), with
|
|
42
|
+
`tests/chrome.sh` and `tests/firefox.sh` for dedicated test browsers.
|
|
43
|
+
- Project scaffolding: quality gates (`make check`: format, clippy with
|
|
44
|
+
warnings as errors, tests, docs, duplication, packaging and version
|
|
45
|
+
consistency), CI on Linux, macOS and Windows, `AGENTS.md`, MIT licence.
|
|
46
|
+
- The server code of chrome-debug-mcp 1.5.6 (35 tools, Chrome over CDP),
|
|
47
|
+
copied without its history and renamed: crate and binary
|
|
48
|
+
`browser-control-mcp`, server title "Browser Control MCP".
|
|
49
|
+
|
|
50
|
+
- `--browser <chrome|firefox>` selects the browser of the default instance
|
|
51
|
+
and `open_instance` takes a `browser` argument (Firefox lands in later
|
|
52
|
+
tasks; until then a Firefox instance answers "not implemented yet").
|
|
53
|
+
- Every tool declares what it needs; a tool an instance's browser cannot
|
|
54
|
+
serve fails with an error naming the tool and the browser, before anything
|
|
55
|
+
is sent. `list_instances` shows each instance's `browser` and
|
|
56
|
+
`capabilities`.
|
|
57
|
+
- On exit (end of stdin, Ctrl-C, SIGTERM on Unix) the server stops every
|
|
58
|
+
browser it started; the exit code reports instances that did not stop.
|
|
59
|
+
|
|
60
|
+
- **Firefox support** over WebDriver BiDi (`--browser firefox`, or
|
|
61
|
+
`open_instance` with `browser: "firefox"`): tabs, `navigate`, `reload`,
|
|
62
|
+
`evaluate_js` (values as plain JSON), `inspect_dom`, `scroll`,
|
|
63
|
+
`click_element` (real pointer input), `fill_input` (real key input),
|
|
64
|
+
`capture_screenshot`, `get_console_logs`, `get_network_logs` (with response
|
|
65
|
+
bodies; no WebSocket frames) and `get_custom_events`; instance tools
|
|
66
|
+
(`open_instance`, `close_instance`, `restart_browser`, `stop_browser`,
|
|
67
|
+
`list_instances` with the Firefox version). Events of iframes are kept with
|
|
68
|
+
their tab. Firefox instances are launched with an ephemeral profile, or
|
|
69
|
+
attached with `--firefox-port`.
|
|
70
|
+
- **JavaScript debugging on Firefox** over the Firefox Remote Debugging
|
|
71
|
+
Protocol: `set_breakpoint` (by `url`), `remove_breakpoint`, `resume`,
|
|
72
|
+
`step_over` (reports the new location), `evaluate_on_call_frame` and
|
|
73
|
+
`search_scripts`. The debugger port is on by default for launched Firefox
|
|
74
|
+
instances; `--no-firefox-debugger` turns it off. `pause_on_load` is Chrome
|
|
75
|
+
only for now.
|
|
76
|
+
- Firefox flags: `--firefox-path`, `--firefox-port`, `--firefox-rdp-port`,
|
|
77
|
+
`--no-firefox-debugger`. `--enable-automation` is reported as ignored on
|
|
78
|
+
Firefox instances by `list_instances`.
|
|
79
|
+
|
|
80
|
+
- `send_bidi_command` (raw WebDriver BiDi; refuses `session.new`,
|
|
81
|
+
`session.end`, `browser.close` and removing the server's own subscription;
|
|
82
|
+
its `session.subscribe` events feed `get_custom_events`) and
|
|
83
|
+
`send_rdp_request` (raw Firefox RDP, with `thread`/`console`/`root`
|
|
84
|
+
shortcuts). Firefox only.
|
|
85
|
+
- `enable_proxy_auth` on Firefox (`network.authRequired` →
|
|
86
|
+
`network.continueWithAuth`, credentials only for 407). **Untested against a
|
|
87
|
+
real authenticating proxy.**
|
|
88
|
+
|
|
89
|
+
- Distribution: cargo-dist release workflow (shell, PowerShell, npm
|
|
90
|
+
`@raultov/browser-control-mcp`, Homebrew, MSI with its own GUIDs) behind a
|
|
91
|
+
preflight that refuses a tag not matching `Cargo.toml`/`package.json` or
|
|
92
|
+
without a CHANGELOG section; release notes are that section. Docker image
|
|
93
|
+
with Google Chrome and Firefox (amd64). glama.json exposes `--browser`.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
- `cdp-browser-lite`'s test mocks are no longer linked into the binary.
|
|
97
|
+
- Dependencies updated to their latest releases; unused test dependencies
|
|
98
|
+
(`hyper`, `hyper-util`, `http-body-util`, `futures-util`,
|
|
99
|
+
`tokio-tungstenite`) removed.
|
|
100
|
+
- `rust-mcp-sdk` stays on 1.x (MCP 2025-11-25) with only its `server`,
|
|
101
|
+
`macros` and `stdio` features: the binary no longer links the SDK's HTTP,
|
|
102
|
+
SSE and OAuth client stack.
|
|
103
|
+
|
|
104
|
+
- `navigate`, `reload`, `evaluate_js`, `evaluate_on_call_frame`,
|
|
105
|
+
`set_breakpoint`, `remove_breakpoint` and `send_cdp_command` return the
|
|
106
|
+
protocol reply as pretty JSON after their usual prefix, instead of the Rust
|
|
107
|
+
`Debug` of the whole message (`WsResponse { .. }`).
|
|
108
|
+
|
|
109
|
+
- **Renamed tools:** `restart_chrome` → `restart_browser`, `stop_chrome` →
|
|
110
|
+
`stop_browser`. Calling an old name returns an error naming the new one.
|
|
111
|
+
- Generated instance ids carry the browser name (`chrome-2`, `firefox-3`).
|
|
112
|
+
- Server title "Browser Control MCP"; new description and instructions that
|
|
113
|
+
cover both browsers.
|
|
114
|
+
|
|
115
|
+
- Tool descriptions say which tools are Chrome only and no longer mention
|
|
116
|
+
tools that do not exist (`pause_on_exceptions`, `step_out`);
|
|
117
|
+
`remove_breakpoint` no longer claims the page must be paused.
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
- `stop_browser` / `restart_browser` on Chrome could deadlock with a tool
|
|
121
|
+
making the instance's first connection (two locks taken in opposite order).
|
|
122
|
+
- Chrome `open_instance` checks the instance limit and the label before
|
|
123
|
+
launching: a refused instance no longer leaves an untracked Chrome running.
|
|
124
|
+
- No instance can take the id `default` (it hijacked the default instance).
|
|
125
|
+
- `open_tab` checks the tab limit and the label before creating the page,
|
|
126
|
+
keeps labels unique when target discovery registered the page first, and
|
|
127
|
+
closes the page if it still cannot register it (Chrome and Firefox).
|
|
128
|
+
- `close_tab` forgets a tab only once the browser closed it (Chrome and
|
|
129
|
+
Firefox).
|
|
130
|
+
- Firefox: a lost WebSocket no longer leaves a dead event pump (empty console
|
|
131
|
+
and network logs): the next tool starts a new session.
|
|
132
|
+
- Firefox: tab ids are not reused after `restart_browser`; the iframe map
|
|
133
|
+
forgets a destroyed context's whole subtree; debuggers of tabs closed by the
|
|
134
|
+
page are dropped.
|
|
135
|
+
- Firefox `enable_proxy_auth` subscribes to `network.authRequired` before the
|
|
136
|
+
intercept exists, so no challenge is missed (and lags are logged).
|
|
137
|
+
- Firefox `set_breakpoint` with `line_number` 4294967295 is an argument error
|
|
138
|
+
instead of an overflow.
|
|
139
|
+
- Shutdown: tool calls are refused once the server is stopping, and a
|
|
140
|
+
debugger tool in flight no longer delays stopping Firefox past its budget.
|
|
141
|
+
- The server exits on SIGTERM/Ctrl-C: it used to stop its browsers and then
|
|
142
|
+
hang, waiting for a stdin read that only returns on input.
|
|
143
|
+
- Chrome `profile_page_performance` no longer times out at random on tabs
|
|
144
|
+
opened with `open_tab`: when target discovery and `open_tab` both attached
|
|
145
|
+
the page, the second registration replaced the tab's listeners with ones on
|
|
146
|
+
a session nobody sent commands to.
|
|
147
|
+
- Chrome `open_tab` with a URL records the page's first requests: the tab is
|
|
148
|
+
created blank and navigated once its domains are enabled.
|
|
149
|
+
- `profile_page_performance` reports a failed `Tracing.end` instead of a
|
|
150
|
+
timeout.
|
|
151
|
+
- `reload` right after `navigate` on Chrome no longer fails with "Not attached
|
|
152
|
+
to an active page": the transient error is retried once after 500 ms.
|
|
153
|
+
- `inspect_dom` no longer crashes the server when the context window around a
|
|
154
|
+
match falls inside a multi-byte character.
|
|
155
|
+
|
|
156
|
+
- The server can no longer panic on a poisoned lock or an unexpected value:
|
|
157
|
+
every `unwrap`/`expect` outside tests was removed; the instance registry
|
|
158
|
+
uses a single lock (two locks taken in different orders could deadlock).
|
|
159
|
+
- Custom-domain listeners started by `send_cdp_command` on a tab are stopped
|
|
160
|
+
when the tab closes (a reference cycle kept them running forever), and no
|
|
161
|
+
longer leak one static string per domain name.
|
|
162
|
+
- Per-tab state fed by the browser is bounded: at most 2,000 parsed scripts,
|
|
163
|
+
100 WebSocket connections (with their frames) and 1,000 WebMCP
|
|
164
|
+
invocations; the oldest are forgotten first.
|
|
165
|
+
|
|
166
|
+
- `search_scripts` / script cache: Chrome's numeric `startLine`/`startColumn`
|
|
167
|
+
were read as strings and always stored as 0.
|
|
168
|
+
- `evaluate_on_call_frame` no longer uses a stale frame after the page
|
|
169
|
+
resumed (`Debugger.resumed` clears it).
|
|
170
|
+
- `pause_on_load` pauses once: the injected `debugger;` script is removed on
|
|
171
|
+
the next `resume` instead of pausing every later navigation.
|
|
172
|
+
- `get_custom_events` with `limit: 0` returns every event (it returned none).
|
|
173
|
+
- `get_network_logs`: a redirect keeps one entry per request id, with the
|
|
174
|
+
final URL and a `redirects` list.
|
|
175
|
+
- `profile_page_performance` restores the cache and its completion channel
|
|
176
|
+
when tracing fails or times out, and keeps the top 5 long tasks its
|
|
177
|
+
description promises (it kept 10).
|
|
178
|
+
- `fill_input` replaces the field's content, as its description says.
|
|
179
|
+
- Closing the active tab activates the most recently used tab (it picked an
|
|
180
|
+
arbitrary one); `list_tabs` reports each tab's current URL; a tab opened
|
|
181
|
+
with `open_tab` can no longer be registered twice.
|
|
182
|
+
- The session-level WebMCP listener starts before `WebMCP.enable`, so the
|
|
183
|
+
initial tool list is not missed.
|
|
184
|
+
|
|
185
|
+
- A Firefox launched by the server no longer starts on `about:home`, where
|
|
186
|
+
BiDi refuses page scripts: its first tab is moved to `about:blank`.
|
|
187
|
+
|
|
188
|
+
### Security
|
|
189
|
+
- `--local` treats `192.168.x.x` only as an IP address: a domain name such
|
|
190
|
+
as `192.168.attacker.com` is no longer accepted as local.
|
|
191
|
+
- `--local` also covers RDP `navigateTo` in `send_rdp_request`.
|
|
192
|
+
- `send_bidi_command` refuses `session.unsubscribe` of the events the server
|
|
193
|
+
listens to (by event or module name), not only of its subscription id.
|
|
194
|
+
- The start-up log no longer prints the proxy password, username or a
|
|
195
|
+
`user:pass@` embedded in `--proxy-server` (chrome-debug-mcp printed every
|
|
196
|
+
argument). Logs now go through `tracing` to stderr (`RUST_LOG`, default
|
|
197
|
+
`info`) and carry ids, method names and error kinds, never payloads;
|
|
198
|
+
`cdp-lite`'s payload logging is capped at `info` whatever `RUST_LOG` says.
|
|
199
|
+
- `enable_proxy_auth` answers only proxy (407) challenges; a site's 401 gets
|
|
200
|
+
the browser's default handling instead of the proxy credentials.
|
|
201
|
+
- `enable_proxy_auth` checks the credentials and `--local` (pre-warm URL)
|
|
202
|
+
before enabling Fetch, so a refused call no longer leaves every request
|
|
203
|
+
paused; enabling it again replaces the previous handler instead of stacking
|
|
204
|
+
a second one; the pre-warm URL is logged as scheme and host only.
|
|
205
|
+
- `scroll`, `click_element` and `fill_input` embed the CSS selector as a JSON
|
|
206
|
+
string literal; a selector with a backslash could break out of the
|
|
207
|
+
JavaScript string before.
|
|
208
|
+
- `--local` also blocks `Target.createTarget` sent through `send_cdp_command`
|
|
209
|
+
(only `Page.navigate` was checked).
|
|
210
|
+
- Cookie import refuses a `source_profile` that is not a single profile
|
|
211
|
+
directory name (`../..` could read outside the browser's user-data root).
|
|
212
|
+
- `get_network_logs` returns `Authorization`, `Proxy-Authorization`, `Cookie`
|
|
213
|
+
and `Set-Cookie` header values as `<redacted>` unless the new
|
|
214
|
+
`include_sensitive_headers: true` is passed.
|
|
215
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 raultov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,467 @@
|
|
|
1
|
-
#
|
|
1
|
+
# browser-control-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Browser Control MCP** lets an AI agent control **and debug** real browsers
|
|
4
|
+
at runtime: breakpoints, pause on load, call frames, evaluation in a paused
|
|
5
|
+
frame, stepping and script search on **Chrome (CDP)** and **Firefox
|
|
6
|
+
(WebDriver BiDi + Firefox RDP)**, plus navigation, DOM, input, screenshots,
|
|
7
|
+
console and network capture.
|
|
8
|
+
|
|
9
|
+
It is the successor of
|
|
10
|
+
[chrome-debug-mcp](https://github.com/raultov/chrome-debug-mcp) (it starts
|
|
11
|
+
from a copy of its code) and complements Playwright MCP or
|
|
12
|
+
chrome-devtools-mcp with runtime JavaScript debugging on both browsers.
|
|
13
|
+
|
|
14
|
+
> **Status: BETA (0.1.1-beta.1).** This version contains known bugs and differences
|
|
15
|
+
> between Chrome and Firefox (see [Limitations and TODO](#limitations-and-todo))
|
|
16
|
+
> and parts that have not been verified (see [Untested](#untested)). Tool
|
|
17
|
+
> replies may change in incompatible ways before 1.0. Please report problems
|
|
18
|
+
> in the [issues](https://github.com/raultov/browser-control-mcp/issues).
|
|
19
|
+
|
|
20
|
+
## Supported browsers
|
|
21
|
+
|
|
22
|
+
| Browser | Protocol | Validated with |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Chrome / Chromium | Chrome DevTools Protocol | Google Chrome 155.0.8059.39 (Linux) |
|
|
25
|
+
| Firefox | WebDriver BiDi, plus the Firefox Remote Debugging Protocol (RDP) for the JavaScript debugger | Firefox 157.0.1 (Linux, snap) |
|
|
26
|
+
|
|
27
|
+
The default browser is Chrome. Every instance runs one browser for its whole
|
|
28
|
+
life: start the server with `--browser firefox`, or pass
|
|
29
|
+
`"browser": "firefox"` to `open_instance`. If Chrome is not installed but Firefox
|
|
30
|
+
is, the launch error says so and tells the agent to ask you before switching.
|
|
31
|
+
|
|
32
|
+
## Feature matrix
|
|
33
|
+
|
|
34
|
+
✓ supported, ✗ not available (the tool answers a capability error naming the
|
|
35
|
+
tool, the browser and the reason; nothing is sent to the browser), partial:
|
|
36
|
+
see the note.
|
|
37
|
+
|
|
38
|
+
| Tool | Chrome | Firefox | Notes |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| `open_instance` | ✓ | partial | `features`, `copy_cookies` and `source_profile` are Chrome only |
|
|
41
|
+
| `close_instance` | ✓ | ✓ | |
|
|
42
|
+
| `list_instances` | ✓ | ✓ | each instance reports its `browser` and `capabilities` |
|
|
43
|
+
| `restart_browser` | ✓ | ✓ | was `restart_chrome` |
|
|
44
|
+
| `stop_browser` | ✓ | ✓ | was `stop_chrome`; on an attached Firefox it ends the session, the browser keeps running |
|
|
45
|
+
| `open_tab` | ✓ | ✓ | |
|
|
46
|
+
| `close_tab` | ✓ | ✓ | |
|
|
47
|
+
| `list_tabs` | ✓ | ✓ | |
|
|
48
|
+
| `switch_tab` | ✓ | ✓ | |
|
|
49
|
+
| `navigate` | ✓ | ✓ | |
|
|
50
|
+
| `reload` | ✓ | ✓ | |
|
|
51
|
+
| `scroll` | ✓ | ✓ | |
|
|
52
|
+
| `inspect_dom` | ✓ | ✓ | |
|
|
53
|
+
| `capture_screenshot` | ✓ | ✓ | |
|
|
54
|
+
| `click_element` | ✓ | ✓ | |
|
|
55
|
+
| `fill_input` | ✓ | ✓ | |
|
|
56
|
+
| `evaluate_js` | ✓ | ✓ | |
|
|
57
|
+
| `get_console_logs` | ✓ | ✓ | |
|
|
58
|
+
| `get_network_logs` | ✓ | partial | Firefox: HTTP requests and response bodies, no WebSocket frames |
|
|
59
|
+
| `get_custom_events` | ✓ | partial | Firefox: events of the modules subscribed with `send_bidi_command` |
|
|
60
|
+
| `get_performance_metrics` | ✓ | ✗ | Firefox has no equivalent of CDP's Performance domain |
|
|
61
|
+
| `profile_page_performance` | ✓ | ✗ | Firefox has no equivalent of CDP's Tracing domain |
|
|
62
|
+
| `set_breakpoint` | ✓ | partial | Firefox: by URL only (no `script_id` / `script_hash`) |
|
|
63
|
+
| `remove_breakpoint` | ✓ | ✓ | |
|
|
64
|
+
| `resume` | ✓ | ✓ | |
|
|
65
|
+
| `step_over` | ✓ | ✓ | |
|
|
66
|
+
| `evaluate_on_call_frame` | ✓ | ✓ | |
|
|
67
|
+
| `search_scripts` | ✓ | ✓ | |
|
|
68
|
+
| `pause_on_load` | ✓ | ✗ | TODO on Firefox, see [Limitations and TODO](#limitations-and-todo) |
|
|
69
|
+
| `enable_proxy_auth` | ✓ | partial | needs `--proxy-server`; untested on Firefox against a real proxy |
|
|
70
|
+
| `send_cdp_command` | ✓ | ✗ | raw Chrome DevTools Protocol |
|
|
71
|
+
| `send_bidi_command` | ✗ | ✓ | raw WebDriver BiDi; refuses `session.new`, `session.end`, `browser.close` |
|
|
72
|
+
| `send_rdp_request` | ✗ | ✓ | raw Firefox RDP request to an actor (`thread`, `console`, `root` shortcuts) |
|
|
73
|
+
| `webmcp_list_tools` | ✓ | ✗ | needs `--enable-webmcp`; WebMCP is a Chrome testing feature |
|
|
74
|
+
| `webmcp_invoke_tool` | ✓ | ✗ | needs `--enable-webmcp` |
|
|
75
|
+
| `webmcp_get_invocation` | ✓ | ✗ | needs `--enable-webmcp` |
|
|
76
|
+
| `webmcp_list_invocations` | ✓ | ✗ | needs `--enable-webmcp` |
|
|
77
|
+
|
|
78
|
+
The JavaScript debugger tools on Firefox need its debugger (RDP) port: on by
|
|
79
|
+
default for a launched Firefox, `--firefox-rdp-port` for an attached one.
|
|
80
|
+
|
|
81
|
+
## 📦 Installation
|
|
82
|
+
|
|
83
|
+
Every method installs the **same pre-compiled binaries** published on each
|
|
84
|
+
[GitHub Release](https://github.com/raultov/browser-control-mcp/releases); a Rust toolchain is only needed for the Cargo and
|
|
85
|
+
source routes. The npm package is a small wrapper whose `postinstall` fetches
|
|
86
|
+
the pre-compiled binary for your platform from GitHub Releases: no compiling,
|
|
87
|
+
no Rust toolchain. Install Chrome and/or Firefox yourself: the server launches
|
|
88
|
+
them, it does not ship them (except the Docker image).
|
|
89
|
+
|
|
90
|
+
> ⚠️ **Always use the scoped npm name `@raultov/browser-control-mcp`.** The
|
|
91
|
+
> unscoped `browser-control-mcp` on npm is a different, unrelated project.
|
|
92
|
+
|
|
93
|
+
Prefer zero effort? The one-prompt route below lets your AI agent do all of
|
|
94
|
+
it. Otherwise, methods are grouped by platform and ordered by popularity.
|
|
95
|
+
|
|
96
|
+
### 🤖 One-Prompt Install (any platform)
|
|
97
|
+
|
|
98
|
+
Paste this prompt into any AI coding agent's chat (Claude Code, opencode,
|
|
99
|
+
Cursor, Codex CLI, Copilot, ...) and let it install the server and register
|
|
100
|
+
it with every MCP-capable client it detects on your machine:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
Install and set up the browser-control-mcp MCP server on this machine. Do not launch Chrome or Firefox yourself: the server launches and manages them.
|
|
104
|
+
|
|
105
|
+
1. Install the server binary with the first method that works on this OS:
|
|
106
|
+
- Homebrew available (macOS/Linux): brew install raultov/tap/browser-control-mcp
|
|
107
|
+
- Node.js >= 14 available (any OS): npm install -g @raultov/browser-control-mcp
|
|
108
|
+
- macOS/Linux fallback: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/raultov/browser-control-mcp/releases/latest/download/browser-control-mcp-installer.sh | sh
|
|
109
|
+
- Windows fallback: download and run the .msi from https://github.com/raultov/browser-control-mcp/releases/latest
|
|
110
|
+
Verify with: browser-control-mcp --version (add the install dir to PATH if it is not found).
|
|
111
|
+
|
|
112
|
+
2. Register the MCP server with every AI client you detect on this machine:
|
|
113
|
+
- Claude Code: claude mcp add --scope user browser-control-mcp browser-control-mcp
|
|
114
|
+
- opencode: merge into ~/.config/opencode/opencode.json —
|
|
115
|
+
"mcp": { "browser-control-mcp": { "type": "local", "command": ["browser-control-mcp"] } }
|
|
116
|
+
- Codex CLI: append to ~/.codex/config.toml —
|
|
117
|
+
[mcp_servers.browser-control-mcp]
|
|
118
|
+
command = "browser-control-mcp"
|
|
119
|
+
- Any other MCP-capable client (Cursor, Windsurf, VS Code, ...): add it with its own MCP settings, command "browser-control-mcp" with no arguments.
|
|
120
|
+
|
|
121
|
+
3. Report what you installed and which clients you configured, and remind me to restart each client so the MCP server loads. Do not enable --user-profile or --allow-cookie-import unless I ask for them.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### macOS
|
|
125
|
+
|
|
126
|
+
**1. Homebrew**, the natural option on macOS:
|
|
127
|
+
```bash
|
|
128
|
+
brew install raultov/tap/browser-control-mcp
|
|
129
|
+
```
|
|
130
|
+
The formula fetches the pre-compiled binary (Apple Silicon and Intel) from
|
|
131
|
+
GitHub Releases, and `brew upgrade` keeps you current.
|
|
132
|
+
|
|
133
|
+
**2. npm / npx**, the MCP-client convention (requires Node.js >= 14):
|
|
134
|
+
```bash
|
|
135
|
+
npm install -g @raultov/browser-control-mcp # installs the `browser-control-mcp` command
|
|
136
|
+
npx -y @raultov/browser-control-mcp # ...or just run it once, nothing to install
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**3. Shell installer**, a one-liner that installs to `~/.cargo/bin`:
|
|
140
|
+
```bash
|
|
141
|
+
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/raultov/browser-control-mcp/releases/latest/download/browser-control-mcp-installer.sh | sh
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**4. Cargo**, if you already have Rust:
|
|
145
|
+
```bash
|
|
146
|
+
cargo install browser-control-mcp # compiles from crates.io
|
|
147
|
+
cargo binstall browser-control-mcp # with cargo-binstall: fetches the pre-compiled binary, no compile
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**5. Manual download**: the `aarch64-apple-darwin` or `x86_64-apple-darwin`
|
|
151
|
+
`.tar.xz` archive from the [Releases](https://github.com/raultov/browser-control-mcp/releases) page.
|
|
152
|
+
|
|
153
|
+
### Linux
|
|
154
|
+
|
|
155
|
+
**1. npm / npx** (requires Node.js >= 14):
|
|
156
|
+
```bash
|
|
157
|
+
npm install -g @raultov/browser-control-mcp # installs the `browser-control-mcp` command
|
|
158
|
+
npx -y @raultov/browser-control-mcp # ...or just run it once, nothing to install
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**2. Shell installer**, a one-liner that installs to `~/.cargo/bin`:
|
|
162
|
+
```bash
|
|
163
|
+
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/raultov/browser-control-mcp/releases/latest/download/browser-control-mcp-installer.sh | sh
|
|
164
|
+
```
|
|
165
|
+
The Linux binaries require glibc >= 2.35 (Ubuntu 22.04+, Debian 12+, Fedora 36+).
|
|
166
|
+
|
|
167
|
+
**3. Homebrew (Linuxbrew)**, if you already use
|
|
168
|
+
[Homebrew on Linux](https://docs.brew.sh/Homebrew-on-Linux):
|
|
169
|
+
```bash
|
|
170
|
+
brew install raultov/tap/browser-control-mcp
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**4. Cargo**, if you already have Rust:
|
|
174
|
+
```bash
|
|
175
|
+
cargo install browser-control-mcp # compiles from crates.io
|
|
176
|
+
cargo binstall browser-control-mcp # with cargo-binstall: fetches the pre-compiled binary, no compile
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**5. Manual download**: the `x86_64-unknown-linux-gnu` or
|
|
180
|
+
`aarch64-unknown-linux-gnu` `.tar.xz` archive from the [Releases](https://github.com/raultov/browser-control-mcp/releases) page.
|
|
181
|
+
|
|
182
|
+
### Windows
|
|
183
|
+
|
|
184
|
+
**1. npm / npx** (requires Node.js >= 14):
|
|
185
|
+
```powershell
|
|
186
|
+
npm install -g @raultov/browser-control-mcp # installs the `browser-control-mcp` command
|
|
187
|
+
npx -y @raultov/browser-control-mcp # ...or just run it once, nothing to install
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**2. MSI installer**, the native Windows experience: download
|
|
191
|
+
`browser-control-mcp-x86_64-pc-windows-msvc.msi` from the [Releases](https://github.com/raultov/browser-control-mcp/releases)
|
|
192
|
+
page and double-click it.
|
|
193
|
+
|
|
194
|
+
**3. PowerShell installer**, a one-liner:
|
|
195
|
+
```powershell
|
|
196
|
+
powershell -ExecutionPolicy Bypass -c "irm https://github.com/raultov/browser-control-mcp/releases/latest/download/browser-control-mcp-installer.ps1 | iex"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**4. Cargo**, if you already have Rust:
|
|
200
|
+
```powershell
|
|
201
|
+
cargo install browser-control-mcp # compiles from crates.io
|
|
202
|
+
cargo binstall browser-control-mcp # with cargo-binstall: fetches the pre-compiled binary, no compile
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**5. Manual download**: the `x86_64-pc-windows-msvc` `.zip` archive from the
|
|
206
|
+
[Releases](https://github.com/raultov/browser-control-mcp/releases) page.
|
|
207
|
+
|
|
208
|
+
### Docker (amd64)
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
docker build -t browser-control-mcp .
|
|
212
|
+
docker run -i --rm browser-control-mcp --headless # Chrome and Firefox inside
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### From source
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
git clone https://github.com/raultov/browser-control-mcp && cd browser-control-mcp
|
|
219
|
+
make install # release build copied to ~/.cargo/bin (or cargo install --path . --locked)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Browser discovery
|
|
223
|
+
|
|
224
|
+
`CHROME_PATH`, then `google-chrome`, `chromium` and the usual install paths;
|
|
225
|
+
`--firefox-path`, then `FIREFOX_PATH`, then `firefox` and the usual install
|
|
226
|
+
paths.
|
|
227
|
+
|
|
228
|
+
## MCP client configuration
|
|
229
|
+
|
|
230
|
+
Universal JSON:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"mcpServers": {
|
|
235
|
+
"browser-control-mcp": { "command": "browser-control-mcp", "args": [] },
|
|
236
|
+
"browser-control-firefox": {
|
|
237
|
+
"command": "browser-control-mcp",
|
|
238
|
+
"args": ["--browser", "firefox"]
|
|
239
|
+
},
|
|
240
|
+
"browser-control-docker": {
|
|
241
|
+
"command": "docker",
|
|
242
|
+
"args": ["run", "-i", "--rm", "browser-control-mcp", "--headless"]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Claude Code:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
claude mcp add browser-control-mcp browser-control-mcp
|
|
252
|
+
claude mcp add browser-control-firefox -- browser-control-mcp --browser firefox
|
|
253
|
+
claude mcp add browser-control-mcp -- npx -y @raultov/browser-control-mcp
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Usage concepts
|
|
257
|
+
|
|
258
|
+
- **Instances.** A `default` instance always exists and uses the `--browser`
|
|
259
|
+
flag. `open_instance` with a `label` starts another one (its id is the
|
|
260
|
+
label), with `browser` to pick Chrome or Firefox. Browsers are launched
|
|
261
|
+
lazily, on the first tool that needs them, with an ephemeral profile.
|
|
262
|
+
- **Tabs.** `open_tab`, `list_tabs`, `switch_tab`, `close_tab`. Every tool
|
|
263
|
+
takes `instance_id` and `tab_id`; without them it targets the default
|
|
264
|
+
instance and its active tab.
|
|
265
|
+
- **Per-tab state.** Console logs, network traffic, breakpoints and custom
|
|
266
|
+
events are kept per tab, in bounded buffers.
|
|
267
|
+
- **Firefox allows a single automation session.** Each Firefox instance holds
|
|
268
|
+
one WebDriver BiDi session shared by its tabs; `close_instance`,
|
|
269
|
+
`stop_browser` and every server shutdown path end it, so the next run can
|
|
270
|
+
attach again.
|
|
271
|
+
|
|
272
|
+
## CLI flags
|
|
273
|
+
|
|
274
|
+
| Flag | Default | Chrome | Firefox |
|
|
275
|
+
| --- | --- | --- | --- |
|
|
276
|
+
| `--browser <chrome\|firefox>` | `chrome` | browser of the default instance | browser of the default instance |
|
|
277
|
+
| `--local` | off | navigation limited to local addresses | same |
|
|
278
|
+
| `--headless` | off | headless Chrome | headless Firefox |
|
|
279
|
+
| `--host <HOST>` | `127.0.0.1` | debugging host | debugging host |
|
|
280
|
+
| `--port <PORT>` | `9222` | debugging port of the default instance | ignored (use `--firefox-port`) |
|
|
281
|
+
| `--max-instances <N>` | `8` | instance cap | shared with Chrome |
|
|
282
|
+
| `--enable-automation` | off | shows the automation infobar | ignored; `list_instances` shows a warning |
|
|
283
|
+
| `--user-profile` | off | your default profile (no `--user-data-dir`) | only with `--firefox-port` (attach to your running Firefox) |
|
|
284
|
+
| `--allow-cookie-import` | off | exposes `copy_cookies` | capability error |
|
|
285
|
+
| `--enable-webmcp` | off | WebMCP switches and `webmcp_*` tools | capability error |
|
|
286
|
+
| `--proxy-server <URL>` | none | proxy for every instance; exposes `enable_proxy_auth` | `http://` and `socks5://` proxies |
|
|
287
|
+
| `--proxy-username <USER>` | none | answers proxy (407) challenges | same (untested) |
|
|
288
|
+
| `--proxy-password <PASS>` | none | never logged | same |
|
|
289
|
+
| `--firefox-path <PATH>` | discovered | — | Firefox binary to launch |
|
|
290
|
+
| `--firefox-port <PORT>` | none (launch) | — | attach to a running Firefox's BiDi port |
|
|
291
|
+
| `--firefox-rdp-port <PORT>` | none | — | debugger port of the attached Firefox (needs `--firefox-port`) |
|
|
292
|
+
| `--no-firefox-debugger` | off | — | launch Firefox without its debugger port (no debugger tools) |
|
|
293
|
+
|
|
294
|
+
`--proxy` and `--webmcp` are aliases. To attach to your own Firefox, start it
|
|
295
|
+
with `--remote-debugging-port <port>` (and `--start-debugger-server ws:<rdp>`
|
|
296
|
+
plus `devtools.debugger.remote-enabled` and `devtools.chrome.enabled` for the
|
|
297
|
+
debugger), then pass `--browser firefox --firefox-port <port>
|
|
298
|
+
--firefox-rdp-port <rdp>`.
|
|
299
|
+
|
|
300
|
+
## Architecture
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
MCP client (agent)
|
|
304
|
+
│ JSON-RPC over stdio (tools/list, tools/call)
|
|
305
|
+
▼
|
|
306
|
+
browser-control-mcp ── tool table: flag gate → capability check → per-browser handler
|
|
307
|
+
│ │
|
|
308
|
+
│ Chrome instance │ Firefox instance
|
|
309
|
+
▼ ▼
|
|
310
|
+
cdp-browser-lite / cdp-lite bidi-browser-lite / bidi-lite
|
|
311
|
+
(launch, ports, profiles, (launch, prefs, one BiDi session,
|
|
312
|
+
CDP over WebSocket) RDP client for the debugger)
|
|
313
|
+
│ CDP commands/events │ BiDi commands/events │ RDP packets
|
|
314
|
+
▼ ▼ ▼
|
|
315
|
+
Chrome Firefox ◄───────────────────┘
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
A tool call resolves the instance (`instance_id`, default `default`) and the
|
|
319
|
+
tab (`tab_id`, default active), checks the instance can serve it, then runs
|
|
320
|
+
the implementation for that instance's browser. Events are pumped into
|
|
321
|
+
per-tab state: on Chrome, one listener per CDP domain and tab; on Firefox,
|
|
322
|
+
one subscription per session, routed by browsing context id.
|
|
323
|
+
|
|
324
|
+
## Debugging JavaScript
|
|
325
|
+
|
|
326
|
+
Chrome and Firefox take the same calls:
|
|
327
|
+
|
|
328
|
+
1. `navigate` to the page (or `open_tab` + `switch_tab`).
|
|
329
|
+
2. `search_scripts` with a needle to find the script URL and line.
|
|
330
|
+
3. `set_breakpoint` with `url` and `line_number` (0-based).
|
|
331
|
+
4. Trigger the code (`click_element`, `evaluate_js` with a `setTimeout`, a
|
|
332
|
+
reload...). The page pauses.
|
|
333
|
+
5. `evaluate_on_call_frame` reads locals of the paused frame; `step_over`
|
|
334
|
+
moves one statement; `resume` continues.
|
|
335
|
+
6. `remove_breakpoint` with the id `set_breakpoint` returned.
|
|
336
|
+
|
|
337
|
+
Chrome also offers `pause_on_load` (pause on the first statement of the next
|
|
338
|
+
load). Firefox notes: breakpoints by URL only; the column is snapped to the
|
|
339
|
+
nearest breakable position automatically; the debugger needs the RDP port.
|
|
340
|
+
|
|
341
|
+
## Migration from chrome-debug-mcp
|
|
342
|
+
|
|
343
|
+
| chrome-debug-mcp 1.5.6 | browser-control-mcp |
|
|
344
|
+
| --- | --- |
|
|
345
|
+
| binary / crate `chrome-debug-mcp` | `browser-control-mcp` |
|
|
346
|
+
| npm `@raultov/chrome-debug-mcp` | `@raultov/browser-control-mcp` |
|
|
347
|
+
| tool `restart_chrome` | `restart_browser` |
|
|
348
|
+
| tool `stop_chrome` | `stop_browser` |
|
|
349
|
+
| replies printed as Rust `Debug` (`WsResponse { .. }`) | prefix + pretty JSON of the reply |
|
|
350
|
+
| `get_network_logs` returned credential headers | `<redacted>` unless `include_sensitive_headers: true` |
|
|
351
|
+
| `enable_proxy_auth` answered every auth challenge | answers proxy (407) challenges only |
|
|
352
|
+
|
|
353
|
+
All command-line flags are kept; `--browser`, `--firefox-path`,
|
|
354
|
+
`--firefox-port`, `--firefox-rdp-port` and `--no-firefox-debugger` are new.
|
|
355
|
+
In client configurations replace the command `chrome-debug-mcp` with
|
|
356
|
+
`browser-control-mcp` (or the npm package name). The old tool names answer an
|
|
357
|
+
error that names the new ones.
|
|
358
|
+
|
|
359
|
+
## Limitations and TODO
|
|
360
|
+
|
|
361
|
+
- **TODO (priority)** Unified `navigate` response: Chrome returns the CDP ids
|
|
362
|
+
(`frameId`, `loaderId`) and Firefox `{navigation, url}`; both will return
|
|
363
|
+
the final URL in the same shape.
|
|
364
|
+
- **TODO (priority)** Unified `evaluate_js` result: Chrome wraps the value in
|
|
365
|
+
the CDP `RemoteObject` (`{"result": {"type", "value"}}`) and Firefox
|
|
366
|
+
returns the bare value; both will return the same shape.
|
|
367
|
+
- **TODO (priority)** Navigation errors: on Chrome, `navigate` and `open_tab`
|
|
368
|
+
to an unreachable host report success (the network error is only inside
|
|
369
|
+
the reply, or absent); both browsers will fail with the network error.
|
|
370
|
+
- **TODO (priority)** Console buffer per navigation: `get_console_logs` will
|
|
371
|
+
clear a tab's buffer when it navigates to a different site (kept across
|
|
372
|
+
subdomains of the same site) and gain a limit, so one call cannot flood
|
|
373
|
+
the client.
|
|
374
|
+
- **TODO (priority)** Unified debugger replies: `set_breakpoint`,
|
|
375
|
+
`remove_breakpoint` and `step_over` answer differently on Chrome and
|
|
376
|
+
Firefox (and Firefox's `step_over` location uses a 1-based line); both
|
|
377
|
+
will return the same shape with 0-based lines.
|
|
378
|
+
- **TODO (priority)** Known bug: on Chrome `get_performance_metrics` returns
|
|
379
|
+
`{}` because the Performance domain is never enabled. Workaround:
|
|
380
|
+
`send_cdp_command` with `Performance.enable` first.
|
|
381
|
+
- **TODO (priority)** Partial performance on Firefox from the standard Web
|
|
382
|
+
Performance APIs (navigation timings, FCP, LCP), stating what Firefox
|
|
383
|
+
cannot give (long tasks, CLS, JS heap).
|
|
384
|
+
- **TODO (priority)** Same default window size on both browsers (today
|
|
385
|
+
Chrome 921x648, Firefox 1280x887): the screen size when known, else
|
|
386
|
+
1920x1080.
|
|
387
|
+
- **TODO (priority)** Firefox WebSocket frames and the full console natively
|
|
388
|
+
over the Firefox debugger (RDP watcher `websocket`, `error-message` and
|
|
389
|
+
`css-message` resources), so both browsers return the same shape without
|
|
390
|
+
any page injection.
|
|
391
|
+
- **TODO** `pause_on_load` on Firefox: neither a preload `debugger;`
|
|
392
|
+
statement nor an RDP interrupt pauses a reloading page (spike verdict).
|
|
393
|
+
- **TODO** `rust-mcp-sdk` 2.0: the server stays on 1.1.0 (MCP protocol
|
|
394
|
+
2025-11-25). 2.0 moves to the stateless MCP protocol (2026-07-28), which
|
|
395
|
+
today's MCP clients probably do not speak yet; upgrade once the clients do.
|
|
396
|
+
- `get_performance_metrics` and `profile_page_performance` are Chrome only:
|
|
397
|
+
Firefox has no Performance or Tracing domain.
|
|
398
|
+
- WebSocket frames in `get_network_logs` are Chrome only: WebDriver BiDi has
|
|
399
|
+
no WebSocket frame events.
|
|
400
|
+
- `webmcp_list_tools`, `webmcp_invoke_tool`, `webmcp_get_invocation` and
|
|
401
|
+
`webmcp_list_invocations` are Chrome only (a Chrome testing feature).
|
|
402
|
+
- `send_cdp_command` is Chrome only; `send_bidi_command` and
|
|
403
|
+
`send_rdp_request` are Firefox only.
|
|
404
|
+
- Cookie import (`--allow-cookie-import`) and launch presets (`features`) are
|
|
405
|
+
Chrome only.
|
|
406
|
+
- Firefox breakpoints are set by URL only.
|
|
407
|
+
- `--local` checks the URLs the server navigates to and the known navigation
|
|
408
|
+
methods of the raw tools; JavaScript running in the page can still navigate
|
|
409
|
+
anywhere.
|
|
410
|
+
- Firefox: after a lost connection a launched Firefox gets a new session on
|
|
411
|
+
the next tool call; an attached Firefox keeps its orphan session, so
|
|
412
|
+
restart that Firefox.
|
|
413
|
+
- Firefox debugger: it attaches to the tab Firefox has selected right after
|
|
414
|
+
activating the requested one; switching tabs at that very moment can bind
|
|
415
|
+
it to the wrong page.
|
|
416
|
+
- `get_network_logs` with response bodies has no total size limit (each
|
|
417
|
+
Firefox body is capped at 2 MB): use `url_filter` on busy pages.
|
|
418
|
+
- The Docker image is amd64 only.
|
|
419
|
+
|
|
420
|
+
## Untested
|
|
421
|
+
|
|
422
|
+
- Proxy authentication on Firefox against a real proxy.
|
|
423
|
+
- macOS and Windows with real browsers (CI builds and unit-tests them; the
|
|
424
|
+
E2E smoke ran on Linux only).
|
|
425
|
+
- Firefox ESR.
|
|
426
|
+
- The Docker image on arm64.
|
|
427
|
+
- The Windows clippy cross-check (`make clippy-windows`) on a machine with
|
|
428
|
+
mingw.
|
|
429
|
+
|
|
430
|
+
## Security notes
|
|
431
|
+
|
|
432
|
+
- `--allow-cookie-import` copies your real Chrome cookies into an isolated
|
|
433
|
+
profile; the agent must ask you before using `copy_cookies`.
|
|
434
|
+
- The proxy password is never logged and is given only to proxy (407)
|
|
435
|
+
challenges, never to a site's own authentication.
|
|
436
|
+
- The Firefox debugger port needs `devtools.chrome.enabled`: any local
|
|
437
|
+
process that reaches that port gets chrome (privileged) access to the
|
|
438
|
+
browser. Use `--no-firefox-debugger` when you do not need the debugger.
|
|
439
|
+
- Credential headers (`Authorization`, `Cookie`, `Set-Cookie`, ...) are
|
|
440
|
+
redacted in `get_network_logs` unless `include_sensitive_headers: true`.
|
|
441
|
+
- Logs carry tool names, ids, protocol method names and sizes, never
|
|
442
|
+
payloads, cookies or URLs with queries.
|
|
443
|
+
|
|
444
|
+
## Development
|
|
445
|
+
|
|
446
|
+
```sh
|
|
447
|
+
make check # fmt, clippy -D warnings, tests, docs, duplication, packaging, version
|
|
448
|
+
make e2e # E2E smoke against real, visible Chrome and Firefox (Linux, macOS)
|
|
449
|
+
make check-all # both
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
`make check` is hermetic: it never starts a browser or touches the network.
|
|
453
|
+
`make e2e` runs `tests/run_all_e2e.sh`: the launched-mode smoke for both
|
|
454
|
+
browsers, then the attach mode against browsers started by `tests/chrome.sh`
|
|
455
|
+
and `tests/firefox.sh`. See [AGENTS.md](AGENTS.md) for the conventions.
|
|
456
|
+
|
|
457
|
+
## Releasing
|
|
458
|
+
|
|
459
|
+
For the maintainer: bump the version in `Cargo.toml` and `package.json`,
|
|
460
|
+
close the `[Unreleased]` section of `CHANGELOG.md`, run `make check-all`,
|
|
461
|
+
tag `vX.Y.Z` and push the tag (the release workflow builds the installers and
|
|
462
|
+
publishes npm and Homebrew; it needs the `NPM_TOKEN` and
|
|
463
|
+
`HOMEBREW_TAP_TOKEN` secrets), then `cargo publish` by hand.
|
|
464
|
+
|
|
465
|
+
## License
|
|
466
|
+
|
|
467
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createWriteStream,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
rmSync,
|
|
7
|
+
} = require("fs");
|
|
8
|
+
const { join, sep } = require("path");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
const { tmpdir } = require("os");
|
|
11
|
+
|
|
12
|
+
const https = require("node:https");
|
|
13
|
+
const http = require("node:http");
|
|
14
|
+
|
|
15
|
+
const tmpDir = tmpdir();
|
|
16
|
+
|
|
17
|
+
const error = (msg) => {
|
|
18
|
+
console.error(msg);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getProxyForUrl(urlString) {
|
|
23
|
+
const url = new URL(urlString);
|
|
24
|
+
const isHttps = url.protocol === "https:";
|
|
25
|
+
|
|
26
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
27
|
+
if (noProxy === "*") return null;
|
|
28
|
+
if (noProxy) {
|
|
29
|
+
const hostname = url.hostname.toLowerCase();
|
|
30
|
+
const noProxyList = noProxy.split(",").map((s) => s.trim().toLowerCase());
|
|
31
|
+
for (const entry of noProxyList) {
|
|
32
|
+
if (hostname === entry || hostname.endsWith("." + entry)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const proxyEnv = isHttps
|
|
39
|
+
? process.env.HTTPS_PROXY || process.env.https_proxy
|
|
40
|
+
: process.env.HTTP_PROXY || process.env.http_proxy;
|
|
41
|
+
|
|
42
|
+
if (!proxyEnv) return null;
|
|
43
|
+
|
|
44
|
+
const proxyUrl = new URL(proxyEnv);
|
|
45
|
+
|
|
46
|
+
let auth = null;
|
|
47
|
+
if (proxyUrl.username || proxyUrl.password) {
|
|
48
|
+
auth = `${proxyUrl.username}:${proxyUrl.password}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
hostname: proxyUrl.hostname,
|
|
53
|
+
port: proxyUrl.port || (proxyUrl.protocol === "https:" ? 443 : 80),
|
|
54
|
+
auth: auth,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function connectThroughProxy(proxy, target) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const headers = {};
|
|
61
|
+
if (proxy.auth) {
|
|
62
|
+
headers["Proxy-Authorization"] =
|
|
63
|
+
"Basic " + Buffer.from(proxy.auth).toString("base64");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const connectReq = http.request({
|
|
67
|
+
hostname: proxy.hostname,
|
|
68
|
+
port: proxy.port,
|
|
69
|
+
method: "CONNECT",
|
|
70
|
+
path: `${target.hostname}:${target.port || 443}`,
|
|
71
|
+
headers,
|
|
72
|
+
});
|
|
73
|
+
connectReq.on("connect", (res, socket) => {
|
|
74
|
+
if (res.statusCode === 200) {
|
|
75
|
+
resolve(socket);
|
|
76
|
+
} else {
|
|
77
|
+
reject(new Error(`Proxy CONNECT failed with status ${res.statusCode}`));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
connectReq.on("error", reject);
|
|
81
|
+
connectReq.end();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function download(urlString, maxRedirects) {
|
|
86
|
+
if (maxRedirects === undefined) maxRedirects = 5;
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
if (maxRedirects < 0) {
|
|
89
|
+
return reject(new Error("Too many redirects"));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parsed = new URL(urlString);
|
|
93
|
+
const isHttps = parsed.protocol === "https:";
|
|
94
|
+
const mod = isHttps ? https : http;
|
|
95
|
+
const proxy = getProxyForUrl(urlString);
|
|
96
|
+
|
|
97
|
+
const doRequest = (extraOptions) => {
|
|
98
|
+
const options = Object.assign(
|
|
99
|
+
{
|
|
100
|
+
hostname: parsed.hostname,
|
|
101
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
102
|
+
path: parsed.pathname + parsed.search,
|
|
103
|
+
method: "GET",
|
|
104
|
+
headers: { "User-Agent": "cargo-dist-npm-installer" },
|
|
105
|
+
},
|
|
106
|
+
extraOptions || {},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (proxy && !isHttps) {
|
|
110
|
+
// HTTP through HTTP proxy: request the full URL via the proxy
|
|
111
|
+
options.hostname = proxy.hostname;
|
|
112
|
+
options.port = proxy.port;
|
|
113
|
+
options.path = urlString;
|
|
114
|
+
if (proxy.auth) {
|
|
115
|
+
options.headers["Proxy-Authorization"] =
|
|
116
|
+
"Basic " + Buffer.from(proxy.auth).toString("base64");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const req = mod.request(options, (res) => {
|
|
121
|
+
if (
|
|
122
|
+
res.statusCode >= 300 &&
|
|
123
|
+
res.statusCode < 400 &&
|
|
124
|
+
res.headers.location
|
|
125
|
+
) {
|
|
126
|
+
res.resume();
|
|
127
|
+
const nextUrl = new URL(res.headers.location, urlString).toString();
|
|
128
|
+
return download(nextUrl, maxRedirects - 1).then(resolve, reject);
|
|
129
|
+
}
|
|
130
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
131
|
+
res.resume();
|
|
132
|
+
return reject(new Error(`HTTP ${res.statusCode} from ${urlString}`));
|
|
133
|
+
}
|
|
134
|
+
resolve(res);
|
|
135
|
+
});
|
|
136
|
+
req.on("error", reject);
|
|
137
|
+
req.end();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (proxy && isHttps) {
|
|
141
|
+
connectThroughProxy(proxy, parsed).then(
|
|
142
|
+
(socket) => doRequest({ socket, agent: false }),
|
|
143
|
+
reject,
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
doRequest();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
class Package {
|
|
152
|
+
constructor(platform, name, url, filename, zipExt, binaries) {
|
|
153
|
+
let errors = [];
|
|
154
|
+
if (typeof url !== "string") {
|
|
155
|
+
errors.push("url must be a string");
|
|
156
|
+
} else {
|
|
157
|
+
try {
|
|
158
|
+
new URL(url);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
errors.push(e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (name && typeof name !== "string") {
|
|
164
|
+
errors.push("package name must be a string");
|
|
165
|
+
}
|
|
166
|
+
if (!name) {
|
|
167
|
+
errors.push("You must specify the name of your package");
|
|
168
|
+
}
|
|
169
|
+
if (binaries && typeof binaries !== "object") {
|
|
170
|
+
errors.push("binaries must be a string => string map");
|
|
171
|
+
}
|
|
172
|
+
if (!binaries) {
|
|
173
|
+
errors.push("You must specify the binaries in the package");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (errors.length > 0) {
|
|
177
|
+
let errorMsg =
|
|
178
|
+
"One or more of the parameters you passed to the Binary constructor are invalid:\n";
|
|
179
|
+
errors.forEach((error) => {
|
|
180
|
+
errorMsg += error;
|
|
181
|
+
});
|
|
182
|
+
errorMsg +=
|
|
183
|
+
'\n\nCorrect usage: new Package("my-binary", "https://example.com/binary/download.tar.gz", {"my-binary": "my-binary"})';
|
|
184
|
+
error(errorMsg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.platform = platform;
|
|
188
|
+
this.url = url;
|
|
189
|
+
this.name = name;
|
|
190
|
+
this.filename = filename;
|
|
191
|
+
this.zipExt = zipExt;
|
|
192
|
+
this.installDirectory = join(__dirname, "node_modules", ".bin_real");
|
|
193
|
+
this.binaries = binaries;
|
|
194
|
+
|
|
195
|
+
if (!existsSync(this.installDirectory)) {
|
|
196
|
+
mkdirSync(this.installDirectory, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
exists() {
|
|
201
|
+
for (const binaryName in this.binaries) {
|
|
202
|
+
const binRelPath = this.binaries[binaryName];
|
|
203
|
+
const binPath = join(this.installDirectory, binRelPath);
|
|
204
|
+
if (!existsSync(binPath)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
install(suppressLogs = false) {
|
|
212
|
+
if (this.exists()) {
|
|
213
|
+
if (!suppressLogs) {
|
|
214
|
+
console.error(
|
|
215
|
+
`${this.name} is already installed, skipping installation.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return Promise.resolve();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
rmSync(this.installDirectory, { recursive: true, force: true });
|
|
223
|
+
} catch {
|
|
224
|
+
// ignore - directory may not exist
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
mkdirSync(this.installDirectory, { recursive: true });
|
|
228
|
+
|
|
229
|
+
if (!suppressLogs) {
|
|
230
|
+
console.error(`Downloading release from ${this.url}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return download(this.url)
|
|
234
|
+
.then((res) => {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
|
|
237
|
+
if (err) return reject(err);
|
|
238
|
+
let tempFile = join(directory, this.filename);
|
|
239
|
+
const sink = res.pipe(createWriteStream(tempFile));
|
|
240
|
+
sink.on("error", (err) => reject(err));
|
|
241
|
+
sink.on("close", () => {
|
|
242
|
+
if (/\.tar\.*/.test(this.zipExt)) {
|
|
243
|
+
const result = spawnSync("tar", [
|
|
244
|
+
"xf",
|
|
245
|
+
tempFile,
|
|
246
|
+
// The tarballs are stored with a leading directory
|
|
247
|
+
// component; we strip one component in the
|
|
248
|
+
// shell installers too.
|
|
249
|
+
"--strip-components",
|
|
250
|
+
"1",
|
|
251
|
+
"-C",
|
|
252
|
+
this.installDirectory,
|
|
253
|
+
]);
|
|
254
|
+
if (result.status == 0) {
|
|
255
|
+
resolve();
|
|
256
|
+
} else if (result.error) {
|
|
257
|
+
reject(result.error);
|
|
258
|
+
} else {
|
|
259
|
+
reject(
|
|
260
|
+
new Error(
|
|
261
|
+
`An error occurred untarring the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} else if (this.zipExt == ".zip") {
|
|
266
|
+
let result;
|
|
267
|
+
if (this.platform.artifactName.includes("windows")) {
|
|
268
|
+
// Windows does not have "unzip" by default on many installations, instead
|
|
269
|
+
// we use Expand-Archive from powershell
|
|
270
|
+
result = spawnSync("powershell.exe", [
|
|
271
|
+
"-NoProfile",
|
|
272
|
+
"-NonInteractive",
|
|
273
|
+
"-Command",
|
|
274
|
+
`& {
|
|
275
|
+
param([string]$LiteralPath, [string]$DestinationPath)
|
|
276
|
+
Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force
|
|
277
|
+
}`,
|
|
278
|
+
tempFile,
|
|
279
|
+
this.installDirectory,
|
|
280
|
+
]);
|
|
281
|
+
} else {
|
|
282
|
+
result = spawnSync("unzip", [
|
|
283
|
+
"-q",
|
|
284
|
+
tempFile,
|
|
285
|
+
"-d",
|
|
286
|
+
this.installDirectory,
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (result.status == 0) {
|
|
291
|
+
resolve();
|
|
292
|
+
} else if (result.error) {
|
|
293
|
+
reject(result.error);
|
|
294
|
+
} else {
|
|
295
|
+
reject(
|
|
296
|
+
new Error(
|
|
297
|
+
`An error occurred unzipping the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
reject(
|
|
303
|
+
new Error(`Unrecognized file extension: ${this.zipExt}`),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
})
|
|
310
|
+
.then(() => {
|
|
311
|
+
if (!suppressLogs) {
|
|
312
|
+
console.error(`${this.name} has been installed!`);
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
.catch((e) => {
|
|
316
|
+
error(`Error fetching release: ${e.message}`);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
run(binaryName) {
|
|
321
|
+
const promise = !this.exists() ? this.install(true) : Promise.resolve();
|
|
322
|
+
|
|
323
|
+
promise
|
|
324
|
+
.then(() => {
|
|
325
|
+
const [, , ...args] = process.argv;
|
|
326
|
+
|
|
327
|
+
const options = { cwd: process.cwd(), stdio: "inherit" };
|
|
328
|
+
|
|
329
|
+
const binRelPath = this.binaries[binaryName];
|
|
330
|
+
if (!binRelPath) {
|
|
331
|
+
error(`${binaryName} is not a known binary in ${this.name}`);
|
|
332
|
+
}
|
|
333
|
+
const binPath = join(this.installDirectory, binRelPath);
|
|
334
|
+
const result = spawnSync(binPath, args, options);
|
|
335
|
+
|
|
336
|
+
if (result.error) {
|
|
337
|
+
error(result.error);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
process.exit(result.status);
|
|
341
|
+
})
|
|
342
|
+
.catch((e) => {
|
|
343
|
+
error(e.message);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports.Package = Package;
|
package/binary.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const { Package } = require("./binary-install");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const libc = require("detect-libc");
|
|
4
|
+
|
|
5
|
+
const error = (msg) => {
|
|
6
|
+
console.error(msg);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
name,
|
|
12
|
+
artifactDownloadUrls,
|
|
13
|
+
supportedPlatforms,
|
|
14
|
+
glibcMinimum,
|
|
15
|
+
} = require("./package.json");
|
|
16
|
+
|
|
17
|
+
// FIXME: implement NPM installer handling of fallback download URLs
|
|
18
|
+
const artifactDownloadUrl = artifactDownloadUrls[0];
|
|
19
|
+
const builderGlibcMajorVersion = glibcMinimum.major;
|
|
20
|
+
const builderGlibcMinorVersion = glibcMinimum.series;
|
|
21
|
+
|
|
22
|
+
const getPlatform = () => {
|
|
23
|
+
const rawOsType = os.type();
|
|
24
|
+
const rawArchitecture = os.arch();
|
|
25
|
+
|
|
26
|
+
// We want to use rust-style target triples as the canonical key
|
|
27
|
+
// for a platform, so translate the "os" library's concepts into rust ones
|
|
28
|
+
let osType = "";
|
|
29
|
+
switch (rawOsType) {
|
|
30
|
+
case "Windows_NT":
|
|
31
|
+
osType = "pc-windows-msvc";
|
|
32
|
+
break;
|
|
33
|
+
case "Darwin":
|
|
34
|
+
osType = "apple-darwin";
|
|
35
|
+
break;
|
|
36
|
+
case "Linux":
|
|
37
|
+
osType = "unknown-linux-gnu";
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let arch = "";
|
|
42
|
+
switch (rawArchitecture) {
|
|
43
|
+
case "x64":
|
|
44
|
+
arch = "x86_64";
|
|
45
|
+
break;
|
|
46
|
+
case "arm64":
|
|
47
|
+
arch = "aarch64";
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (rawOsType === "Linux") {
|
|
52
|
+
if (libc.familySync() == "musl") {
|
|
53
|
+
osType = "unknown-linux-musl-dynamic";
|
|
54
|
+
} else if (libc.isNonGlibcLinuxSync()) {
|
|
55
|
+
console.warn(
|
|
56
|
+
"Your libc is neither glibc nor musl; trying static musl binary instead",
|
|
57
|
+
);
|
|
58
|
+
osType = "unknown-linux-musl-static";
|
|
59
|
+
} else {
|
|
60
|
+
let libcVersion = libc.versionSync();
|
|
61
|
+
let splitLibcVersion = libcVersion.split(".");
|
|
62
|
+
let libcMajorVersion = splitLibcVersion[0];
|
|
63
|
+
let libcMinorVersion = splitLibcVersion[1];
|
|
64
|
+
if (
|
|
65
|
+
libcMajorVersion != builderGlibcMajorVersion ||
|
|
66
|
+
libcMinorVersion < builderGlibcMinorVersion
|
|
67
|
+
) {
|
|
68
|
+
// We can't run the glibc binaries, but we can run the static musl ones
|
|
69
|
+
// if they exist
|
|
70
|
+
console.warn(
|
|
71
|
+
"Your glibc isn't compatible; trying static musl binary instead",
|
|
72
|
+
);
|
|
73
|
+
osType = "unknown-linux-musl-static";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Assume the above succeeded and build a target triple to look things up with.
|
|
79
|
+
// If any of it failed, this lookup will fail and we'll handle it like normal.
|
|
80
|
+
let targetTriple = `${arch}-${osType}`;
|
|
81
|
+
let platform = supportedPlatforms[targetTriple];
|
|
82
|
+
|
|
83
|
+
if (!platform) {
|
|
84
|
+
error(
|
|
85
|
+
`Platform with type "${rawOsType}" and architecture "${rawArchitecture}" is not supported by ${name}.\nYour system must be one of the following:\n\n${Object.keys(
|
|
86
|
+
supportedPlatforms,
|
|
87
|
+
).join(",")}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return platform;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const getPackage = () => {
|
|
95
|
+
const platform = getPlatform();
|
|
96
|
+
const url = `${artifactDownloadUrl}/${platform.artifactName}`;
|
|
97
|
+
let filename = platform.artifactName;
|
|
98
|
+
let ext = platform.zipExt;
|
|
99
|
+
let binary = new Package(platform, name, url, filename, ext, platform.bins);
|
|
100
|
+
|
|
101
|
+
return binary;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const install = (suppressLogs) => {
|
|
105
|
+
if (!artifactDownloadUrl || artifactDownloadUrl.length === 0) {
|
|
106
|
+
console.warn("in demo mode, not installing binaries");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const pkg = getPackage();
|
|
110
|
+
|
|
111
|
+
return pkg.install(suppressLogs);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const run = (binaryName) => {
|
|
115
|
+
const pkg = getPackage();
|
|
116
|
+
|
|
117
|
+
pkg.run(binaryName);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
install,
|
|
122
|
+
run,
|
|
123
|
+
getPackage,
|
|
124
|
+
};
|
package/install.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 3,
|
|
3
|
+
"name": "@raultov/browser-control-mcp",
|
|
4
|
+
"packages": {
|
|
5
|
+
"": {
|
|
6
|
+
"bin": {
|
|
7
|
+
"browser-control-mcp": "run-browser-control-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"detect-libc": "^2.1.2"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"prettier": "^3.8.3"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=14.14",
|
|
17
|
+
"npm": ">=6"
|
|
18
|
+
},
|
|
19
|
+
"hasInstallScript": true,
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"name": "@raultov/browser-control-mcp",
|
|
22
|
+
"version": "0.1.1-beta.1"
|
|
23
|
+
},
|
|
24
|
+
"node_modules/detect-libc": {
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=8"
|
|
27
|
+
},
|
|
28
|
+
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
31
|
+
"version": "2.1.2"
|
|
32
|
+
},
|
|
33
|
+
"node_modules/prettier": {
|
|
34
|
+
"bin": {
|
|
35
|
+
"prettier": "bin/prettier.cjs"
|
|
36
|
+
},
|
|
37
|
+
"dev": true,
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=14"
|
|
40
|
+
},
|
|
41
|
+
"funding": {
|
|
42
|
+
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
43
|
+
},
|
|
44
|
+
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
|
47
|
+
"version": "3.8.3"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"requires": true,
|
|
51
|
+
"version": "0.1.1-beta.1"
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,100 @@
|
|
|
1
1
|
{
|
|
2
|
+
"artifactDownloadUrls": [
|
|
3
|
+
"https://github.com/raultov/browser-control-mcp/releases/download/v0.1.1-beta.1"
|
|
4
|
+
],
|
|
5
|
+
"author": "raultov <raultov@protonmail.com>",
|
|
6
|
+
"bin": {
|
|
7
|
+
"browser-control-mcp": "run-browser-control-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"detect-libc": "^2.1.2"
|
|
11
|
+
},
|
|
12
|
+
"description": "MCP server to control and debug Chrome and Firefox at runtime: breakpoints, call frames and stepping over CDP and WebDriver BiDi/RDP, plus navigation, DOM, input, screenshots, console and network capture.",
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"prettier": "^3.8.3"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=14.14",
|
|
18
|
+
"npm": ">=6"
|
|
19
|
+
},
|
|
20
|
+
"glibcMinimum": {
|
|
21
|
+
"major": 2,
|
|
22
|
+
"series": 35
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/raultov/browser-control-mcp",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"development-tools::debugging",
|
|
27
|
+
"web-programming",
|
|
28
|
+
"command-line-utilities",
|
|
29
|
+
"mcp",
|
|
30
|
+
"browser",
|
|
31
|
+
"debugging",
|
|
32
|
+
"devtools",
|
|
33
|
+
"firefox"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
2
36
|
"name": "@raultov/browser-control-mcp",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
37
|
+
"preferUnplugged": true,
|
|
38
|
+
"repository": "https://github.com/raultov/browser-control-mcp",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"fmt": "prettier --write **/*.js",
|
|
41
|
+
"fmt:check": "prettier --check **/*.js",
|
|
42
|
+
"postinstall": "node ./install.js"
|
|
43
|
+
},
|
|
44
|
+
"supportedPlatforms": {
|
|
45
|
+
"aarch64-apple-darwin": {
|
|
46
|
+
"artifactName": "browser-control-mcp-aarch64-apple-darwin.tar.xz",
|
|
47
|
+
"bins": {
|
|
48
|
+
"browser-control-mcp": "browser-control-mcp"
|
|
49
|
+
},
|
|
50
|
+
"zipExt": ".tar.xz"
|
|
51
|
+
},
|
|
52
|
+
"aarch64-pc-windows-msvc": {
|
|
53
|
+
"artifactName": "browser-control-mcp-x86_64-pc-windows-msvc.zip",
|
|
54
|
+
"bins": {
|
|
55
|
+
"browser-control-mcp": "browser-control-mcp.exe"
|
|
56
|
+
},
|
|
57
|
+
"zipExt": ".zip"
|
|
58
|
+
},
|
|
59
|
+
"aarch64-unknown-linux-gnu": {
|
|
60
|
+
"artifactName": "browser-control-mcp-aarch64-unknown-linux-gnu.tar.xz",
|
|
61
|
+
"bins": {
|
|
62
|
+
"browser-control-mcp": "browser-control-mcp"
|
|
63
|
+
},
|
|
64
|
+
"zipExt": ".tar.xz"
|
|
65
|
+
},
|
|
66
|
+
"x86_64-apple-darwin": {
|
|
67
|
+
"artifactName": "browser-control-mcp-x86_64-apple-darwin.tar.xz",
|
|
68
|
+
"bins": {
|
|
69
|
+
"browser-control-mcp": "browser-control-mcp"
|
|
70
|
+
},
|
|
71
|
+
"zipExt": ".tar.xz"
|
|
72
|
+
},
|
|
73
|
+
"x86_64-pc-windows-gnu": {
|
|
74
|
+
"artifactName": "browser-control-mcp-x86_64-pc-windows-msvc.zip",
|
|
75
|
+
"bins": {
|
|
76
|
+
"browser-control-mcp": "browser-control-mcp.exe"
|
|
77
|
+
},
|
|
78
|
+
"zipExt": ".zip"
|
|
79
|
+
},
|
|
80
|
+
"x86_64-pc-windows-msvc": {
|
|
81
|
+
"artifactName": "browser-control-mcp-x86_64-pc-windows-msvc.zip",
|
|
82
|
+
"bins": {
|
|
83
|
+
"browser-control-mcp": "browser-control-mcp.exe"
|
|
84
|
+
},
|
|
85
|
+
"zipExt": ".zip"
|
|
86
|
+
},
|
|
87
|
+
"x86_64-unknown-linux-gnu": {
|
|
88
|
+
"artifactName": "browser-control-mcp-x86_64-unknown-linux-gnu.tar.xz",
|
|
89
|
+
"bins": {
|
|
90
|
+
"browser-control-mcp": "browser-control-mcp"
|
|
91
|
+
},
|
|
92
|
+
"zipExt": ".tar.xz"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"version": "0.1.1-beta.1",
|
|
96
|
+
"volta": {
|
|
97
|
+
"node": "18.14.1",
|
|
98
|
+
"npm": "9.5.0"
|
|
99
|
+
}
|
|
6
100
|
}
|