Get up and running
Native browser automation,
one line at a time.
Install bruster, launch a native webview, and control it from your scripts or agent. A small Rust executable, a JSON-lines interface, and a built-in MCP server.
Install bruster
Prebuilt releases support macOS (arm64 and x86_64), Linux (x86_64 and arm64), and Windows (x86_64). The controller uses a native browser runtime, installed separately where required.
macOS and Linux
The installer selects your OS and CPU and puts bruster in ~/.local/bin. Add that directory to PATH if your shell does not already include it.
curl -fsSL https://raw.githubusercontent.com/aliilyas/bruster/main/install.sh | shRead the installation script or download an archive directly.
Windows
Download the Windows x86_64 archive from GitHub Releases, extract bruster.exe, and add its directory to PATH. Bruster requires the Microsoft Edge WebView2 Runtime. In PowerShell, run the executable with .\bruster.exe from its directory.
Linux and CI setup
Install the WebKitGTK 4.1 runtime. On Ubuntu or Debian:
sudo apt-get update
sudo apt-get install libwebkit2gtk-4.1-0 xvfb xauthOther distributions provide webkit2gtk-4.1 (Alpine) or webkit2gtk4.1 (Fedora). Hidden mode moves windows off-screen; WebKitGTK still needs a display. With no DISPLAY, bruster automatically tries xvfb-run -a. You can also launch it explicitly:
xvfb-run -a bruster --hidden --quietBuild from source
A source build requires Rust and your platform’s WebView development libraries. On Ubuntu, these include libwebkit2gtk-4.1-dev and libgtk-3-dev.
cargo build --release --lockedYour first browser session
Run bruster to see its browser window, or bruster --hidden to keep it off-screen. Send these requests one line at a time on stdin, waiting for each matching response before the next dependent operation:
{"id":1,"method":"navigate","params":{"url":"https://example.com"}}
{"id":2,"method":"snapshot","params":{"format":"text"}}
{"id":3,"method":"screenshot","params":{"path":"example.png"}}
{"id":4,"method":"close"}Responses and page events share stdout. Match responses using id; handle events separately. For example:
{"event":"load","tab":1,"url":"https://example.com/"}
{"id":1,"result":{"tab":1,"url":"https://example.com/"}}Keep stdin open while you use the session. Closing it shuts down the browser, so piping a batch of commands and immediately ending the stream is not a substitute for a request/response client.
Automate a browser with Python
Use the standard library to run a small smoke test. This example waits for matching response IDs, handles errors, checks the page title, and saves a screenshot. It requires the bruster executable on PATH.
import json
import subprocess
browser = subprocess.Popen(
["bruster", "--hidden", "--quiet"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
bufsize=1,
)
request_id = 0
def call(method, params=None):
global request_id
request_id += 1
request = {"id": request_id, "method": method, "params": params or {}}
browser.stdin.write(json.dumps(request) + "\n")
browser.stdin.flush()
for line in browser.stdout:
message = json.loads(line)
if message.get("id") != request_id:
continue # Page events can arrive between responses.
if "error" in message:
raise RuntimeError(message["error"])
return message.get("result")
raise RuntimeError("Browser exited before responding")
try:
call("navigate", {"url": "https://example.com", "timeout": 15000})
assert call("title") == "Example Domain"
print(call("snapshot", {"format": "text"}))
call("screenshot", {"path": "example.png"})
finally:
browser.stdin.close()
try:
browser.wait(timeout=5)
except subprocess.TimeoutExpired:
browser.kill()
browser.wait()
browser.stdout.close()For a test suite, add an overall process deadline in your test runner as well as per-request timeouts. Use fill, click, and wait_for for application-specific checks; the API reference lists their parameters.
Connect an MCP client
Start bruster with --mcp --quiet from your MCP client. Browser methods become tools, screenshots return as image content, and page events arrive as notifications.
opencode mcp add bruster -- bruster --mcp --quietSee configurations for OpenCode, Claude Code, Claude Desktop, Cursor, Codex, and VS Code →
Command-line options
bruster [--mcp] [--hidden|--visible] [--quiet] [options]| flag | effect |
|---|---|
--mcp | use the Model Context Protocol on stdio instead of the native protocol. This option also sets --hidden |
--hidden / --visible | move the windows off-screen / show the windows |
--quiet | send stderr to /dev/null. This mutes GL, ALSA, and GTK noise |
--viewport WxH | initial window size, default 1280x800 |
--user-agent UA | replace the user agent |
--profile DIR | keep cookies and storage between runs |
--proxy URL | http://host:port or socks5://host:port |
--init-script FILE | JavaScript that runs in every page before its own scripts |
--dark | persist dark color-scheme for this profile (or the default state); use light to reset |
--insecure | ignore TLS errors (Linux only) |
--no-images | do not load images. Pages load about 30% faster (Linux only) |
--profile DIR keeps cookies and storage between runs. Window and color preferences are stored separately from WebView data. See window and color controls for persistence details.
Native JSON-lines protocol
Each request is a JSON object with id, method, and optional params. Each reply carries the same id and either result or error. A result can be null.
Address an element using a CSS selector such as {"selector":"button[type=submit]"}, or a reference such as {"ref":"e3"} returned by the last snapshot or find. Request a fresh snapshot after the page changes.
Text snapshots
Native snapshots default to JSON; MCP snapshots default to text. Set format explicitly to choose. A text snapshot can look like this:
https://example.com/
Example Domain
e1 heading "Example Domain"
e2 text "This domain is for use in..."
e3 link "Learn more" href=https://iana.org/help/example-domainsTimeouts and navigation
navigate accepts until (load, dom, or idle) and a timeout in milliseconds. Page-runtime calls support an outer request_timeout, defaulting to at least 30,000 ms, or the requested timeout/time plus 5,000 ms if longer.
A request timeout stops waiting for the reply; it does not interrupt JavaScript already executing in the page. Navigation uses its own timeout. Native operations such as screenshots do not use request_timeout.
Dialogs never block, and beforeunload handlers are disarmed. Use dialog to configure responses and inspect dialog events. The close method shuts down all tabs; close_tab closes one tab.
Browser automation API reference
These methods are available through the native protocol and as MCP tools. A target is a selector or snapshot ref. Include tab in parameters to address a particular tab where supported.
On smaller screens, scroll the table horizontally to read all parameters and results.
| method | params | result |
|---|---|---|
navigate | url, until? (load default, dom, idle), timeout? ms (default 30000) | {tab, url} on load/DOM/idle, or a failure on timeout, DNS/error, or download (Linux) |
reload back forward | none | {url}, or null if there is nowhere to go |
url title | none | string |
eval | expression | JSON value. Promises are awaited. Compiled as a script |
snapshot | selector?, max? (1000), boxes?, format? (native default json, MCP default text) | {url,title,elements[],truncated?} or text lines |
find | text? or regex? + snapshot params | filtered snapshot |
screenshot | path?, full?, clip? {x,y,width,height} | {path,bytes} or {png_base64}. Full pages are capped at 16000 px |
rect | target | {x,y,width,height} viewport-relative |
click | target or x,y; force?, timeout? | null |
hover | target | null |
fill | target, value | null |
fill_form | fields: [{selector|ref, value}] | null (checkboxes bool, selects string/array) |
type | target, text, delay?, submit? | null |
press | target?, key (Enter, Control+a) | null |
select | target, value | selected values |
check | target, checked? | bool |
drag | from target, to target | null |
upload | target, paths[] | file names set on the input |
scroll | target or x,y deltas | {x,y} current position |
resize | width, height (1 to 16000) | {width,height} |
text html | target | string |
attr | target, name | string or null |
count | selector | number |
wait_for | selector/ref + state? (visible, attached, hidden), text, textGone, url, idle, time ms; timeout? ms | null |
network | clear? | fetch/XHR entries with index, method and status where available, plus resource timing entries |
network_request | index | one entry with req_headers, res_headers, and body (first 16 KB) |
console | clear? | [{level,text,ms}] console output and uncaught errors |
dialog | accept?, text?, clear? | dialogs seen. Sets the answer for the next dialogs |
cookies | none | list |
set_cookie | name, value, domain?, path? (/) | null |
new_tab | url?, timeout? ms (default 30000) | {tab}, or {tab,url} after load; does not switch the current tab |
tabs | none | [{tab,url,current}] |
switch_tab | tab | null |
close_tab | (uses tab) | null; closing the last tab is rejected (use close) |
close | none | exits |
Page events
Unsolicited messages use an event field instead of a response ID:
ready, navigating, load, tab_opened, tab_closed, dialog, tls_error (Linux), load_failed, and download (Linux).
Your client should continue reading until it finds the matching response ID. In MCP mode, page events are sent as notifications.
Know the boundaries
- Input uses synthetic DOM events, with
isTrustedset to false. Native select popups, OS shortcuts, and CSS:hoverbehavior are not reproduced. - Cross-origin iframes are out of reach. Same-origin iframes and open shadow roots are supported.
- Navigate to HTTP(S) URLs. Serve local pages over localhost rather than using
file:ordata:URLs. - Network and console inspection is patched into the page and resets on navigation. It does not capture the document request or request bodies. Use
--proxyfor real interception. - Set file inputs with
upload; bruster does not intercept native file-chooser dialogs. - Storage lasts for one session unless you use
--profile. - Full-page screenshots are capped at 16,000 pixels. macOS can cut very tall pages; Windows screenshots do not support
clip. - Linux hidden mode still needs a display or Xvfb, and WebKit timer throttling can affect timer-driven pages.
Build in the open
bin/bruster runs the development build. tests/run_all.sh runs the build, unit tests, smoke tests, edge tests, process checks, MCP launch checks, and end-to-end tests. On Linux, run the suite under Xvfb.
Find the source, report issues, and contribute on GitHub. Bruster is released under the MIT License.