npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

browserctl-mcp

v0.5.1

Published

Universal Browser Control Protocol & MCP Server for AI Agents and Developers

Readme

browserctl

A Chrome/Edge extension + local bridge server + MCP server that lets any AI agent (Claude, Antigravity, Cursor, or scripts) drive a real Chromium browser through a neutral HTTP/WebSocket API.

[!WARNING] Safety & Isolation Disclaimer: browserctl grants AI agents full programmatic control over browser DOM, forms, cookies, and network requests.

  • DO NOT USE ON YOUR PRIMARY PERSONAL BROWSER PROFILE (e.g. personal profiles containing saved banking credentials, password managers, or private data).
  • STRONGLY RECOMMENDED: Run browserctl in a dedicated, isolated browser profile, an open-source Chromium test build (e.g. Ungoogled Chromium / raw Chromium), or an isolated sandbox/testing browser instance.

How it works

Agent (Claude / GPT / your script)
      │  HTTP REST  +  WebSocket
      ▼
Bridge server (Node, runs on localhost)   <- relay, no build step
      │  WebSocket
      ▼
Chrome extension (Manifest V3)
   ├─ background.js  (service worker: WS client, command dispatch, target-tab pinning)
   ├─ content.js     (DOM reads + accessibility tree/refs, clicks/types/scrolls)
   └─ cdp.js         (opt-in chrome.debugger: coordinate input, background screenshots, console/network/HAR)

The agent never talks to Chrome directly. It POSTs a command to the bridge; the bridge relays it to the extension over WebSocket; the extension runs it and the result travels back the same path.

Control is DOM-first: the extension reads the page — interactive elements with an index (snapshot), or the accessibility tree with stable refs (read_page) — and the agent acts by ref/index ("click ref_5", "type into 8"). No chrome.debugger, so no "is being debugged" banner. It drops to CDP (chrome.debugger) only where the DOM can't reach: pixel-coordinate clicks on canvas/WebGL/maps, screenshotting a background tab, console/network/HAR capture, and CSP-bypass JS eval.

The agent pins one target tab on first use and keeps acting on it — including DOM interaction and screenshots while that tab sits in the background — so you can keep using your other tabs without the agent following you or stealing focus. group_tab puts the controlled tab in a labelled tab group so you can see which one it is. This mirrors the official "Claude in Chrome" control model, kept open (no blocklist / org-lock / per-action gating) with the agent driven externally over MCP/HTTP.

One exception to background operation: Chrome delivers CDP synthetic input only to a foreground tab, so coordinate_click, coordinate_drag, and press_key with modifiers cannot work on a hidden tab — they now fail with an actionable error instead of silently doing nothing. Everything else, including every DOM action and every screenshot, genuinely works in the background. See docs/REFERENCE.md for the full matrix.

Quickstart & Installation

1. Install & Configure MCP (Zero-Setup via NPX)

You can run browserctl directly without cloning the repository.

For Claude Desktop / Antigravity / Cursor / Windsurf (.mcp.json)

Add browserctl to your MCP configuration:

{
  "mcpServers": {
    "browserctl": {
      "command": "npx",
      "args": ["-y", "browserctl-mcp"],
      "env": {
        "BROWSERCTL_BRIDGE_URL": "http://127.0.0.1:8765",
        "BROWSERCTL_MCP_PROFILE": "core" // 'core' (~24 tools) or 'all' (all 70+ tools)
      }
    }
  }
}

For Claude Code CLI

claude mcp add browserctl -- npx -y browserctl-mcp

Global CLI Installation

To use the browserctl command from anywhere in your terminal:

npm install -g browserctl-mcp

2. Load the Chrome Extension

  1. Open chrome://extensions (or edge://extensions on Microsoft Edge)
  2. Enable Developer mode (top right toggle)
  3. Click Load unpacked -> select the extension/ folder
  4. Click the extension icon in Chrome toolbar and press Connect in the popup.

Auto-Reconnect & Keepalive: Once connected, the extension remembers the link and automatically reconnects on browser startup. Only an explicit Disconnect in the popup stops reconnection.


3. Bridge Daemon & Zero-Terminal Execution

No manual terminal needed: When an MCP client launches or when you run any browserctl CLI command, the local bridge daemon is started automatically in the background.

You can also manage the daemon explicitly:

browserctl status                         # Check bridge health, daemon state & extension
browserctl start                          # Start bridge daemon in background
browserctl stop                           # Stop bridge daemon (records explicit stopped state)
browserctl restart                        # Restart bridge daemon

State Machine (Docker/Tailscale Model):

  • If you explicitly ran browserctl stop, subsequent commands will NOT auto-start the daemon unexpectedly; they prompt you to run browserctl start (or pass --auto-daemon).
  • To disable auto-start globally, set BROWSERCTL_AUTO_START=manual.

CLI Reference (browserctl)

# Navigation & History
browserctl open https://github.com        # Navigate target tab (alias: navigate)
browserctl back | forward | reload        # History navigation

# Inspection & Fast Property Queries (get)
browserctl snapshot --compact             # Token-efficient DOM snapshot (saves 75% tokens)
browserctl read_page                      # Read accessibility tree with refs
browserctl get title                      # Get page title
browserctl get url                        # Get page URL
browserctl get text @e1                   # Get visible text of element
browserctl get value @e1                  # Get input/textarea value
browserctl get attr @e1 href              # Get element attribute
browserctl get box @e1                    # Get bounding box (x, y, width, height)
browserctl get count "button"             # Count matching elements

# Interaction & Form Utilities
browserctl click @e1                      # Click by ref (@e1, ref_1, 0)
browserctl click --text "Sign In"         # Click by visible text
browserctl fill @e1 "my query"            # Clear input and fill text (React/Vue v-model compatible)
browserctl paste @e1 "markdown content"   # Paste multi-line text into inputs or rich-text editors (ProseMirror/Tiptap)
browserctl type @e2 "appended text"       # Type into input field
browserctl clear @e1                      # Clear input field
browserctl check @e3                      # Check checkbox / radio button
browserctl uncheck @e3                    # Uncheck checkbox
browserctl select @e4 "value"             # Select dropdown option
browserctl hover @e1                      # Hover element
browserctl focus @e1                      # Focus element
browserctl scroll down 400                # Scroll page

> **Form & Rich-Text Compatibility**: `fill`, `type`, and `paste` inject values instantly via native prototype setters (fully compatible with React/Vue `v-model`) and seamlessly handle rich-text `contenteditable` editors (ProseMirror, Tiptap, Quill, Lexical). When multiple forms coexist on a page, always target elements by their stable `@ref` from `snapshot` to avoid selector ambiguity.

# Synchronization & Timing
browserctl wait 2000                      # Sleep for 2000 ms
browserctl wait @e1                       # Wait for element to appear
browserctl wait --text "Welcome"          # Wait for text to appear
browserctl wait --network-idle            # Wait for network idle
browserctl wait --settle                  # Wait for DOM mutations to settle

# Capture, Export & JavaScript
browserctl screenshot page.png [-f]       # Capture viewport or fullpage screenshot to file
browserctl pdf document.pdf               # Print page to PDF file directly
browserctl eval -r "document.title"       # Run JS and output raw value to stdout
browserctl tab [list|new|switch|close]    # Manage browser tabs

Output Formatting

By default, CLI output uses a smart format optimized for both humans and AI agents: scalar queries return direct values, tab lists render as ASCII tables, and snapshots use the compact DOM tree view.

Override with explicit flags when needed:

| Flag | Description | Use Case | |---|---|---| | (none) | Smart default (token-efficient, zero info loss) | AI agent interaction, general use | | -r / --raw | Raw unformatted value, no trailing newline | Shell piping: URL=$(browserctl get url -r) | | --json | Compact single-line JSON | Automated script parsing | | --pretty | 2-space indented JSON | Human inspection, debugging |

browserctl get title               # -> Example Domain
browserctl get title -r            # -> Example Domain  (no newline, perfect for piping)
browserctl get title --json        # -> {"property":"title","value":"Example Domain"}
browserctl get title --pretty      # -> { "property": "title", "value": "Example Domain" }
browserctl tabs                    # -> clean ASCII table
browserctl tabs --json             # -> {"tabs":[...]}

Using it via Model Context Protocol (MCP)

The mcp/ server exposes browser automation tools for MCP clients (Antigravity, Claude Code, Cursor, Windsurf).

Dynamic Tool Discovery & Profile Management

AI agents can dynamically load and unload specialized tool categories into the active session without restarting the server:

  • browser_load_tools: Load a category ("network", "cdp", "cookies", "storage", "console", "record", "tabs", "advanced", "all") or specific tools directly into the prompt.
  • browser_unload_tools: Unload extra tools and reset back to the lightweight "core" profile to free system prompt tokens.
  • browser_list_available_tools: Check which tool categories are currently active vs available for loading.

MCP Core Tools

| Tool | Description | |---|---| | browser_click | Click element by ref/index/selector/text (supports waitFor selector) | | browser_fill | Fill input or rich-text editor (ProseMirror/Tiptap/Vue/React) | | browser_paste | Paste large text/Markdown via Clipboard events without AST corruption | | browser_type | Focus element and set text (React/Vue v-model compatible) | | browser_snapshot | Fast token-efficient DOM snapshot with stable refs | | browser_read_page | Accessibility tree inspection | | browser_screenshot | Viewport or full-page screenshot (lossless PNG or vision-optimized JPEG) | | browser_eval_js | Evaluate JavaScript in page context | | browser_load_tools | Dynamically load tool categories (cdp, network, cookies, etc.) into prompt | | browser_unload_tools | Unload extra tools and reset active prompt back to core profile | | browser_list_available_tools | List all tool categories and active status | | browser_start | Start bridge daemon in background if stopped | | browser_stop | Stop bridge daemon (records explicit stopped state) | | browser_status | Check bridge health, daemon state, extension connection |

Output Format Parameter

Tools that return structured data (browser_snapshot, browser_eval_js, browser_status, browser_list_available_tools) accept an optional format parameter ("smart" default, "json", "pretty", "raw").

{
  "mcpServers": {
    "browserctl": {
      "command": "npx",
      "args": ["-y", "browserctl-mcp"],
      "env": {
        "BROWSERCTL_BRIDGE_URL": "http://127.0.0.1:8765",
        "BROWSERCTL_MCP_PROFILE": "core"
      }
    }
  }
}

The bridge daemon auto-starts when the MCP server boots. The extension must be installed and connected in Chrome. Typical agent use: "snapshot the page, then click the login button" -> Agent calls browser_snapshot, reads the indexed elements, then browser_click.

Using it from any other agent (raw HTTP)

Send commands as JSON over HTTP. See PROTOCOL.md for the full list.

# Take a snapshot of the current page (interactive elements + text)
curl -s -X POST http://localhost:8765/command \
  -H 'content-type: application/json' \
  -d '{"action":"snapshot"}'

# Navigate
curl -s -X POST http://localhost:8765/command \
  -H 'content-type: application/json' \
  -d '{"action":"navigate","params":{"url":"https://example.com"}}'

# Click the element that snapshot labeled index 3
curl -s -X POST http://localhost:8765/command \
  -H 'content-type: application/json' \
  -d '{"action":"click","params":{"index":3}}'

A typical agent loop: snapshot -> reason about the returned element list -> issue click / type / scroll / navigate -> snapshot again.

Status

Working, v0.5, 65 MCP tools over 64 bridge actions. Control parity with the official "Claude in Chrome" surface (open): DOM-index + accessibility-tree (read_page) reads with stable refs, ref/coordinate interaction, background-tab control, screenshots (incl. background tabs), console/network/HAR capture, record/replay, and tab grouping. Reads and interaction pierce open shadow DOM and cover iframes (including cross-origin) via all_frames injection with frame-qualified refs.

Tests: 18/18 unit, 62/62 e2e, 58 of 60 commands exercised.

Docs:

  • docs/REFERENCE.md — the operator's guide: install, control model, every tool grouped with its params, recipes, failure modes, the foreground-input matrix. Start here.
  • PROTOCOL.md — wire-level command spec and per-version changelog.
  • docs/prior-art.md — how this compares to similar projects, and the positioning decision (general-purpose browser control, explicitly not test automation).
  • docs/backlog-capability-gaps.md — the five tracked gaps, with verified CDP surfaces.
  • docs/debugger-policy.md — which commands need chrome.debugger (45 of 65 never do), what a per-site denial would cost, and the single chokepoint to enforce it at.

Testing

End-to-end tests drive the live stack (bridge -> extension -> Chrome) by POSTing real commands against a controlled page the runner serves over http:

# bridge must be running and the extension connected
node tests/e2e/run.mjs                    # 62 checks; never steals focus
E2E_FOREGROUND=1 node tests/e2e/run.mjs   # + the 2 synthetic-input tests (steals focus)

# bridge relay only, no Chrome needed (~0.5s, safe alongside a live bridge)
node --test tests/unit/bridge.test.mjs

It creates a dedicated tab, exercises nearly all commands (all but focus_window and reload_extension, which steal focus / drop the connection), asserts behaviour including the framework-safe value setter, ref-addressed element screenshots, shadow-DOM reads, and history navigation, then closes the tab and prints a pass/fail + coverage report. After editing extension code, reload it (chrome://extensions -> reload, or the reload_extension command) before re-running so the test hits the new code.

Security

This is a single-user, internal tool. It runs on my own machine, driven by my own agent, and is not meant to be shared, exposed, or run on a multi-user host. The security model is deliberately "trusted local machine": there is no auth and no access control, by design. The hardening items below are known and intentionally not implemented — none of them affect the MCP/HTTP functionality, so for a single-user setup they buy nothing. If this project is ever shared or moved off a trusted machine, revisit them first.

Known, accepted risks (single-user only):

  • Any web page you visit can reach the bridge. The bridge binds 127.0.0.1, but a page you browse can fetch("http://127.0.0.1:8765/command", ...) as a no-preflight "simple" request (or open ws://127.0.0.1:8765/extension) and issue commands to your browser. Localhost binding does not stop same-machine web content; only an Origin allowlist + shared token would, and neither is implemented.
  • The extension↔bridge link is unauthenticated cleartext ws, and the bridge host is user-configurable on the options page. Whatever answers on that socket gets full browser control. Keep the extension host set to 127.0.0.1 so Chrome talks to the local bridge.
  • Listening on 0.0.0.0 by default allows HTTP requests (e.g. from an MCP client on another LAN machine) to reach the bridge. If running on an untrusted network, override via HOST=127.0.0.1 npm start or firewall port 8765 accordingly.
  • get_cookies reads cookies for the whole browser profile (all sites), not just the target tab. There is no redaction on network/HAR/cookie output — headers (incl. Cookie / Authorization) come back verbatim, which is the point for a local debug tool.

Prompt injection still applies. A web page can embed hidden text that tries to hijack whatever agent is driving the browser (the same risk the official Claude in Chrome documents). No login removes that risk. When pointing an agent at untrusted pages, keep a human in the loop for anything destructive or sensitive.