Skip to main content
Glama
README.md
English | [简体中文](./README.zh-CN.md)

# browser-relay — Multi-Session Browser-Isolated MCP

**One MCP endpoint, N isolated browsers.**

An MCP multiplexer: to your IDE / MCP host (ZCode, Claude Desktop, Codex CLI, dsh, …) it looks like a single, ordinary Playwright MCP server, but internally it routes every call tagged with a `session_id` to a dedicated [`@playwright/mcp`](https://github.com/microsoft/playwright-mcp) child process for that id — so every session / subagent gets a fully isolated Chrome of its own (separate profile, separate tabs, cookies invisible to each other).

```
IDE (registers one MCP entry: browser-relay, stdio)
  └─ relay.mjs
       ├─ session_id "task-1" → child playwright-mcp --isolated → Chrome #1
       ├─ session_id "task-2" → child playwright-mcp --isolated → Chrome #2
       └─ ... (concurrency cap, idle reaping, crash auto-restart, full teardown on exit)
```

Within one relay process, different ids are isolated from each other. Different host sessions each spawn their own relay process, so even colliding ids are naturally isolated (process-level backstop).

## Why

When multiple subagents / tasks run in parallel and share one playwright MCP, they share one browser: tabs stomp on each other, login state leaks between agents, one agent closes the page another agent is reading. browser-relay gives **every session a browser of its own**, while the host only registers a single MCP entry.

## Prerequisites

1. **Node.js ≥ 18**.

2. **Install the original (upstream) playwright MCP server** — all real browser automation is done by it:

   ```bash
   npm install -g @playwright/mcp
   ```

3. **Ready a browser**. `@playwright/mcp` drives the locally installed Google Chrome by default:

   ```bash
   npx playwright install chrome
   ```

   No Chrome, or prefer the bundled Chromium? Run `npx playwright install chromium`, then pass `--child-arg --browser --child-arg chromium` to the relay (forwarded to every child process).

4. A global install is not mandatory: when the relay cannot find a global `@playwright/mcp`, it falls back to `npx -y @playwright/mcp@0.0.82` (downloaded on first run; version is pinned — a global install is recommended to follow upstream updates). You can also point the `RELAY_MCP_PATH` environment variable or the `--mcp-path` option directly at the upstream `cli.js`.

## Install & Run

```bash
git clone https://github.com/CarrotHu-secret-workstation/mcp-browser-relay.git
cd mcp-browser-relay
npm install

node relay.mjs --help          # show options
node relay.mjs                 # start (stdio, foreground)
```

Or skip the clone and run straight from GitHub with npx:

```bash
npx github:CarrotHu-secret-workstation/mcp-browser-relay --help
```

Running `node relay.mjs` on its own just waits on stdio — that is normal; that is how an MCP server gets launched by its host. To see it in action, register it with your MCP host as described below.

## Hooking It into an MCP Host

Ground rules:

- Register **only this one entry** per session; if you previously had a direct `@playwright/mcp` entry, disable it so you don't end up with two sets of near-identical tools.
- MCP connections are established when a session starts — **newly opened sessions only**.
- `command` is always `node` (or the absolute path to node on your machine); `args` points at the repo's `relay.mjs`.

**ZCode** (`~/.zcode/cli/config.json`; field names may vary by version):

```json
{
  "mcpServers": {
    "browser-relay": {
      "command": "node",
      "args": ["/path/to/mcp-browser-relay/relay.mjs"]
    },
    "playwright": { "enabled": false }
  }
}
```

**Claude Desktop / other JSON-configured hosts**:

```json
{
  "mcpServers": {
    "browser-relay": {
      "command": "node",
      "args": ["/path/to/mcp-browser-relay/relay.mjs"]
    }
  }
}
```

**Codex CLI** (`~/.codex/config.toml`):

```toml
[mcp_servers.browser-relay]
command = "node"
args = ["/path/to/mcp-browser-relay/relay.mjs"]
startup_timeout_sec = 60
```

**dsh** (profile overlay patch, e.g. `~/.dsh/profiles/<profile>/cordis.patch.yml`):

```yaml
- insert:
    - id: mcp-browser-relay
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        serverName: browser-relay
        transport: stdio
        command: node
        args:
          - /path/to/mcp-browser-relay/relay.mjs
```

Verify with whatever your host offers (`codex mcp list`, `dsh --profile web --dump-config`, …). Once connected you will see the full upstream tool set (25 `browser_*` tools as of 0.0.82) plus the relay's own `relay_status`.

**Rolling back / disabling**: delete or `enabled: false` the entry above and restore your direct playwright entry.

## Usage

Tool names are identical to `@playwright/mcp`; every tool takes one extra **required** parameter `session_id`:

```
browser_navigate { session_id: "task-1", url: "https://example.com" }
```

- `session_id` rules: `[A-Za-z0-9_-]{1,64}`. Missing or invalid → immediate error asking you to fix it.
- Different id = a completely different Chrome. **Multiple agents sharing one id = sharing one browser** (shared login state) — that is a feature.
- `relay_status { }` lists active sessions (id / pid / idle time / concurrency cap); pass `session_id` to inspect a single one.

### Assigning ids from the orchestrator (key discipline)

The `session_id` is **assigned by the main session**: when spawning a subagent, put the id in its prompt. Template:

> For browser work, use only the `mcp__browser-relay__*` tools and pass
> `session_id: "task-3"` on EVERY call (that browser is yours alone).
> Do not use any other session_id.

- Naming suggestion: `<task>-<n>` (e.g. `audit-1`, `crawl-2`); the orchestrator guarantees uniqueness.
- To keep login state across relay restarts, start the relay with `--profile-root <dir>`: each id's profile persists at `<dir>/<session_id>` (the default `--isolated` is an in-memory profile, wiped when the browser is reaped).

## relay.mjs Options

| Option | Default | Description |
|---|---|---|
| `--max-sessions <n>` | auto | Cap on concurrent child browsers. Default: computed at **every relay start** as `available RAM ÷ 400 MiB` (byte-based, floored, min 1); an explicit value overrides the automatic one |
| `--idle-kill-ms <ms>` | 1800000 | Reap an id's browser after this much idle time (0 = never) |
| `--headless` | off | Headless child browsers (default headed, so you can eyeball the multiple Chromes) |
| `--profile-root <dir>` | none | Enable a persistent per-id profile (default `--isolated` in-memory profile) |
| `--mcp-path <file>` | auto | Path to the upstream `@playwright/mcp/cli.js` (default: global npm prefix first, then npx fallback) |
| `--child-arg <arg>` | none | Extra argument forwarded to every child playwright-mcp (repeatable) |

The `RELAY_MCP_PATH` environment variable is equivalent to `--mcp-path`.

## Known Limitations

- **Identity is "discipline + schema validation" grade**: any caller can claim someone else's id. The orchestrator's id assignment discipline is the single source of isolation; colliding ids share a browser (exploitable on purpose).
- Upstream capabilities follow `@playwright/mcp` upgrades automatically (the tool list is aggregated from the child at startup).
- The concurrency cap takes effect **per relay process**: each host session spawns its own relay, which sizes itself from remaining memory at its own start (earlier sessions get a bigger quota, later ones shrink — natural first-come-first-served). The value stays fixed for the lifetime of the process. 400 MiB is a coarse per-browser budget (including all its child processes and pages); tune it to your machine.
- Developed and verified on **Windows**; the macOS/Linux code paths (POSIX signals, process-tree kill, npx fallback) are in place but untested — issues and PRs welcome.

## Acknowledgements

Every bit of browser capability in this project stands on the shoulders of the following projects — our respect and thanks:

- **[@playwright/mcp](https://github.com/microsoft/playwright-mcp)** (Apache-2.0, Microsoft) — the real upstream Playwright MCP server. The relay is just a session pool + forwarder: launching every Chrome, the whole `browser_*` tool set, snapshots / screenshots / clicks / typing — all done by it.
- **[Playwright](https://github.com/microsoft/playwright)** (Apache-2.0, Microsoft) — the foundation under all of the above: the cross-browser automation engine.
- **[Model Context Protocol](https://modelcontextprotocol.io) and the official TypeScript SDK [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk)** (MIT, © Anthropic, PBC and community contributors) — the MCP protocol and SDK that make "one host, one stdio, bidirectional forwarding" possible.

Inspired by a real pain point: the browser turf war between parallel agents. This repo is a thin layer of glue — upstream upgrades (new tools, new capabilities) are picked up with zero code changes. Without the projects above, this repository would not exist.

## License

This project's code is released under the [MIT](./LICENSE) license.

Third-party licenses and copyright notices: see [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) —
`@playwright/mcp` and `playwright` are Apache-2.0; `@modelcontextprotocol/sdk` is MIT.
This repository does not copy or modify their source code; they are used at runtime as npm dependencies. The notices file exists for compliance and as a tribute.