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

@mehmoodqureshi/chrome-mcp

v0.6.7

Published

Drive your real Chrome browser over MCP — real logins, real cookies. A stdio MCP server (CLI) plus an MV3 extension, driving Chrome via chrome.scripting/chrome.tabs. Multi-tab batch automation, accessibility snapshots, deny-all security by default.

Readme

chrome-mcp

CI npm license

Let Claude use the Chrome you are already logged into. Not a fresh automated browser that greets every site as a stranger — your Chrome, with your sessions, your cookies, your 2FA already done. If you can see a page in your browser, your agent can read it, without logging in again and without pasting credentials anywhere.

Most browser MCP servers launch their own Chromium and hand your agent a signed-out window. chrome-mcp does the opposite: an MV3 extension dials into a localhost WebSocket server and drives the browser you already have open, through chrome.scripting/chrome.tabs. Works with Claude Code, Claude Desktop, and any other MCP host.

Distributed as an npx CLI (the MCP server) plus a load-unpacked extension.

This build is extension-only. It never launches or attaches a Chromium of its own, so the extension is required, not optional — without it, no tool can run. The CDP flags (--cdp-fallback, --no-cdp-fallback, --cdp-endpoint, --prefer) are still accepted for back-compat but are ignored.

Full design: docs/BLUEPRINT.md — architecture, wire protocol, the complete tool surface, the extension manifest, the security model, and the phased build plan.

Quickstart

1. Register the MCP server with your host.

claude mcp add chrome-mcp -s user -- \
  npx -y @mehmoodqureshi/chrome-mcp \
  --allow-domain example.com --enable-mutations --persist-token

Everything before -- belongs to Claude Code; everything after it is this server's command and flags. Keep the -- or --allow-domain gets read as a Claude Code option.

-s user registers it for every project on your machine. Use -s local (the default) for just the current project, or -s project to write a .mcp.json your team can commit.

Check it came up with claude mcp list. After upgrading the server, reconnect it with /mcp inside a session — no restart needed.

{
  "mcpServers": {
    "chrome-mcp": {
      "command": "npx",
      "args": ["-y", "@mehmoodqureshi/chrome-mcp",
               "--allow-domain", "example.com", "--enable-mutations",
               "--persist-token"]
    }
  }
}

By default everything is deny-all (no domains, no eval, no mutations). Grant exactly what you need with --allow-domain <glob> (repeatable), --enable-mutations, --enable-downloads, --enable-uploads, --unsafe-enable-eval, or --unsafe-all-domains.

--enable-uploads permits upload_file (setting local file(s) on a page's file <input>). It is off by default because sending local files to a page is an exfiltration risk; it is also gated by the destination-domain allowlist. Pair it with --uploads-dir <path> to restrict uploads to files inside that directory (.. traversal is blocked) — strongly recommended for unattended use.

Pair once, never again. Both examples above include --persist-token, which is what makes the pairing survive a restart — drop it if you'd rather have the stricter default described next.

Without --persist-token a fresh token is minted every boot (the secure default), which means re-pairing the extension on each restart. With it, the token is stored 0600 at ~/.chrome-mcp/token and reused; the extension's keepalive auto-reconnects with no manual step. CHROME_MCP_TOKEN pins the token explicitly (and is never written to disk).

2. Load the extensionrequired; the server can drive nothing without it.

extension-dist/ ships prebuilt inside the npm package, so there is nothing to compile. Install globally to get a stable path to it:

npm install -g @mehmoodqureshi/chrome-mcp
npm root -g     # → <root>; the extension is at <root>/@mehmoodqureshi/chrome-mcp/extension-dist

Then chrome://extensions → enable Developer modeLoad unpacked → select that extension-dist/ directory. (Working from a git clone instead? Run npm install && npm run build:ext first — extension-dist/ is gitignored.)

3. Pair it: run npx chrome-mcp --print-pairing to write the handshake and print its path, open the extension's Options page, and paste the port + token from ~/.chrome-mcp/handshake.json.

Running more than one session

The extension dials exactly one bridge port, so only one chrome-mcp can drive your Chrome at a time — but every MCP host session (each Claude tab/window) spawns its own server. With a pinned --port, the newest session takes the port over: it reads the owning pid from handshake.json, confirms that process really is a chrome-mcp, and stops it. Newest tab wins; the older session's browser tools go quiet until it reconnects. Nothing that isn't a verified chrome-mcp is ever touched — a port held by some other program is reported, never killed.

Two servers can only run side by side if each has its own port and its own paired extension — i.e. a separate Chrome profile running its own copy of the extension, pointed at the other port (--port 9223). A single Chrome pairs to one server at a time, so a second server with no extension of its own can drive nothing.

One server can, however, serve several browsers at once: connections are routed by profile key (--profile <name>, matching the profile set in the extension's Options), so each paired Chrome gets its own routing slot.

Without --port, each server binds an ephemeral port (no conflict ever), but the port changes every boot — so you'd re-pair the extension each time. Pin --port plus --persist-token for a pair-once setup.

Windows

WSL2 is not required — native Windows works. One config change is, though: on Windows npx is npx.cmd, a batch shim, and MCP hosts spawn the server without a shell, which cannot execute a .cmd. So "command": "npx" fails to start. Wrap it in cmd /c:

{
  "mcpServers": {
    "chrome-mcp": {
      "command": "cmd",
      "args": ["/c", "npx", "-y", "@mehmoodqureshi/chrome-mcp",
               "--allow-domain", "example.com", "--enable-mutations",
               "--persist-token"]
    }
  }
}

Or from Claude Code: claude mcp add chrome-mcp -- cmd /c npx -y @mehmoodqureshi/chrome-mcp --allow-domain example.com

Everything else is the same — load extension-dist/ from npm root -g and pair as above.

The tools cover tabs, navigation, interaction (click/type/press/hover/ scroll/select_option), reads (get_text/get_html/screenshot/eval/wait_for), an accessibility snapshot (interactive elements with stable refs the model can target instead of guessing CSS selectors), session access (get_cookies/storage), helpers (extract_links/read_as_markdown/fill_form/download_file/upload_file), and chrome_status. upload_file sets local file(s) on a file <input> without the OS dialog (requires --enable-uploads).

click/type accept trusted: true for real OS-level input (works on React/Vue controlled inputs); interactions auto-wait for the target to appear.

Driving several tabs at once — batch

batch runs many tool calls in one request — parallel (default) or serial (with optional stopOnError). Each sub-op goes through the same policy gate, rate limit, and error handling as a direct call (no bypass, no nesting). Use it to fan work out across tabs:

// open three product pages (background, so they don't fight for focus)…
{ "name": "batch", "arguments": { "ops": [
  { "tool": "tab_new", "args": { "url": "https://a.example/p" } },
  { "tool": "tab_new", "args": { "url": "https://b.example/p" } },
  { "tool": "tab_new", "args": { "url": "https://c.example/p" } }
]}}

// …then read them all at once (wall-clock ≈ the slowest one, not the sum)
{ "name": "batch", "arguments": { "ops": [
  { "tool": "get_text", "args": { "tabId": "<a tabId>" } },
  { "tool": "get_text", "args": { "tabId": "<b tabId>" } },
  { "tool": "get_text", "args": { "tabId": "<c tabId>" } }
]}}

In parallel mode, tab-scoped ops must pass an explicit tabId — the active-tab default is unsafe under concurrency, so it's rejected rather than silently mis-routed. (tab_new, tabs_list, chrome_status are exempt.)

tab_new focuses the new tab by default (so "open X" behaves like opening a link, instead of replacing your current page — use tab_new, not navigate, to open without losing the current tab). Pass active: false to open in the background; parallel batches do this automatically.

Status

v0.5.0 — safe multi-tab concurrency. Adds the batch fan-out tool, makes parallel tab automation race-free (explicit-tabId guard; per-tab chrome.debugger serialization; collision-free tab_new), captures screenshots via chrome.debugger (a specific tab without stealing focus — plus true full-page and element capture), and focuses newly opened tabs by default. 111 automated tests + a gated headed extension smoke.

v0.2.0 — all six build phases complete and green. End-to-end working: npx chrome-mcp ⇄ bridge ⇄ extension ⇄ your real Chrome, with a Playwright CDP fallback. v0.2 adds the accessibility snapshot + element refs, auto-wait, cookies/storage/select_option, trusted input (chrome.debugger), a toolbar status badge, and a stable pairing token (--persist-token).

  • [x] Phase 0 — Contracts & skeleton: shared/protocol.ts (wire contract), src/executor/types.ts (Executor interface), src/security/policy.ts (default-deny policy + capability gates), src/config.ts (CLI/env/policy resolution), build + test harness.
  • [x] Phase 1 — MCP server + StubExecutor: mcp/server.ts (clean-stdout stdio), mcp/tools.ts (28-tool catalog + never-throw dispatch + drift-check), validators/envelopes/helpers, ExecutorManager + StubExecutor, cli.ts. Point an MCP host at node dist/src/cli.js today.
  • [x] Phase 2 — WebSocket bridge + auth: bridge/server.ts (loopback WS, hello-token gate, welcome/unauthorized, displacement), bridge/auth.ts (per-boot 256-bit token, atomic-0600 handshake, SHA-256 timingSafeEqual), bridge/connection.ts (id-correlation, method-aware timeouts, backpressure, reject-all-on-close, heartbeat).
  • [x] Phase 3 — ExtensionExecutor + CdpExecutor + selection: executor/extension-executor.ts (Executor over the bridge), executor/cdp-executor.ts (Playwright connect/launch + lock recovery + tab resolution), executor/select.ts (extension-if-ping-responsive else CDP). CLI now starts the bridge, writes the 0600 handshake, and serves a real backend. Adds playwright.
  • [x] Phase 4 — MV3 extension: extension/manifest.json, sw/ws-client.ts (dial + hello/welcome + pong), sw/executor.ts (chrome.scripting/chrome.tabs command impls), sw/router.ts (never-throw + drift), sw/background.ts (top-level listeners + 25s keepalive/reconnect), options page (manual pairing), esbuild build → extension-dist/. Verified by a live --load-extension smoke (pair → navigate → get_text). Adds esbuild + @types/chrome.
  • [x] Phase 5 — Helpers, downloads, HITL: hardened download_file (shared/download.ts — path-traversal/dangerous-ext sanitize + size cap, wired into both backends), richer read_as_markdown, and a human-in-the-loop harness (hitl/npm run test:hitl [-- --include-mutating]) with pure, unit-tested gating. 50 automated tests.
  • [x] Phase 6 — Packaging & docs: files whitelist (ships dist/src, dist/shared, extension-dist, LICENSE, blueprint — not source/tests), prepack build, bin, quickstart + .mcp.json snippet. Verified by a tarball install smoke (npm pack → install → MCP tools/list).

Security posture (default)

Deny-all safe mode. With no policy configured: empty domain allowlist, eval off, downloads off, mutating tools off. Opt in explicitly:

chrome-mcp --allow-domain example.com --enable-mutations
chrome-mcp --policy ./policy.json          # see policy.example.json
chrome-mcp --unsafe-all-domains            # loud footgun

The per-boot 256-bit token in ~/.chrome-mcp/handshake.json (mode 0600) is the only trust boundary; it is never written to stdout/stderr. On POSIX the mode is re-verified after every write and the server fails closed if the file ends up group/other-accessible. Windows has no such bits — chmod there only toggles the read-only attribute — so the check is skipped and the token's confidentiality rests on the per-user ACL of %USERPROFILE%\.chrome-mcp.

Develop

npm install
npm run typecheck       # server/test sources
npm run typecheck:ext   # extension sources (@types/chrome)
npm run build:ext       # bundle the extension → extension-dist/
npm test                # builds, then runs node --test on dist/test
RUN_EXT_SMOKE=1 node --test dist/test/extension-smoke.test.js   # live, headed

The extension

extension/ builds (esbuild) to extension-dist/, loaded via chrome://extensionsLoad unpacked → select extension-dist/. Pair it from the extension's Options page using the port + token from ~/.chrome-mcp/handshake.json (run npx chrome-mcp --print-pairing to get the path).

Reads/interaction use chrome.scripting/chrome.tabs — no "is being debugged" banner, CSP-safe reads (isolated world), testable under Playwright. chrome.debugger is used only where it's needed and worth it: trusted: true input (real OS-level events on React/Vue inputs) and screenshot (captures a specific tab without activating it — safe under parallel batch — with true full-page and element capture). Those ops briefly show the debug banner while attached.