Skip to main content
Glama
hy-2005
by hy-2005
README.md
# πŸ–₯️ Browser Pool MCP

> Dynamic browser pool MCP server β€” every agent gets its own isolated browser.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green)](https://nodejs.org/)
[![MCP](https://img.shields.io/badge/MCP-Server-blue)](https://modelcontextprotocol.io/)

**English** | [δΈ­ζ–‡ζ–‡ζ‘£](./README_zh.md)

A [Model Context Protocol](https://modelcontextprotocol.io/) server that wraps
[Microsoft's `@playwright/mcp`](https://github.com/microsoft/playwright-mcp) and
turns it into a **dynamic browser pool**: each unique caller gets its own
isolated Chrome instance, so multiple Claude Code sessions, subagents, or any
MCP clients can run browser automation **concurrently without conflicts**.

## ✨ Features

- 🧩 **Per-agent isolation** β€” each unique `_agent_id` gets its own browser. No more
  `"Browser is already in use"` errors when multiple agents hit the browser at once.
- πŸš€ **On-demand spawning** β€” browsers are lazily started on first use (local CLI, ~1s),
  never pre-allocated.
- 🧹 **Auto-cleanup** β€” idle instances are killed after 30 min; orphaned instances from
  crashed wrappers are cleaned on startup.
- πŸ›‘οΈ **Never kills busy agents** β€” when the pool is full, the wrapper evicts only *idle*
  instances; if all are busy it waits, so a running agent's browser is never yanked away.
- πŸ” **Backward compatible** β€” calling tools without `_agent_id` shares one "default"
  browser, preserving original single-session behavior.
- 🌐 **Full Playwright toolset** β€” all `@playwright/mcp` tools (navigate, click, type,
  screenshot, evaluate, tabs, network…), no feature loss.

## 🧠 Architecture

```
Claude Code main session ──stdio──> playwright-pool-mcp (this server)
                                        β”‚
                                        β”œβ”€β”€ agent "A" ──SSE──> @playwright/mcp :9000 ──> Chrome A
                                        β”œβ”€β”€ agent "B" ──SSE──> @playwright/mcp :9001 ──> Chrome B
                                        β”œβ”€β”€ agent "C" ──SSE──> @playwright/mcp :9002 ──> Chrome C
                                        └── "default" ──SSE──> @playwright/mcp :9003 ──> Chrome D
```

The wrapper does **not** control Chrome directly. It spawns `@playwright/mcp`
instances on demand and proxies MCP tool calls to them, adding per-agent routing.

## πŸš€ Quick Start

### Prerequisites

- Node.js β‰₯ 18
- npm

### Install

```bash
git clone <your-repo-url> playwright-pool-mcp
cd playwright-pool-mcp
npm install
# First-time Chromium download (if needed):
npx playwright install chromium
```

### Configure Claude Code

**User-level global config** (`~/.claude.json`):

```json
{
  "mcpServers": {
    "playwright-pool": {
      "type": "stdio",
      "command": "node",
      "args": ["/path/to/playwright-pool-mcp/playwright-pool-script.js"],
      "env": {
        "BROWSER_POOL_BASE_PORT": "9000",
        "BROWSER_POOL_MAX_INSTANCES": "10",
        "BROWSER_POOL_GLOBAL_MAX": "12",
        "BROWSER_POOL_INSTANCE_TIMEOUT_MS": "1800000",
        "BROWSER_POOL_DEBUG": "0"
      }
    }
  }
}
```

Restart Claude Code. Tools appear as `mcp__playwright-pool__*`.

## πŸ’‘ Usage

### Mode 1: Shared (backward compatible)

Omit `_agent_id` β€” all calls share one "default" browser:

```json
mcp__playwright-pool__browser_navigate(url="https://example.com")
mcp__playwright-pool__browser_snapshot()
```

### Mode 2: Isolated per agent (recommended)

Pass `_agent_id` β€” each unique value gets its **own** browser:

```json
subagent A: mcp__playwright-pool__browser_navigate(url="https://news.ycombinator.com", _agent_id="agent-a")
subagent B: mcp__playwright-pool__browser_navigate(url="https://github.com/trending",   _agent_id="agent-b")
subagent C: mcp__playwright-pool__browser_navigate(url="https://www.baidu.com",         _agent_id="agent-c")
```

> ⚠️ **Important**: pass the **same** `_agent_id` across all calls from one agent to
> keep its browser state (cookies, login, navigation). Different agents use different values.

### Subagent template

Create `.claude/agents/web-worker.md`:

```markdown
---
name: web-worker
description: Run browser tasks (scraping / research / testing / automation)
tools: mcp__playwright-pool__*, Read, Bash
---

Use mcp__playwright-pool__* tools to operate the browser.

**Key rule: pass the `_agent_id` parameter on every browser tool call.**
The main conversation may dispatch multiple web-workers concurrently; different
workers must use different `_agent_id` values so each gets its own isolated browser.

Examples:
- mcp__playwright-pool__browser_navigate(url="...", _agent_id="worker-1")
- mcp__playwright-pool__browser_evaluate(function="...", _agent_id="worker-1")

Use the same `_agent_id` for all calls within one worker.
```

## πŸ› οΈ Tools

All tools accept an optional `_agent_id` parameter. Aligned 1:1 with `@playwright/mcp` v0.0.78.

> ℹ️ **Element targeting**: interaction tools use `target` (snapshot ref or CSS selector).
> The `ref` parameter is accepted as an alias for `target` (Claude Code's client maps it).

| Tool | Description |
|------|-------------|
| `browser_navigate` | Navigate to a URL |
| `browser_navigate_back` | Go back |
| `browser_snapshot` | Accessibility tree (LLM-friendly) |
| `browser_take_screenshot` | Screenshot (PNG/JPEG) |
| `browser_click` | Click element |
| `browser_type` | Type text into element |
| `browser_press_key` | Press keyboard key |
| `browser_hover` | Hover element |
| `browser_select_option` | Select dropdown option |
| `browser_drag` / `browser_drop` | Drag & drop |
| `browser_evaluate` | Run JavaScript |
| `browser_wait_for` | Wait for condition |
| `browser_find` | Find text in page snapshot |
| `browser_fill_form` | Fill multiple form fields (JSON array of `{name, target, type, value}`) |
| `browser_tabs` | Manage tabs |
| `browser_resize` | Resize window |
| `browser_handle_dialog` | Handle alert/confirm |
| `browser_file_upload` | Upload files |
| `browser_console_messages` | Get console logs |
| `browser_network_requests` / `browser_network_request` | Network activity |
| `browser_mouse_move_xy` / `browser_mouse_click_xy` / `browser_mouse_drag_xy` / `browser_mouse_down` / `browser_mouse_up` / `browser_mouse_wheel` | Mouse control (screen coordinates) |
| `browser_run_code_unsafe` | Run arbitrary code (dangerous) |
| `browser_close` | Close browser for this agent |
| `pool_status` | Pool status (per agent) |

## βš™οΈ Configuration

All config via environment variables in `~/.claude.json` β€” **no code changes needed**:

| Variable | Default | Description |
|----------|---------|-------------|
| `BROWSER_POOL_BASE_PORT` | `9000` | Starting port for browser instances |
| `BROWSER_POOL_MAX_INSTANCES` | `10` | Max concurrent browsers **per wrapper** (β‰ˆ300MB RAM each) |
| `BROWSER_POOL_GLOBAL_MAX` | `0` (disabled) | **Global** max browsers across ALL wrapper sessions (multiple Claude Code windows). File-lock based counter in `~/.browser-pool/`. Set e.g. `12` to cap total instances across every session. |
| `BROWSER_POOL_INSTANCE_TIMEOUT_MS` | `1800000` | Idle timeout (30 min) |
| `BROWSER_POOL_DEBUG` | `0` | Set `1` for debug logging to `debug.log` |

Changes take effect after restarting Claude Code.

> πŸ’‘ **Global limit** (`BROWSER_POOL_GLOBAL_MAX`): each Claude Code window runs its own
> wrapper, and `BROWSER_POOL_MAX_INSTANCES` limits only that wrapper. To cap browser
> instances across ALL sessions (e.g. two windows must share a total of 12), set
> `BROWSER_POOL_GLOBAL_MAX` to the same value in every session's config. Slots are
> counted via a lock-protected counter file (`~/.browser-pool/global-count`) and are
> released when instances close or the wrapper exits gracefully.

## πŸ” How It Works

1. Any browser tool call arrives β†’ wrapper extracts `_agent_id` (defaults to `"default"`)
2. Looks up the per-agent assignment map β†’ reuse existing browser if alive
3. If none: check the global limit (`BROWSER_POOL_GLOBAL_MAX`) β†’ find a free port β†’ spawn local `@playwright/mcp` CLI (`node cli.js --port X --isolated`)
4. Mark the instance `busy` during the call, so eviction never kills an in-flight call
5. Pool full? Evict only **idle** instances (oldest first); if all busy, wait up to 60s
6. Idle > 30 min β†’ auto-killed; wrapper startup scans for orphaned instances and cleans them (PPID-guarded, never kills another live session's browsers)
7. Global slot accounting: acquired before spawn, released when the instance closes or the wrapper exits gracefully

## πŸ“¦ Dependencies

- [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk) β€” MCP protocol
- [`@playwright/mcp`](https://github.com/microsoft/playwright-mcp) β€” browser automation engine
- [`zod`](https://zod.dev/) β€” schema validation

## πŸ“„ License

[MIT](LICENSE)

TDQS

C2.5/5.0

Scored across 25 tools

Disambiguation3/5

Most tools have distinct purposes, but browser_take_screenshot and browser_screenshot are explicit aliases, causing ambiguity. Additionally, browser_drag and browser_drop could be confused as both relate to drag-and-drop operations.

Naming Consistency3/5

The majority of tools follow a browser_verb pattern (e.g., browser_navigate, browser_click), but there are exceptions like browser_snapshot, browser_console_messages, and browser_network_requests which use nouns. The pair browser_take_screenshot and browser_screenshot also breaks consistency.

Tool Count3/5

At 25 tools, the server is on the heavy side but still plausible for a comprehensive browser automation tool. However, the presence of a duplicate screenshot tool and a test tool (pool_test) suggests some redundancy.

Completeness4/5

The tool set covers a wide range of browser actions: navigation, interaction, screenshots, snapshots, tab management, dialog handling, file upload, and more. Minor gaps exist (e.g., no explicit cookie management or scrolling) but core workflows are well covered.

Maintenance

ActivitySlowing
ResponsivenessNo issues