Skip to content

Repository files navigation

opencode-browser-mcp

npm CI License: MIT

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.

Why this exists

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.

Requirements

  • Linux x64
  • Bun for the default chrome-devtools-mcp worker 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_DIR for the default daemon socket, unless a custom socket is configured

Installation

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 --help

Or install the CLI globally:

npm install --global @mirsella/opencode-browser-mcp
opencode-browser-mcp --help

The 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-mcp

The 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 --help

For 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"
  ]
}

OpenCode setup

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 browser MCP 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.

Compact tools

OpenCode sees only two small browser tool definitions by default:

  • browser_dispatch executes any available browser action.
  • browser_help lists 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.

Generic MCP setup

Run the package as a normal stdio MCP server:

npx -y @mirsella/opencode-browser-mcp

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

Architecture

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]
Loading

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.

Ownership guarantees

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

Browser compatibility

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-mcp

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

Lifecycle

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.

Configuration

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-daemon

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

Troubleshooting

  • If XDG_RUNTIME_DIR is unavailable, set OPENCODE_BROWSER_SOCKET to 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-mcp cannot start, verify that bunx is available or set OPENCODE_BROWSER_DOWNSTREAM_COMMAND to a compatible launcher.
  • The plugin refuses to replace an unrelated mcp.browser entry. Remove or rename that MCP configuration before enabling this package.

Security model

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.

Limitations

  • 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.
  • isolatedContext is intentionally disabled because it conflicts with shared-profile ownership.
  • A fresh existing CDP endpoint and remote CDP endpoints are not supported.

Development

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 --nocapture

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

License

MIT

About

Session-isolated browser automation for OpenCode: one shared Chromium profile with separate CDP tab ownership per conversation.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages