One browser profile. Separate tab ownership for every MCP session.
This package is a local browser broker built around
chrome-devtools-mcp.
Concurrent agents share one managed Chromium-family browser and its signed-in
profile, while a Rust daemon assigns each CDP tab to exactly one session.
It works as a standard stdio MCP server. The bundled OpenCode plugin adds a stronger integration that multiplexes many OpenCode conversations through the same MCP connection without exposing routing details to the model.
Tab control is isolated. Browser data is shared. See security model before using a profile that contains sensitive accounts.
| Setup | Result |
|---|---|
| One DevTools MCP server shared by every agent | Agents share current-page state and can collide with each other |
| One browser per agent | Clean isolation, but every agent needs another browser and another login |
| This project | One browser and profile, with daemon-enforced tab ownership per session |
The daemon does not infer ownership from whichever page happens to be selected. It creates tabs through CDP, records the exact stable target ID, and treats that record as the authority for routing and cleanup.
- Linux x64
- Bun for the default
chrome-devtools-mcpworker launcher - Node.js 20.19+, 22.12+, or 23+ when installing or running through npm
- A compatible local Chromium-family browser
- A graphical display server; headless mode is not supported
- An MCP host, or OpenCode 1.18+ for the bundled plugin
XDG_RUNTIME_DIRfor the default daemon socket, unless a custom socket is configured
The canonical npm package is @mirsella/opencode-browser-mcp. It includes the
OpenCode plugin, browser skill, JavaScript launcher, and Linux x64 native binary.
OpenCode users normally install it by adding the package to opencode.json as
shown below; OpenCode resolves the npm package on restart.
To run the npm CLI without installing it globally:
npx -y @mirsella/opencode-browser-mcp --helpOr install the CLI globally:
npm install --global @mirsella/opencode-browser-mcp
opencode-browser-mcp --helpThe Cargo package and binary are both named opencode-browser-mcp. The crate is
not currently published on crates.io; install the native MCP server from GitHub:
cargo install --git https://github.com/mirsella/opencode-browser-mcp.git --locked opencode-browser-mcpThe Cargo installation contains only the native generic MCP server. Use the npm package for the OpenCode plugin, compact tools, skill, and session lifecycle hooks.
To build a source checkout:
git clone https://github.com/mirsella/opencode-browser-mcp.git
cd opencode-browser-mcp
bun install
cargo build --release
./target/release/opencode-browser-mcp --helpFor local OpenCode plugin development, reference the checkout directly after building it:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"file:///absolute/path/to/opencode-browser-mcp/plugin/browser-routing.ts"
]
}Add the npm package to opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@mirsella/opencode-browser-mcp"]
}Restart OpenCode after changing the configuration. The plugin:
- registers the local
browserMCP server; - advertises one short browser skill and loads its instructions only when needed;
- injects the active OpenCode session ID after model argument generation;
- releases the session's owned tabs when the agent turn ends or the session is deleted.
The routing field is absent from every tool schema, so models do not need to know about it or spend output tokens supplying it.
OpenCode sees only two small browser tool definitions by default:
browser_dispatchexecutes any available browser action.browser_helplists action names or loads the current schema for one action.
The complete Chrome DevTools schemas are therefore absent from ordinary turns. When browser automation is needed, the agent loads the short skill and requests only the action definitions it needs. Generic MCP clients continue to receive the full direct tool surface.
Compact mode uses browser_dispatch as its OpenCode permission boundary. If
the plugin detects existing per-action browser_* permission or tool rules, it
automatically falls back to direct tools so those rules keep working. Set
compactTools: false explicitly when policies are generated by another plugin
or otherwise unavailable during configuration.
To expose every direct browser_* tool in OpenCode instead:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@mirsella/opencode-browser-mcp", { "compactTools": false }]
]
}To keep browser tabs between turns, disable completion cleanup and rely on the daemon's idle timeout instead:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@mirsella/opencode-browser-mcp", { "releaseOnIdle": false }]
]
}OpenCode 1.18 or newer is required.
Run the package as a normal stdio MCP server:
npx -y @mirsella/opencode-browser-mcpFor hosts that use JSON configuration, the equivalent shape is:
{
"mcpServers": {
"browser": {
"command": "npx",
"args": ["-y", "@mirsella/opencode-browser-mcp"]
}
}
}After a global npm or Cargo installation, configure the installed binary directly instead:
{
"mcpServers": {
"browser": {
"command": "opencode-browser-mcp"
}
}
}Each stdio connection gets its own browser session. Calls on that connection see only its owned tabs. When the connection closes, the daemon closes those tabs and leaves tabs owned by other connections alone.
Generic clients must not set __opencode_session_id. That field belongs to the
OpenCode adapter and is rejected on ordinary MCP connections.
flowchart LR
G[Generic MCP client] -->|one session per connection| GB[stdio bridge]
O[OpenCode plugin] -->|many conversation IDs| OB[stdio bridge]
GB --> S[private Unix socket]
OB --> S
S --> D[Rust browser daemon]
D --> T[CDP target tracker]
D --> W1[session worker A]
D --> W2[session worker B]
W1 --> C[chrome-devtools-mcp]
W2 --> C
T --> B[managed browser]
C --> B
B --> P[shared profile]
Each active browser session leases its own chrome-devtools-mcp worker.
Reusable workers are kept in a bounded, workspace-keyed warm pool to avoid
browser-level CDP reconnect churn and are health-checked before each lease.
Worker-local numeric page IDs never cross session boundaries. The daemon maps
them to stable browser-level CDP tab targets and removes page selection tools
from the public surface.
Ownership state is written atomically beside the daemon socket. If the daemon crashes while the browser survives, it restores ownership only when the exact browser WebSocket generation and live tab targets still match.
- A tab has at most one owner.
- Sessions run concurrently through separate downstream workers.
- Popups inherit the owner of their opener tab.
- A session cannot select, inspect, or close another session's tabs through this MCP server.
- Manual and otherwise unowned tabs are never claimed during session cleanup.
- Stale browser generations, malformed worker responses, ownership conflicts, and tracker disconnects fail closed.
- Reusing one routed session ID from another workspace is rejected.
pageId,select_page, and the OpenCode routing field are not exposed to models.
These are routing guarantees, not an operating-system security boundary. Any local process that connects directly to the CDP port bypasses the broker.
The managed browser must be a Chromium-family browser with a Chrome-compatible
browser-level CDP endpoint. In particular, this project relies on Chromium's
Target discovery, flat sessions, tab targets, opener lineage, and
Target.createTarget with forTab.
Google Chrome and Chrome for Testing are the browsers officially supported by
the pinned chrome-devtools-mcp@1.6.0. The launcher also detects Helium,
Chromium, Brave, and Microsoft Edge. Those browsers are best-effort because
their CDP behavior is not guaranteed by the upstream MCP project.
Use any compatible local executable explicitly:
OPENCODE_BROWSER_EXECUTABLE=/path/to/browser \
OPENCODE_BROWSER_PROFILE=/path/to/user-data-dir \
npx -y @mirsella/opencode-browser-mcpThe executable must accept Chromium command-line flags. The profile value is a
full user-data directory, not a named profile such as Default.
Chrome 136 and newer ignore remote-debugging flags for Chrome's default data
directory. For Google Chrome and Chrome for Testing, the default user-data
directory is $XDG_CONFIG_HOME/opencode-browser-mcp/google-chrome, or
~/.config/opencode-browser-mcp/google-chrome when XDG_CONFIG_HOME is unset.
Set OPENCODE_BROWSER_PROFILE to another non-default directory if you want a
previously signed-in profile. The current release manages a loopback CDP
endpoint; attaching to an arbitrary existing or remote endpoint is not
supported.
| Event | Behavior |
|---|---|
| Generic MCP connection closes | Its owned tabs and worker lease are released |
| OpenCode turn becomes idle | Its owned tabs are released by default |
| OpenCode session is deleted | Its owned tabs are released |
| Release arrives during an active call | Cleanup is queued and runs after active work completes |
| Session exceeds the idle timeout | The daemon retries cleanup as a fallback |
| Last MCP client disconnects | After two seconds, the daemon closes the managed browser and clears ownership |
| Daemon crashes but browser survives | Matching generation state is recovered conservatively |
releaseOnIdle: false keeps OpenCode tabs alive between turns. The default
daemon idle timeout is 60 minutes.
| Environment variable | Default | Purpose |
|---|---|---|
OPENCODE_BROWSER_EXECUTABLE |
auto-detected | Chromium-family browser executable |
OPENCODE_BROWSER_PROFILE |
browser-dependent; see Browser compatibility | Shared browser data directory |
OPENCODE_BROWSER_SOCKET |
$XDG_RUNTIME_DIR/opencode-browser-mcp/daemon.sock |
Private daemon socket |
OPENCODE_BROWSER_CDP_HOST |
127.0.0.1 |
CDP host; loopback only |
OPENCODE_BROWSER_CDP_PORT |
9222 |
CDP port |
OPENCODE_BROWSER_IDLE_MINUTES |
60 |
Fallback session cleanup; 0 disables expiry |
OPENCODE_BROWSER_STARTUP_SECONDS |
20 |
Browser startup deadline |
OPENCODE_BROWSER_OPERATION_SECONDS |
540 |
Browser operation deadline |
OPENCODE_BROWSER_DOWNSTREAM_COMMAND |
bunx |
chrome-devtools-mcp launcher |
OPENCODE_BROWSER_DISABLE_CRASH_REPORTER |
false |
Disable Chromium crash reporting |
RUST_LOG |
opencode_browser_mcp=info |
Native daemon log filter |
The first client starts the daemon and fixes these values for its lifetime. Later clients with a different daemon configuration are rejected.
The default socket directory is created under XDG_RUNTIME_DIR with mode
0700. A custom socket path does not create its parent; the parent must already
exist, be owned by the current user, and have no group or other permissions. The
browser profile must satisfy the same ownership and privacy requirements.
For a socket path <socket>, runtime files are:
<socket>
<socket>.lock
<socket>.state.json
<socket>.stderr.log
The browser profile also contains opencode-browser-mcp.lock. Environment
overrides must be exported to the OpenCode process so the plugin and MCP server
inherit them.
CLI controls:
opencode-browser-mcp --help
opencode-browser-mcp --version
opencode-browser-mcp --stop-daemonThe routing modes are connection for generic MCP clients, opencode for the
compact OpenCode interface, and opencode-direct for OpenCode with every direct
tool exposed. The plugin selects the appropriate OpenCode mode automatically.
- If
XDG_RUNTIME_DIRis unavailable, setOPENCODE_BROWSER_SOCKETto a socket inside an existing private directory. - Permission errors for the socket or profile mean the path is not owned by the
current user or permits group/other access. Restrict the directory to
0700. - A process already listening on the configured CDP port must belong to the
exact managed browser generation. Stop it or choose another
OPENCODE_BROWSER_CDP_PORT; arbitrary existing CDP endpoints are rejected. - A profile-lock error means another daemon is already using that profile.
- Browser startup and daemon errors are written to
<socket>.stderr.log. - If
chrome-devtools-mcpcannot start, verify thatbunxis available or setOPENCODE_BROWSER_DOWNSTREAM_COMMANDto a compatible launcher. - The plugin refuses to replace an unrelated
mcp.browserentry. Remove or rename that MCP configuration before enabling this package.
The broker protects tab ownership only for clients that use its MCP interface.
| Protected by the broker | Shared by design | Outside the boundary |
|---|---|---|
| Tab visibility, selection, and cleanup | Cookies, logins, storage, extensions, cache, permissions, and downloads | Direct access to the loopback CDP port |
| Worker-local page selection state | Browser process and profile | Other processes running as the same user |
| Generation-scoped ownership recovery | Network and website content | Prompt injection from pages |
| Private daemon socket and state files | Workspace access granted to the MCP host | OS, container, or VM isolation |
The downstream worker disables usage statistics and CrUX field-data requests. It also disables update checks and redacts headers that upstream classifies as sensitive. URLs, bodies, and other headers can still appear in tool output.
Read SECURITY.md before using a profile with sensitive data.
- The published native binary supports Linux x64 only.
- Browser windows are headed; there is no headless option yet.
- Browser profile data is shared between sessions.
- The CDP endpoint is unauthenticated loopback TCP.
- Browser content is untrusted input and can contain prompt injection.
- The target ownership model requires Chromium-specific CDP behavior.
- Browser tools rely on experimental interfaces from
chrome-devtools-mcp@1.6.0. isolatedContextis intentionally disabled because it conflicts with shared-profile ownership.- A fresh existing CDP endpoint and remote CDP endpoints are not supported.
cargo fmt --check
cargo test --all-targets
cargo clippy --all-targets -- -D warnings
bun test tests/plugin.test.ts
cargo test --release --test e2e -- --ignored --nocaptureThe headed E2E test uses a temporary profile and CDP port. Building the npm artifact requires Rust 1.89 or newer and the musl target:
rustup target add x86_64-unknown-linux-musl
npm pack| Path | Responsibility |
|---|---|
plugin/ |
OpenCode routing and lifecycle adapter |
bin/ |
npm launcher for the native binary |
src/daemon.rs |
stdio bridges, Unix socket daemon, connection routing, and controls |
src/cdp.rs |
stable CDP target tracking and ownership |
src/main.rs |
browser supervision, session workers, and response filtering |
src/persistence.rs |
atomic generation-scoped ownership state |
tests/e2e.rs |
real-browser isolation and cleanup coverage |
See CONTRIBUTING.md for the development and release
workflow.