Lets an AI agent drive a self-hosted Chrome session that a human can watch and take over through a live noVNC view.
Also known as AetherAI3/agent-browser, aether-browser
- Stars
- 14
- Tools
- —
- Downloads, 30 days
- 252
- Latest version
- 0.2.2
- Last commit
- Oct 27, 2026
- License
- Apache-2.0
What is Agent Browser MCP?
Agent Browser lets an AI agent drive a self-hosted Chrome session that a human can watch and take over through a live noVNC view.
Summary generated from the project's README
About Agent Browser MCP server
Developers who need to monitor and intervene in AI-driven web browsing can use agent-browser to manage a single headed Chrome session. Unlike standard headless tools that only provide transcripts, this server allows you to watch the agent's actions in real time through a live noVNC view. This setup is ideal for handling complex scenarios like two-factor authentication or login prompts where an AI might otherwise stall or guess incorrectly.
The server is self-hosted using Docker Compose on a Linux host, which manages the Xvfb, x11vnc, and noVNC stack. It provides nine specific tools for MCP clients like Claude Desktop, Cursor, or Windsurf, including browser_navigate, browser_click, and browser_type. Users can interact with the session via a JSON API or through dedicated Node.js and Python clients. Because the agent and the human participant interact with the exact same browser instance, you can step in to resolve issues and then hand control back to the model.
What you can do with it
- 1Monitoring agent web navigationDevelopers can watch a live stream of the browser session to ensure the AI is navigating to the correct URLs.
- 2Automating structured web tasksAn AI client can use specific tools to click elements, type text, and scroll through pages to complete a web-based workflow.
Written from the project's README. Every use case is checked against a quote from it.
How to install Agent Browser MCP
Formats verified 10 Nov 2026Install Agent Browser MCP in Claude Code
$ claude mcp add --env AGENT_BROWSER_CONTROLLER_TOKEN=<YOUR_AGENT_BROWSER_CONTROLLER_TOKEN> --env AGENT_BROWSER_OBSERVER_TOKEN=<YOUR_AGENT_BROWSER_OBSERVER_TOKEN> agent-browser -- npx -y aether-browserInstall Agent Browser MCP in Cursor
~/.cursor/mcp.json
{
"mcpServers": {
"agent-browser": {
"command": "npx",
"args": [
"-y",
"aether-browser"
],
"env": {
"AGENT_BROWSER_CONTROLLER_TOKEN": "<YOUR_AGENT_BROWSER_CONTROLLER_TOKEN>",
"AGENT_BROWSER_OBSERVER_TOKEN": "<YOUR_AGENT_BROWSER_OBSERVER_TOKEN>"
}
}
}
}Install Agent Browser MCP in VS Code
.vscode/mcp.json
{
"servers": {
"agent-browser": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"aether-browser"
],
"env": {
"AGENT_BROWSER_CONTROLLER_TOKEN": "<YOUR_AGENT_BROWSER_CONTROLLER_TOKEN>",
"AGENT_BROWSER_OBSERVER_TOKEN": "<YOUR_AGENT_BROWSER_OBSERVER_TOKEN>"
}
}
}
}Install Agent Browser MCP in Claude Desktop
claude_desktop_config.json
{
"mcpServers": {
"agent-browser": {
"command": "npx",
"args": [
"-y",
"aether-browser"
],
"env": {
"AGENT_BROWSER_CONTROLLER_TOKEN": "<YOUR_AGENT_BROWSER_CONTROLLER_TOKEN>",
"AGENT_BROWSER_OBSERVER_TOKEN": "<YOUR_AGENT_BROWSER_OBSERVER_TOKEN>"
}
}
}
}Install Agent Browser MCP in Codex
~/.codex/config.toml
[mcp_servers.agent-browser] command = "npx" args = ["-y", "aether-browser"] [mcp_servers.agent-browser.env] AGENT_BROWSER_CONTROLLER_TOKEN = "<YOUR_AGENT_BROWSER_CONTROLLER_TOKEN>" AGENT_BROWSER_OBSERVER_TOKEN = "<YOUR_AGENT_BROWSER_OBSERVER_TOKEN>"
Install Agent Browser MCP in Gemini CLI
~/.gemini/settings.json
{
"mcpServers": {
"agent-browser": {
"command": "npx",
"args": [
"-y",
"aether-browser"
],
"env": {
"AGENT_BROWSER_CONTROLLER_TOKEN": "<YOUR_AGENT_BROWSER_CONTROLLER_TOKEN>",
"AGENT_BROWSER_OBSERVER_TOKEN": "<YOUR_AGENT_BROWSER_OBSERVER_TOKEN>"
}
}
}
}Configuration
AGENT_BROWSER_CONTROLLER_TOKENSecretController token, if the server runs authenticated.
AGENT_BROWSER_CONTROLLER_TOKENSecretController token, if the server runs authenticated.
AGENT_BROWSER_OBSERVER_TOKENSecretObserver token for read-only health and snapshot calls.
AGENT_BROWSER_OBSERVER_TOKENSecretObserver token for read-only health and snapshot calls.
AGENT_BROWSER_URLBase URL of your Agent Browser server. Defaults to http://127.0.0.1:8092.
AGENT_BROWSER_URLBase URL of your Agent Browser server. Defaults to http://127.0.0.1:8092.
SESSION_ID
| Name | Description | |
|---|---|---|
| AGENT_BROWSER_CONTROLLER_TOKEN | Controller token, if the server runs authenticated. | Secret |
| AGENT_BROWSER_CONTROLLER_TOKEN | Controller token, if the server runs authenticated. | Secret |
| AGENT_BROWSER_OBSERVER_TOKEN | Observer token for read-only health and snapshot calls. | Secret |
| AGENT_BROWSER_OBSERVER_TOKEN | Observer token for read-only health and snapshot calls. | Secret |
| AGENT_BROWSER_URL | Base URL of your Agent Browser server. Defaults to http://127.0.0.1:8092. | |
| AGENT_BROWSER_URL | Base URL of your Agent Browser server. Defaults to http://127.0.0.1:8092. | |
| SESSION_ID | — |
Automated checks
Ran Nov 12, 2026Full report →Passed our automated checks. That is not a security audit: read the code and give the server the narrowest access it needs. How checks work
- Source repository linked and reachablePass
- Open source license (OSI)Pass
- Commit or release in the last yearPass
- Package name not close to a popular packagePass
- Remote endpoint uses HTTPSNot checked
- Destructive tools are labelledNot checked
- No hidden instructions to the modelPass
- No critical advisory in the package or its dependenciesPass
README
View on GitHubWhy this exists
Most browser tooling for agents hands the model a browser you cannot see. When it misreads a page or stalls on a login, all you get is a transcript and a guess.
Agent Browser runs one headed Chrome session. Your agent drives it through a small JSON API, and a live view of that same session sits open in front of you. When the agent gets stuck, you take over in the window it is already using — then hand it back.
Start the runtime:
docker compose up --build
Then reach it however you like:
npm install aether-browser # TypeScript
pip install aether-browser # Python
claude mcp add agent-browser -- npx -y aether-browser mcp # any MCP client
Quickstart
One command, on Docker Engine for Linux with Compose v2. It builds from the source checkout and starts Xvfb, x11vnc, noVNC, and the API — the stack that owns a single headed Chrome session. Both user-facing listeners bind to numeric loopback.
docker compose up --build
Once health responds, open the live view at
127.0.0.1:6080/vnc.html and check the API from another terminal:
curl -fsS http://127.0.0.1:8092/browser/health | jq .
Ctrl+C stops it.
The first build takes several minutes. It installs the hash-locked Python environment, then uses Patchright to install the current Google Chrome Stable package. The exact browser version is captured with each accepted image, so rebuilding the same source later may pick up a newer Stable.
Linux host networking is deliberate. It keeps the unauthenticated v0.x noVNC surface on numeric loopback. Docker Desktop and remote-host deployment are outside this quickstart.
The API in four calls
With the runtime healthy and curl plus jq installed. Local loopback needs no bearer token by
design — read the authority contract before you change the
deployment shape.
# Open the session. This is the browser you are about to watch.
SESSION_ID="$(curl -fsS -X POST http://127.0.0.1:8092/browser/session/create \
-H 'Content-Type: application/json' \
-d '{"api_version":"v1"}' | jq -er '.session_id')"
# Go somewhere. The page changes in the live view as this runs.
curl -fsS -X POST http://127.0.0.1:8092/browser/navigate \
-H 'Content-Type: application/json' \
-d "{\"api_version\":\"v1\",\"session_id\":\"${SESSION_ID}\",\"url\":\"https://example.com\"}" \
| jq '{status, final_url, title, readable_text}'
# Read it back as structure, not pixels.
curl -fsS -X POST http://127.0.0.1:8092/browser/snapshot \
-H 'Content-Type: application/json' \
-d "{\"api_version\":\"v1\",\"session_id\":\"${SESSION_ID}\"}" \
| jq '{status, url, title, sequence, vision_steps_remaining}'
# Give the browser back.
curl -fsS -X POST http://127.0.0.1:8092/browser/session/end \
-H 'Content-Type: application/json' \
-d "{\"api_version\":\"v1\",\"session_id\":\"${SESSION_ID}\"}" | jq .
What that did: session/create started one headed Chrome session and returned its UUID plus the
local view URL. navigate validated the destination, pinned the allowed addresses, and changed the
page on the shared display. snapshot returned bounded text, accessibility state, viewport
metadata, counters, and a PNG of that same page. At no point did the API and the human view create
competing browsers — they met at one owned session.
The same flow ships as examples/curl.sh.
Use it from an MCP client
Any MCP client — Claude Code, Claude Desktop, Cursor, Windsurf — can drive the session while you watch, and take over when it gets stuck.
claude mcp add agent-browser -- npx -y aether-browser mcp
Already have the Python client? aether-browser mcp serves the same nine tools.
{
"mcpServers": {
"agent-browser": {
"command": "npx",
"args": ["-y", "aether-browser", "mcp"]
}
}
}
Nine tools — browser_open · browser_navigate · browser_read · browser_click ·
browser_type · browser_press · browser_scroll · browser_status · browser_close
browser_open hands back the live view URL, and every response after it repeats that URL, so you
always know where to look. On connect, the server tells the model to stop and ask for a takeover at
a login, a payment, or a 2FA prompt rather than guessing — the loop in the recording above.
Setup and limits: docs/MCP.md.
Node and TypeScript client
The same API from Node, with types and cleanup you cannot forget:
npm install aether-browser
import { AgentBrowser, withSession } from 'aether-browser'
const browser = new AgentBrowser({ controllerToken: process.env.AGENT_BROWSER_CONTROLLER_TOKEN })
await withSession(browser, async (session) => {
const page = await session.navigate('https://example.com')
await session.click({ selector: '#login' })
await session.type({ selector: '#user', text: 'ada' })
console.log(page.title, session.viewUrl)
})
withSession always ends the session, including when your callback throws, so a crash cannot leave
the single slot occupied. No runtime dependencies, and it runs anywhere that can reach the server.
It carries a small CLI too: npx aether-browser doctor tells you what is missing before a first
run, and up builds and starts the runtime on a Linux host.
Python client
The same client, same name, same commands, released version for version with the npm package:
pip install aether-browser
import os
from aether_browser import AgentBrowser, session
browser = AgentBrowser(controller_token=os.environ["AGENT_BROWSER_CONTROLLER_TOKEN"])
with session(browser) as live:
page = live.navigate("https://example.com")
live.click(selector="#login")
live.type("ada", selector="#user")
print(page["title"], live.view_url)
The session context manager always ends the session, including when the body raises. No runtime
dependencies — the transport is urllib from the standard library — ships type hints, and runs on
Python 3.10 or newer, anywhere that can reach the server. Same CLI as the npm package:
aether-browser doctor, up, status, open, down.
Optional Jev web decisions (Python)
The Python client has an opt-in aether_browser.jev layer for Jev → your selected model.
Jev reads a caller-selected text excerpt and chooses between up to three caller-approved URLs,
handoff, or human takeover. The browser navigates only to a URL the caller supplied, and its
normal destination checks still apply. On handoff, your callback receives the current page
evidence and Jev's request IDs, token counts, and costs; your application invokes its chosen
reasoning model to write, analyze, or continue browsing. Jev returns decisions, not prose or
visual judgments, so screenshots and complex page interactions belong to that selected model or
the human watching the live browser.
import os
from aether_browser import AgentBrowser, session
from aether_browser.jev import HumanTakeover, JevWebAgent, NavigationOption, OpenRouterJev
browser = AgentBrowser(controller_token=os.environ["AGENT_BROWSER_CONTROLLER_TOKEN"])
with session(browser) as live:
first_page = live.navigate("https://example.com")
result = JevWebAgent(OpenRouterJev(os.environ["OPENROUTER_API_KEY"])).run(
live,
goal="Read the site's documentation",
initial_page=first_page,
options_for=lambda page: (
[NavigationOption("https://example.com/docs", "Official documentation")]
if page["final_url"] == "https://example.com/"
else []
),
excerpt_for=lambda page: page["readable_text"][:6000],
selected_model=lambda handoff: your_model(handoff), # Supply your own model callback.
)
if isinstance(result, HumanTakeover):
print("Take control at", result.view_url)
input("Press Enter when the human is done to close this session: ")
else:
print(result)
The example's your_model is an application-defined function, not part of this package. The
excerpt_for callback explicitly chooses text sent to OpenRouter, and options_for explicitly
chooses candidate URLs. Never return private page content or signed URLs unless your application
intends to send them to that provider. The OpenRouter key stays in the calling process; the
browser server does not store it. No Jev request occurs unless you call this optional layer.
If Jev fails or returns an invalid decision, the selected-model callback receives a handoff
with reason="jev_unavailable" and the browser takes no additional action. Recognized authentication
or payment pages prompt human takeover before Jev receives their text. The session remains owned
by your code; keep it open while the human takes over. See docs/JEV.md for
the complete contract and limitations.
What makes it different
- One session, two participants. The agent acts through JSON. You watch the same display, and take the controls whenever you want them.
- Structure before pixels. Readable text and a bounded accessibility tree come back before you spend a vision step on a screenshot.
- A small control surface. The v0.x API exposes explicit browser actions — not a shell, not arbitrary JavaScript, not raw DevTools.
- Model-agnostic and self-hosted. Bring the framework you already use, and keep the browser on hardware you control.
What it does today
| Capability | v0.x contract |
|---|---|
| Browser | One headed Google Chrome Stable session launched through Patchright |
| State | URL, title, readable text, bounded accessibility nodes, viewport, and PNG snapshot |
| Actions | Navigate, click, type, scroll, and allowlisted key presses |
| Human view | The same Xvfb display through loopback-only x11vnc and noVNC |
| Ownership | One explicit UUID session with expiry, vision budget, and idempotent cleanup |
| Authority | Observer/controller separation when authenticated; strict local loopback mode otherwise |
| MCP | Nine stdio tools from either client (aether-browser mcp), no extra dependencies |
| Navigation | HTTP(S)-only validation across requested, redirected, and browser-initiated navigation |
Request and response shapes, limits, and stable error codes: docs/API.md.
Architecture
flowchart LR
Agent["Agent client"] -->|bounded JSON API| API["FastAPI"]
API --> Guard["authority + navigation policy"]
Guard --> Session["single-session manager"]
Session --> Chrome["Patchright + headed Google Chrome"]
Chrome --> State["text · accessibility · PNG"]
State --> Agent
Chrome --> Display["shared Xvfb display"]
Display -->|loopback noVNC| Human["Human observer / takeover"]
The session manager owns the page, browser context, temporary profile, timers, counters, and
cleanup. The API and the live view are different interfaces to that shared resource, not two
independent automation paths. See docs/ARCHITECTURE.md.
[!IMPORTANT] Agent Browser v0.2.3 is source-first and self-hosted. It is not a hosted service, and the v0.x noVNC surface is unauthenticated and meant for numeric loopback on a machine you control. No Chrome-containing image, image tar, or public layer cache is distributed unless separate redistribution authorization is documented.
Security boundary
- API and noVNC listen on numeric loopback by default; noVNC remains loopback-only in the v0.x line.
- Remote API clients require a separately operated same-host HTTPS reverse proxy, an exact trusted loopback peer, strict Host validation, and distinct strong observer/controller tokens.
- Destination validation rejects credentials, unsupported schemes, blocked address classes, unsafe redirects, and DNS rebinding. Browser egress is pinned through an owned TCP proxy.
- Non-proxied WebRTC UDP is disabled so it cannot silently bypass the TCP egress boundary.
- Inputs, outputs, interactions, timeouts, lifetimes, and screenshot budgets are bounded.
- Cleanup converges on session end, expiry, launch failure, application shutdown, and process failure.
Trust assumptions and residual risks are spelled out in docs/SECURITY.md.
Report vulnerabilities privately through SECURITY.md — please do not open a public
security issue.
Source recovery and exclusions
The source-recovery rule is reuse general browser behavior, not private domain code.
Lifecycle, structured-state, interaction, and cleanup patterns may be adapted from authorized
references; ATS/trading integrations, broker or account selectors, order actions, secrets,
and credential injection are excluded from the public core. Provenance status is tracked in
docs/SOURCE-RECOVERY.md.
What it does not do
- No hosted cloud service, cloud control plane, or production remote-hosting claim.
- No bundled LLM or default model calls, account system, dashboard, credential vault, or credential injection.
- No CAPTCHA bypass, anti-detection guarantee, stealth claim, or proxy rotation.
- No arbitrary JavaScript, shell, filesystem, upload, clipboard, download, or raw CDP API.
- No multi-session pool, ATS integration, trading integration, or brokerage behavior.
Roadmap
Shipped. Both clients and their CLI are published as aether-browser, version for version,
on npm from clients/node and
on PyPI from clients/python. Since
0.2.0 both also serve the MCP server.
Two tracks are open, each with an issue, and each is a good first contribution:
- Multi-session worker pool — explicit isolation and capacity semantics.
- Session trace and recording export — with clear privacy controls.
These are candidates, not shipped features.
Contributing
Start with CONTRIBUTING.md, the
Code of Conduct, and the current API contract. Small,
well-tested changes that keep the authority boundary narrow are very welcome — the two roadmap
issues above are the best place to start. Security reports go through the private process in
SECURITY.md, never a public issue.
License and third-party notices
Aether-owned source code is licensed under the Apache License 2.0. Google Chrome is
separately licensed under Google's Chrome terms and is not
covered by Aether's Apache license; dependencies, system packages, fonts, and web assets also
remain under their respective terms. See THIRD_PARTY_NOTICES.md.
Aether is not affiliated with or endorsed by Google. The v0.x distribution target is source that
builds locally; this repository does not distribute a prebuilt Chrome-containing image.
Agent Browser MCP questions
How do I add Agent Browser MCP to Claude Code?
Run claude mcp add --env AGENT_BROWSER_CONTROLLER_TOKEN=<YOUR_AGENT_BROWSER_CONTROLLER_TOKEN> --env AGENT_BROWSER_OBSERVER_TOKEN=<YOUR_AGENT_BROWSER_OBSERVER_TOKEN> agent-browser -- npx -y aether-browser in your terminal. Add --scope user to make it available in every project.
How do I add Agent Browser MCP to Cursor?
Open ~/.cursor/mcp.json and add the server under mcpServers, using the Cursor config shown in the install section above.
How do I add Agent Browser to an MCP client?
Run the one-line add command with npx, or add an mcpServers entry pointing at npx aether-browser mcp. It works with Claude Code, Claude Desktop, Cursor, and Windsurf.
What tools does the MCP server expose?
It provides nine browser tools covering open, navigate, read, click, type, press, scroll, status, and close. Each response repeats the live view URL so you know where to look.
Can a human take over the browser when the agent is stuck?
Yes, the live view shows the same Chrome session the agent uses, so a human can step in and then hand control back. The server instructs the model to ask for a takeover at logins, payments, or 2FA prompts.
How do I start the runtime?
Run docker compose up --build on Docker Engine for Linux with Compose v2. The first build takes several minutes while it installs Python and Chrome.
Where can I watch the agent's browser session?
Open the noVNC live view on local loopback port 6080 after the runtime is healthy. The same URL is returned when a session is created.