@yawlabs/vew-mcp
v0.4.2
Published
Browser MCP server for AI agents: drive a real Chromium via Playwright, with a compact token-efficient ref-addressable page snapshot at the center
Maintainers
Readme
@yawlabs/vew-mcp
A browser MCP server that lets AI agents drive a real Chromium optimally. Bring-your-own client: runs as a stdio MCP server so any MCP-compatible client (Claude Code, Claude Desktop, Cursor, mcph, …) can browse the web through a single persistent page.
One click adds this to your local Yaw MCP config so it's available in every Yaw Terminal session. Or install manually below.
Why a snapshot instead of the DOM
An agent driving a browser does not need the raw DOM — tens of thousands of tokens of markup, inline styles, and script. It needs to know which elements it can act on, what each one is, and a handle to act on it. That is what browser_snapshot returns:
# Login
URL: https://example.com/login
## Interactive elements
[e1] textbox "Email"
[e2] textbox "Password"
[e3] button "Sign in"
[e4] link "Forgot password?"
## Page text
Welcome back. Please sign in to continue...Then you act by ref: browser_type({ ref: "e1", text: "[email protected]" }), browser_click({ ref: "e3" }). Every action returns a fresh snapshot, so you always act off the latest view.
Refs are stable within a snapshot and renumber on the next one. Always use a ref from the most recent snapshot; a stale ref returns an error rather than clicking the wrong element after the page changed. This is the same model vew's own agent layer uses.
What it gives the model
| Tool | What it does |
|------|--------------|
| browser_navigate | Go to a URL, return a fresh snapshot of the loaded page |
| browser_snapshot | Core — compact ref-tagged element list + readable page text |
| browser_click | Click the element addressed by ref, return a fresh snapshot |
| browser_type | Type into the field addressed by ref (optionally submit), return a snapshot |
| browser_scroll | Scroll up / down / top / bottom, return a snapshot |
| browser_back / browser_forward | History navigation, each returns a snapshot |
| browser_get_text | Readable page text only (no element list) |
| browser_screenshot | PNG of the viewport or full page, as an image |
| browser_wait_for | Wait for text to appear / disappear (or a fixed time), then snapshot |
| browser_press_key | Press a key (Enter, Escape, Arrow*, Control+A, …), optionally on a ref |
| browser_hover | Hover a ref to reveal hover-only menus / tooltips |
| browser_file_upload | Set local file(s) on an <input type=file> ref |
| browser_evaluate | Run a JS expression in the page, return the JSON-serializable result |
| browser_handle_dialog | Set accept/dismiss for the next native dialog; report the last one |
One persistent Chromium + one page per server process. The browser launches lazily on the first navigation and tears down cleanly when the client disconnects.
Install & run
# One-off (auto-updates each spawn via @latest)
npx -y @yawlabs/vew-mcp@latest
# Or globally
npm i -g @yawlabs/vew-mcp
vew-mcpRequires Node ≥22. The first run needs the Chromium browser that Playwright drives:
npx playwright install chromium(One-time, ~150 MB. The server itself does not download it for you — keeping the npm package light.)
Set VEW_HEADLESS=0 to watch the browser instead of running it headless.
Driving an existing browser (Vew)
The default mode launches a self-owned headless Chromium. To drive an existing browser instead -- most commonly the Vew desktop browser when the user has enabled the MCP bridge in Vew's Agent panel -- set VEW_CDP_URL to the host's Chrome DevTools Protocol endpoint:
VEW_CDP_URL=ws://127.0.0.1:9222 npx -y @yawlabs/vew-mcp@latestIn attach mode the host owns the process lifetime: VEW_HEADLESS is ignored, and vew-mcp does not close the host's browser on exit -- it just disconnects. The active page is selected from the host's first browser context (the most-recently focused page). Native dialogs on the host page default to dismiss in attach mode (vs auto-accept when vew drives its own browser), so the agent does not silently confirm the human's prompts; use browser_handle_dialog to accept a specific one. The threat model is wider than the default mode: a prompt-injected instruction that says "navigate to file:///etc/passwd" navigates the user's actual browser, with their cookies and authenticated sessions, not an isolated Playwright Chromium. Enable the bridge in Vew only when you trust the agent.
Configure in Claude Code / Claude Desktop
Add to your client's MCP config (usually claude_desktop_config.json or ~/.claude.json):
{
"mcpServers": {
"vew": {
"command": "npx",
"args": ["-y", "@yawlabs/vew-mcp@latest"]
}
}
}Or via mcph:
mcph add vewTool reference
browser_navigate
{
url: string; // absolute http/https URL
wait_until?: "load" | "domcontentloaded" | "networkidle" | "commit"; // default "load"
timeout_ms?: number; // default 30000
}Navigates the persistent page and returns a fresh snapshot. Usually the first call.
browser_snapshot
{
text_budget?: number; // max chars of page text (default 8000)
}Returns the compact view of the current page: a ref-tagged element list followed by readable text. Each element line is [ref] role "name", with optional trailing (value: "...", checked, disabled). Prefer this over any raw-DOM approach.
browser_click
{
ref: string; // e.g. "e3", from the most recent snapshot
timeout_ms?: number; // default 10000
}Clicks the element and returns a fresh snapshot. Use for buttons, links, checkboxes, tabs, menu items.
browser_type
{
ref: string; // a textbox/searchbox/combobox ref
text: string; // value to type (field is cleared first)
submit?: boolean; // press Enter after typing (default false)
timeout_ms?: number; // default 10000
}Fills the field and optionally submits (e.g. to run a search). Returns a fresh snapshot.
browser_scroll
{
direction: "up" | "down" | "top" | "bottom";
}Scrolls one viewport (up/down) or jumps to the page extreme (top/bottom), then returns a fresh snapshot — useful to surface lazily-loaded content.
browser_back / browser_forward
No parameters. Navigate one entry in browser history and return a fresh snapshot. Errors if there is no entry in that direction.
browser_get_text
{
text_budget?: number; // max chars (default 8000)
}Returns the page's readable text (innerText), normalized and clamped — without the interactive-element list.
browser_screenshot
{
full_page?: boolean; // capture the whole scrollable page (default false = viewport)
}Returns a PNG as MCP image content. Prefer browser_snapshot for acting on the page; use a screenshot when you need to see pixels (layout, charts, visual verification). An over-cap image (very tall full_page) is refused with an error rather than swamping the client context — retake with full_page: false.
browser_wait_for
{
text?: string; // wait until this text becomes visible
text_gone?: string; // wait until this text is no longer visible
time_ms?: number; // OR just wait this many ms (max 30000)
timeout_ms?: number; // max wait for a text condition (default 10000)
}Provide at least one of text / text_gone / time_ms (when several are given they apply in that order), then get a fresh snapshot. Use it to settle async / SPA rendering before snapshotting instead of racing a half-rendered DOM.
browser_press_key
{
key: string; // "Enter", "Escape", "ArrowDown", "Control+A", …
ref?: string; // focus this element first; omit to target current focus
timeout_ms?: number; // default 10000
}Press a single key, optionally on a ref. With no ref the key goes to the focused element (e.g. Escape to close a modal). Returns a fresh snapshot.
browser_hover
{ ref: string; timeout_ms?: number }Hover the element to reveal hover-only menus, tooltips, or action icons, then return a fresh snapshot.
browser_file_upload
{
ref: string; // an <input type=file> ref (shows as a textbox in the snapshot)
paths: string[]; // absolute path(s) on the machine running the browser
timeout_ms?: number; // default 10000
}browser_evaluate
{
expression: string; // a JS expression evaluated in the page; result must be JSON-serializable
timeout_ms?: number; // abort with an error if it hasn't resolved (default 30000)
}The escape hatch for reading computed state or extracting structured data the snapshot does not surface. Wrap an object literal in parens: ({ a: 1 }). Runs in the page sandbox (not the server process); arbitrary JS can mutate the page, so it is not a read-only call. The call is bounded by timeout_ms so a slow/stalled expression returns control with an error instead of hanging the shared page (a synchronous infinite loop still pins the page's JS thread, so prefer terminating expressions).
browser_handle_dialog
{
accept?: boolean; // accept (default) or dismiss the next dialog
prompt_text?: string; // answer for a prompt(); ignored when accept is false
}In its own launched browser vew auto-accepts native dialogs (alert / confirm / prompt / beforeunload) by default, so a confirm-gated action proceeds and alert() text is captured rather than silently dismissed; when attached to an existing browser it dismisses by default so it does not hijack the human's dialogs. Pass accept to override that for the next dialog (and prompt_text to answer a prompt()); the override stays armed until a dialog actually fires, then reverts to the mode default. Calling with no arguments does not change the disposition — it just reports the last dialog and leaves the mode default in place (so a no-arg read can't accidentally arm an accept on the human's browser in attach mode).
How a typical session looks
browser_navigate { url: "https://news.ycombinator.com" }
-> snapshot with [e1] link "Hacker News", [e12] link "Show HN: ...", ...
browser_click { ref: "e12" }
-> fresh snapshot of the linked story
browser_back {}
-> back to the front page (refs renumbered)
browser_type { ref: "e3", text: "playwright", submit: true } // search box
-> fresh snapshot of resultsDevelopment
npm install
npx playwright install chromium # one-time, for live-browser usage
npm run build
npm test # vitest (pure snapshot logic + --version; no browser needed)
npm run lint # biome
npm run typecheckThe vitest suite exercises the token-shaping logic (ref numbering, element-line rendering, text normalization + clamping, full snapshot layout) without launching Chromium — the heavy browser binary is never required to verify the part of the code where the real complexity lives.
Release
This repo follows the YawLabs "CI trim" convention: no .github/workflows/release.yml. Releases ship locally:
npm login --auth-type=web # once, in your terminal (WebAuthn)
bash release.sh 0.2.0 # lint, test, build, bump, tag, publish, GitHub + MCP Registry releaserelease.sh is idempotent — re-running with the same version skips completed steps.
License
MIT © Yaw Labs
Links
- npm: https://www.npmjs.com/package/@yawlabs/vew-mcp
- issues: https://github.com/YawLabs/vew-mcp/issues
- Yaw Labs: https://vew.sh
