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.

Terminal
curl -fsSL https://raw.githubusercontent.com/aliilyas/bruster/main/install.sh | sh

Read 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:

Ubuntu / Debian
sudo apt-get update
sudo apt-get install libwebkit2gtk-4.1-0 xvfb xauth

Other 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:

CI / virtual display
xvfb-run -a bruster --hidden --quiet

Build 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.

From a repository checkout
cargo build --release --locked

Your 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:

JSON lines · stdin
{"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:

Example · stdout
{"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.

Python · smoke_test.py
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
opencode mcp add bruster -- bruster --mcp --quiet

See configurations for OpenCode, Claude Code, Claude Desktop, Cursor, Codex, and VS Code →

Command-line options

bruster [--mcp] [--hidden|--visible] [--quiet] [options]
Command-line flags and their effects
flag effect
--mcpuse the Model Context Protocol on stdio instead of the native protocol. This option also sets --hidden
--hidden / --visiblemove the windows off-screen / show the windows
--quietsend stderr to /dev/null. This mutes GL, ALSA, and GTK noise
--viewport WxHinitial window size, default 1280x800
--user-agent UAreplace the user agent
--profile DIRkeep cookies and storage between runs
--proxy URLhttp://host:port or socks5://host:port
--init-script FILEJavaScript that runs in every page before its own scripts
--darkpersist dark color-scheme for this profile (or the default state); use light to reset
--insecureignore TLS errors (Linux only)
--no-imagesdo 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:

Example snapshot
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-domains

Timeouts 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.

Browser methods, parameters, and results
method params result
navigateurl, 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 forwardnone{url}, or null if there is nowhere to go
url titlenonestring
evalexpressionJSON value. Promises are awaited. Compiled as a script
snapshotselector?, max? (1000), boxes?, format? (native default json, MCP default text){url,title,elements[],truncated?} or text lines
findtext? or regex? + snapshot paramsfiltered snapshot
screenshotpath?, full?, clip? {x,y,width,height}{path,bytes} or {png_base64}. Full pages are capped at 16000 px
recttarget{x,y,width,height} viewport-relative
clicktarget or x,y; force?, timeout?null
hovertargetnull
filltarget, valuenull
fill_formfields: [{selector|ref, value}]null (checkboxes bool, selects string/array)
typetarget, text, delay?, submit?null
presstarget?, key (Enter, Control+a)null
selecttarget, valueselected values
checktarget, checked?bool
dragfrom target, to targetnull
uploadtarget, paths[]file names set on the input
scrolltarget or x,y deltas{x,y} current position
resizewidth, height (1 to 16000){width,height}
text htmltargetstring
attrtarget, namestring or null
countselectornumber
wait_forselector/ref + state? (visible, attached, hidden), text, textGone, url, idle, time ms; timeout? msnull
networkclear?fetch/XHR entries with index, method and status where available, plus resource timing entries
network_requestindexone entry with req_headers, res_headers, and body (first 16 KB)
consoleclear?[{level,text,ms}] console output and uncaught errors
dialogaccept?, text?, clear?dialogs seen. Sets the answer for the next dialogs
cookiesnonelist
set_cookiename, value, domain?, path? (/)null
new_taburl?, timeout? ms (default 30000){tab}, or {tab,url} after load; does not switch the current tab
tabsnone[{tab,url,current}]
switch_tabtabnull
close_tab(uses tab)null; closing the last tab is rejected (use close)
closenoneexits

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 isTrusted set to false. Native select popups, OS shortcuts, and CSS :hover behavior 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: or data: 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 --proxy for 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.

Compare these trade-offs with Playwright and Puppeteer →

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.