chromewright
Chromewright is a local-first browser automation MCP server built on Chrome DevTools Protocol (CDP). It exposes a real Chrome or Chromium browser to MCP clients over stdio or loopback streamable HTTP, with high-level tools for navigation, page reading, tab management, screenshots, viewport emulation, and bounded interaction. The default build also includes a semantic terminal browser and a co-hosted loopback MCP companion for its live document state.
Use Chromewright when an agent needs browser state from a real browser without embedding a Node.js automation stack or writing raw CDP calls. Chromewright is not an end-to-end test runner; it is a browser control layer for AI agents and MCP clients.
When to use Chromewright
- Attach an MCP client to an existing Chrome or Chromium profile on a DevTools endpoint.
- Launch a dedicated local browser session for agent work.
- Read pages through snapshots, markdown extraction, targeted inspection, and link inventory.
- Browse a page's semantic DOM locally in the terminal, with a co-hosted MCP companion for its active document and selection state.
- Drive bounded interactions such as click, input, select, hover, key press, scroll, and wait.
- Capture managed PNG screenshots without letting callers choose arbitrary output paths.
- Reuse revision-scoped
cursorhandles from snapshots instead of relying only on CSS selectors.
Installation
Chromewright requires Rust 1.88 or newer when you install or build it with Cargo.
Install from crates.io:
Install with Homebrew:
Install from source:
You can also download prebuilt archives from GitHub Releases and place the chromewright binary on your PATH.
Verify the binary is available:
Expected output:
chromewright <version>
Quickstart
This path starts a visible Chrome profile with DevTools enabled, then serves Chromewright over loopback HTTP at http://127.0.0.1:3000/mcp.
Prerequisites
chromewrighton yourPATH- Chrome, Chromium, or another CDP-compatible browser
- An MCP client that supports streamable HTTP or stdio servers
1. Start a dedicated browser profile
On macOS, run:
Use a dedicated profile when you do not want agent automation attached to your personal browser session. The default Chromewright attach mode expects DevTools at http://127.0.0.1:9222.
2. Start Chromewright
Run the streamable HTTP server:
Expected log line:
Ready to accept MCP connections at http://127.0.0.1:3000/mcp
3. Connect an MCP client
For JSON-configured clients that support streamable HTTP:
For Codex over stdio, let the client start the server:
[]
= "/absolute/path/to/chromewright"
= true
For Codex against the long-lived HTTP server from step 2:
[]
= "http://127.0.0.1:3000/mcp"
= true
4. Verify with the client
Use your MCP client to call tab_list. A connected session should return at least one tab with a stable tab_id. If no active tab is useful, call new_tab before calling snapshot.
Browser modes
Chromewright has two browser modes:
| Mode | How to start | What it does |
|---|---|---|
| Attach | Run chromewright or chromewright serve with no launch flags. |
Connects to http://127.0.0.1:9222 by default. |
| Attach to another endpoint | Pass --ws-endpoint <URL>. |
Connects to a browser WebSocket URL or a DevTools HTTP origin such as http://127.0.0.1:9333. |
| Launch | Pass any launch flag such as --user-data-dir, --headless, --executable-path, or --debug-port. |
Starts a local browser session. Launch mode is headed unless you pass --headless. |
Examples:
# Default: attach to http://127.0.0.1:9222 and serve MCP over stdio.
# Serve streamable HTTP on the default loopback endpoint.
# Serve streamable HTTP on a custom port and path.
# Attach to a different DevTools endpoint.
# Launch a visible browser with a dedicated profile.
# Launch a headless browser and serve streamable HTTP.
# Seed two managed tabs at startup. This works with attach, launch, serve, and tui modes.
--url is repeatable and seeds one separate managed tab for every value, in
the order supplied; the final seeded tab becomes active. It accepts absolute
http:, https:, and about: URLs. Relative, unsafe, and protocol-relative
URLs are rejected before any startup tab is opened. In attach mode, existing
tabs are untouched; only seeded tabs become managed by chromewright.
CLI reference
| Option or command | Default | Description |
|---|---|---|
chromewright |
stdio transport | Starts the MCP server over stdio. |
chromewright serve |
127.0.0.1:3000/mcp |
Starts the MCP server over loopback streamable HTTP. |
serve --port <PORT>, serve -p <PORT> |
3000 |
Sets the HTTP port. |
serve --http-path <PATH> |
/mcp |
Sets the HTTP endpoint path. |
--ws-endpoint <URL> |
http://127.0.0.1:9222 when no launch flags are present |
Connects to an existing browser WebSocket URL or DevTools HTTP origin. This conflicts with launch flags. |
--headless |
false |
Launches a new browser in headless mode. |
--executable-path <PATH> |
auto-detected by the browser backend | Uses a specific browser executable in launch mode. |
--user-data-dir <DIR> |
backend default | Uses a persistent browser profile directory in launch mode. |
--debug-port <PORT> |
auto-selected | Uses a specific DevTools port for a locally launched browser. |
--url <URL> |
none; repeatable | Seeds each supplied safe URL in its own managed startup tab. The final seeded tab is active. |
chromewright tui |
(feature tui, default-on) |
Starts the semantic terminal browser against the same browser session. Always co-hosts a loopback MCP companion. |
tui --config <PATH> |
$XDG_CONFIG_HOME/chromewright/tui.toml or ~/.config/chromewright/tui.toml |
TOML keymap plus optional [theme] and [layout] overlays. An explicit path must exist and parse; a missing default file keeps built-in bindings. |
tui --companion-port <PORT> |
0 (ephemeral) |
Loopback port for the co-hosted streamable-HTTP MCP companion. |
tui --companion-path <PATH> |
/mcp |
HTTP path for the co-hosted companion. |
--browser-session <reuse|restart> |
reuse with --headless tui |
Reuse or replace Chromewright's owned managed headless browser. Only valid with --headless tui. |
Source: src/bin/mcp_server.rs.
Terminal browser (TUI)
The default build includes a semantic terminal browser. It attaches to the same Chromewright browser session and renders semantic DOM content only (not pixels, CSS, or browser layout).
# Attach to an existing DevTools endpoint (default http://127.0.0.1:9222).
# Managed private headless Chrome for the normal terminal-browser flow.
To connect an MCP client to the co-hosted companion, choose a fixed loopback
port; the default 0 selects an ephemeral port that the TUI does not print.
[]
= "http://127.0.0.1:3334/mcp"
= true
The companion exposes eight tui_* coordination tools and bounded semantic
resources. semantic_ref values are tied to one document revision; the active
capture plus the previous eight revisions are retained, while stale or evicted
references fail closed. Attention messages are limited to 512 characters.
Semantic Markdown resources paginate at 32,000 characters by default and cap
at 200,000; JSON resources fail instead of returning a truncated document.
The header is a single browser-like bar: tab ordinal (2/5) left of the history arrows, then location/title, with a lifecycle glyph on the right. Keyboard bindings are not shown in the terminal chrome. Defaults are Vimari-compatible. Multi-key sequences such as gg and gi wait for the full chord; an unbound prefix is rejected rather than re-firing the last key.
Default keymap
Browser-first (Vimari) defaults, with md-tui-style aliases where they do not collide. Overlaying an action replaces all of its sequences (primary + aliases).
| Key | Action name | Behavior |
|---|---|---|
f / s |
link_hints_follow |
Enter hint mode over viewport-visible links and form controls; type the two-key label. Links follow in the current tab; text inputs start edit mode; other controls are selected. One label per target (first line only if wrapped). (s = md-tui select-link.) |
F / S |
link_hints_new_tab |
Same targets as f; links open in a new tab (hint mode stays open for chaining until Esc). Form targets still select/edit in the current tab. (S = md-tui select-link alt.) |
j / ↓ |
scroll_down |
Scroll or move selection down one block. |
k / ↑ |
scroll_up |
Scroll or move selection up one block. |
h |
scroll_left |
Horizontal scroll left when content overflows. (Not md-tui half-page; browser tables/code need pan.) |
l |
scroll_right |
Horizontal scroll right when content overflows. |
u / → |
half_page_up |
Pan the view half a page up (selection unchanged). |
d / ← |
half_page_down |
Pan the view half a page down (selection unchanged). |
Ctrl-u |
page_select_up |
Move selection up by about half a page. |
Ctrl-d |
page_select_down |
Move selection down by about half a page. |
gg |
go_top |
Jump to the document top. (Single g is not bound so gi stays available.) |
G |
go_bottom |
Jump to the document bottom. |
gi |
focus_first_input |
Focus the first form control and start editing. |
H / b |
history_back |
Browser history back. (b = md-tui back.) |
L |
history_forward |
Browser history forward. |
r |
reload |
Reload the active page. |
[ |
prev_tab |
Switch to the previous browser tab. |
] |
next_tab |
Switch to the next browser tab. |
x |
close_tab |
Close the current tab. |
t |
new_tab |
Open a new tab. |
o |
open_url |
Open the URL entry prompt with an empty buffer. While editing, Tab / Shift-Tab accept or cycle local URL history (ghost suffix shows the best match). |
O |
edit_url |
Open the URL entry prompt prefilled with the current address (edit from the end). Same Tab history completion as o. |
/ |
search |
Start forward search by exact semantic content. (md-tui also binds f to search; we keep f for link hints.) |
n |
search_next |
Repeat the last search forward. |
N |
search_previous |
Repeat the last search backward. |
Space |
collapse |
Collapse or expand the selected block. |
zw |
toggle_wrap |
Toggle soft word-wrap (on by default). When on, long lines wrap to the viewport and h/l horizontal pan is disabled. |
zs |
toggle_structure |
Toggle prose (default) vs structure projection. Prose hides landmark/list/group chrome and flattens indent; structure shows DOM-like containers. Collapse (Space) only works in structure mode. |
i |
inspect |
Open a compact inspect panel under the selected block; title is the full DOM path (main > … > tag#id), body has action fields and ref/rev; follows selection until Esc. |
y |
copy_block |
Copy selection: link/image URL (resolved), otherwise rendered block text (OSC 52). |
Y |
copy_ref |
Copy the opaque semantic_ref (OSC 52). |
Tab |
tab_next |
Next focusable control; writes the leaving field to the live DOM + same-doc patch, then moves focus. |
Shift-Tab |
tab_prev |
Previous focusable control; same write-on-leave behavior as Tab. |
Enter |
confirm |
Text input: apply value to the live DOM + same-doc patch (no submit needed; live search works). Checkbox/radio: toggle. Select: cycle options. Submit button: write staged fields + click + recapture. Link/other: activate. |
Esc |
escape |
Leave prompt, hint, or inspect mode. In Normal mode, also clears sticky /search footer (/{query} n/m). After a failed page action, dismiss Error back to Ready (retained page stays) so keys work again. |
w |
toggle_full_width |
Toggle full-width content vs the configured content_max_width column (default 100, capped by default). |
e |
edit_external |
Open the current page’s semantic markdown in $VISUAL, then $EDITOR, then vi (read-only; TUI suspends like Nereid). |
q / Ctrl-c |
quit |
Quit the TUI. |
Hints use deterministic two-key labels from the alphabet asdfgqwertzxcvb (for example aa, as), assigned to viewport-visible links and form controls (one label per target; painted on the first line only when a target wraps).
After navigation or a link follow settles, a URL fragment such as #section moves the TUI selection to the matching component (id, then named anchor), expands collapsed ancestors, and scrolls it into view. Unmatched fragments keep the prior selection.
The content pane uses a terminal-native ANSI-16 role palette (clearer heading ladder H1–H6, blue links, light-cyan forms, yellow hints) with reverse-video selection applied last. Colors inherit the terminal light/dark theme; override individual roles under [theme] in tui.toml. By default the markdown/content area has 1 column of left/right padding and is capped at 100 columns (centered; header and footer stay full width); press w for full width. A one-column Amp-style block scrollbar sits on the far right of the content band. Override layout under [layout] in tui.toml.
Default reading mode is prose (markdown-like): no ▾ [main] / ol / group chrome, fully flat lines. Press zs for structure (DOM-like outline). Wrap (zw) and structure (zs) are not shown in the header bar; toggle feedback appears in the status line.
Search follows Vim semantics: a new /pattern starts after the current selection and wraps at the end; n repeats forward, N repeats backward, and submitting an empty / prompt repeats the previous pattern. The footer shows the cmdline while typing (/…) and keeps /{pattern} n/m while a search is active — press Esc in Normal mode to clear it (Esc while typing /… only cancels the prompt and keeps the prior pattern for n/N). Link hints (f / F) also use the footer (f as) rather than the header. Bracketed paste is accepted only in URL, search, and form input modes and is bounded to 4096 characters.
Custom keymap
Bindings are replaceable by action name. --config PATH takes precedence; if omitted, Chromewright reads $XDG_CONFIG_HOME/chromewright/tui.toml, falling back to ~/.config/chromewright/tui.toml. A missing default file keeps the built-ins; an explicitly requested file must parse successfully.
# Only list keys you want to change. Unknown names or conflicting
# bindings abort startup rather than partially applying the overlay.
[]
= "ctrl-r"
= "ctrl-q"
= "shift-tab"
# Optional content-pane padding + column width (header/footer stay full width).
# Defaults: 1 col L/R, 0 row T/B, content_max_width = 100 (0 = always full).
# Press w to toggle full width vs the capped column.
[]
# content_padding_x = 1
# content_padding_y = 0
# content_max_width = 100
[]
# Optional ANSI names, reset, or #rrggbb — defaults already use a clear ladder.
# link = "blue"
# h1 = "lightblue"
# h2 = "green"
# h3 = "magenta"
# h4 = "cyan"
# h5 = "yellow"
# h6 = "lightred"
# form_control = "lightcyan"
# hint_label = "yellow"
Binding specs accept single keys (r, space, esc, enter, tab), multi-key letter sequences (gg, gi), and chords with -, +, or space separators (ctrl-c, C-c, shift-tab). Supported named keys include esc, enter, tab, backtab / shift-tab, backspace, arrow keys, home, end, pageup / pgup, pagedown / pgdn, space, and function keys such as f1.
Theme roles: link, h1–h6, landmark, group, list, image, form_control, hint_label, muted, chrome_ready, chrome_loading, chrome_error, chrome_mode, attention_fg, attention_bg.
Layout keys: content_padding_x, content_padding_y (symmetric inset around the markdown/content pane only), content_max_width (default 100; 0 disables the cap; w toggles full vs capped).
The source of truth for defaults is src/tui/keymap.rs and src/tui/action.rs.
Tool workflow
A typical agent workflow is:
- Call
tab_listornew_tabto establish an active tab. - Call
snapshotto read the current page and collect actionable nodes. - Prefer a fresh
cursorfromsnapshotorinspect_nodewhen targeting follow-up actions. - Use
inspect_node,get_markdown,extract, orread_linksfor more focused reads. - Use
click,input,select,hover,press_key,scroll,wait, or tab tools for bounded interaction. - Call
snapshotagain after navigation, DOM-changing actions, viewport changes, or ambiguous target recovery.
snapshot supports these modes:
| Mode | Use it when |
|---|---|
viewport |
You want the default local reread of the current visible scope. |
delta |
You want the changed local surface when a compatible prior snapshot base exists. |
full |
You need an exhaustive page-wide read. |
DOM-targeted tools accept a public target object:
or:
Selector strings are still accepted for compatibility by tools that use the public target type, but the object form is the canonical contract.
Production MCP tool surface
Production MCP sessions register the default high-level tools plus the guarded operator tool evaluate.
| Category | Tools |
|---|---|
| Navigation | navigate, go_back, go_forward, wait |
| Interaction and viewport | click, input, select, hover, press_key, scroll, set_viewport |
| Tabs and lifecycle | new_tab, tab_list, switch_tab, close_tab, close |
| Reading and inspection | snapshot, inspect_node, get_markdown, extract, read_links |
| Managed artifacts | screenshot |
| Operator diagnostics | evaluate |
evaluate executes JavaScript in the active page and requires confirm_unsafe = true on each call. It is available for diagnostics and escape-hatch inspection when bounded tools cannot answer a page-specific question.
Source: src/tools/core/mod.rs and src/browser/session.rs.
Screenshots and viewport emulation
Use screenshot when a caller needs a managed PNG artifact. The tool accepts:
mode:viewport,full_page,element, orregionscale:deviceorcss- optional
tab_id - optional
targetforelementcaptures - optional
regionforregioncaptures
Successful calls return managed artifact metadata, including artifact_uri, artifact_path, mime_type, byte_count, image dimensions, CSS dimensions, device pixel ratio, pixel scale, revealed_from_offscreen, and optional clip data. Callers do not provide output paths.
Use set_viewport to emulate responsive breakpoints through CDP. Successful calls return viewport_metrics_after; later snapshot calls expose the live metrics under scope.viewport.
Safety boundaries
- Attach mode can see the tabs, cookies, and authenticated state in the browser profile you connect to.
- Use a dedicated browser profile for agent work when you do not want automation attached to a personal browser session.
evaluaterequiresconfirm_unsafe = truebecause it runs arbitrary JavaScript in the active page.navigateandnew_taballowhttp:,https:,about:, and same-origin relative paths by default.data:,file:, other absolute schemes, and protocol-relative URLs requireallow_unsafe = true.go_backandgo_forwardapply the same unsafe-scheme gate and revert rejected history moves.close_tabrequiresconfirm_destructive = truebefore closing an unmanaged active tab in a connected session.closerequiresconfirm_destructive = truebefore expanding connected-session cleanup from managed tabs to all tabs.cursorandnode_reftargets are revision-scoped. After navigation or DOM-changing actions, refresh withsnapshot.
Operation metrics
Finished tool results include operation_metrics metadata when a tool records non-zero metrics. operation_metrics.output_bytes is optional; it appears only when a tool path measures the exact serialized output size.
Measured paths may include:
- browser evaluation count
- poll iterations
- DOM extraction count and extraction time
- last DOM node count
- snapshot render time
- handoff rebuild count and time
- exact serialized output size when measured
Run the focused operation metrics tests:
Local development
Build from source:
Run the normal test suite:
Run the browser smoke suite from the repository root:
The smoke script runs:
Browser smoke checks launch a local browser and are intended for maintainer workstations. CI covers formatting, clippy, MSRV, cargo check, tests, and packaging without requiring a live browser attach target.
Source anchors
- Package metadata and Rust version: Cargo.toml
- CLI flags and transports: src/bin/mcp_server.rs
- Tool registry: src/tools/core/mod.rs
- MCP handler: src/mcp/handler.rs
- Public target contract: src/contract/target.rs
- Screenshot contract: src/tools/screenshot.rs
- TUI default keymap and actions: src/tui/keymap.rs, src/tui/action.rs
- Browser smoke script: scripts/browser-smoke.sh
License
MIT