Mehmoodqureshi

MCP Browser Extension

Community Mehmoodqureshi
Updated

MCP browser extension and server: let Claude, Cursor or any MCP client use the Chrome you're already logged into. Real sessions and cookies, deny-all domain allowlist, 42 tools.

MCP Browser Extension

CInpmlicense

Website and docs: mcp-browser-extension.com ·Quickstart ·All 42 tools ·Install the extension from the Chrome Web Store

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

Most browser MCP servers launch their own Chromium and hand your agent asigned-out window. MCP Browser Extension does the opposite: an MV3 extension dials into alocalhost WebSocket server and drives the browser you already have open, throughchrome.scripting/chrome.tabs. Works with Claude Code, Claude Desktop, and anyother MCP host.

MCP Browser Extension has two parts: the extension, and a small MCP server you runwith npx (npm package and command: mcp-browser-extension). The extensioncomes from theChrome Web Storeor loaded unpacked.

This build is extension-only. It never launches or attaches a Chromium ofits own, so the extension is required, not optional — without it, no toolcan 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, wireprotocol, the complete tool surface, the extension manifest, the securitymodel, and the phased build plan.

Use Claude in your signed-in Chrome

You are already signed in to Gmail, GitHub, your analytics dashboards, the adminpanel, the CRM. MCP Browser Extension lets Claude (Claude Code, Claude Desktop, or anyother MCP host) work in those same tabs: no re-login, no second 2FA prompt, nopassword or cookie in a config file. The extension runs inside your normalChrome, so a page the agent opens is the page you would see.

  • Gmail — read a thread or search results in a tab you already have open.
  • GitHub — go through PRs, issues and settings pages as yourself, privaterepositories included.
  • Dashboards — pull numbers from analytics, billing or admin screens thathave no API, or whose API you never set up.

Other tools drive a signed-in Chrome too (the hangwin/mcp-chrome extension,for one), so the question is what you get on top of the session:

  • A per-domain allowlist, deny-all by default. With no flags the agent canread nothing. --allow-domain mail.google.com --allow-domain github.comopens exactly those hosts (*.example.com covers a domain and itssubdomains); every other site is refused before the call reaches the page.Reads are gated, not only clicks, and the extension re-checks the same policyon its side. Mutations, eval, downloads and uploads are separate opt-ins.
  • Password values are never returned, and --redact scrubs tokens, APIkeys and JWTs out of page reads.
  • An audit trail: every call is logged with the URL, the allow/denyverdict, duration and bytes returned.
  • It stays out of your tab: the agent opens its own tabs in the backgroundand works there, so the page you are on is never navigated, clicked or pulledout of focus. batch reads many of them at once in one call.
  • Several Chrome profiles: load the extension in your work and personalprofiles and switch between them with profile_use.
  • auth_check: when a session does expire, the agent gets an[AUTH_REQUIRED] signal instead of a confusing timeout, so it can stop andask you to sign in again. MCP Browser Extension never holds credentials or signs in foryou.

Setup is two pieces: the MCP server (npx, below) and the extension, which youcan install from the Chrome Web Storeor load from the folder the server unpacks.

Quickstart

Up and running in one paste

Hand this to your AI agent (Claude Code, Cursor, Windsurf, anything MCP) and itinstalls the server, wires it into the client, and walks you through the twosteps that must happen inside Chrome:

Set up MCP Browser Extension on this machine by fetching and following
https://raw.githubusercontent.com/Mehmoodqureshi/mcp-browser-extension/main/SETUP.md
exactly, step by step. Work autonomously and verify each step.

Prefer to read before you run an agent on your machine? SETUP.mdis the exact file the agent follows. The manual steps are below.

1. Register the MCP server with your host.

Claude Code (terminal) — one command, no config file to find
claude mcp add mcp-browser-extension -s user -- \
  npx -y mcp-browser-extension@latest \
  --allow-domain example.com --enable-mutations --persist-token

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

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

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

Claude Desktop and other MCP hosts — JSON config
{
  "mcpServers": {
    "mcp-browser-extension": {
      "command": "npx",
      "args": ["-y", "mcp-browser-extension@latest",
               "--allow-domain", "example.com", "--enable-mutations",
               "--persist-token"]
    }
  }
}

By default everything is deny-all (no domains, no eval, no mutations). Grantexactly 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 anexfiltration risk; it is also gated by the destination-domain allowlist. Pair itwith --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, whichis what makes the pairing survive a restart — drop it if you'd rather have thestricter default described next.

Without --persist-token a fresh token is minted every boot (the securedefault), which means re-pairing the extension on each restart. With it, thetoken is stored 0600 at ~/.mcp-browser-extension/token and reused; the extension'skeepalive auto-reconnects with no manual step. MCP_BROWSER_EXTENSION_TOKEN pins the tokenexplicitly (and is never written to disk).

2. Install the extension — required; the server can drive nothing without it.

Install MCP Browser Extension from the Chrome Web Store.One click, no Developer mode, and Chrome keeps it updated for you. This isthe way to install it.

3. Click Connect. A fresh install opens the extension's Settings with aConnect button (it is in the toolbar popup too). Click it, allow the onepermission Chrome asks for, and you are paired: the server registers a smallhelper with Chrome every time it starts, and the button asks it for the port andtoken. Chrome lets only this extension talk to that helper. If the server everchanges its token, the extension fetches the new one by itself. Nothing to copyor paste.

Developers: load it unpacked instead

The extension also ships inside the npm package, and every time the server bootsit copies it to ~/mcp-browser-extension (%USERPROFILE%\mcp-browser-extension onWindows; MCP_BROWSER_EXTENSION_EXTENSION_DIR moves it). chrome://extensions → enableDeveloper mode → Load unpacked → pick that folder. It pairs itself from apairing.json the server writes there, and the server refreshes the files on eachboot (never with an older build) so it reloads itself within 30 seconds.

An unpacked copy does not update through Chrome, so its popup suggests the storecopy. Fresh unpacked builds carry the store's public key and so have the same id:load one or the store copy in a Chrome profile, not both. Updates preserve theID of an existing unpacked installation, including older copies without amanifest key; those continue to pair through the bundled pairing.json.Working from a git clone?npm install && npm run build:ext first; extension-dist/ is gitignored.npm run clean clears server build output and keeps extension-dist/, whosemanifest and pairing data may belong to an installation already loaded in Chrome.

Where to see the badge: it sits on the extension's icon in Chrome'stoolbar, not on the chrome://extensions page. Chrome hides new extensionsbehind the puzzle-piece button at the right of the address bar, so click that,find MCP Browser Extension, and click the pin next to it once; the icon thenstays in the toolbar. Hover it for the status in words.

Badge Meaning
green dot paired and connected
yellow dots connecting
grey circle not paired yet (no server has run, or no pairing file)
red exclamation mark token rejected; the server rotated it, re-pairs by itself in a moment

Manual fallback (the helper is turned off with --no-native-host, or Connectreports a problem): run npx -y mcp-browser-extension@latest --print-pairing,open the extension's Settings, and paste the port + token from~/.mcp-browser-extension/handshake.json.

Extension missing after restarting Chrome

Run the read-only diagnostic to see the actual installation type, missing files,Chrome disable reasons, and any conflicting extension registrations:

npx -y mcp-browser-extension@latest extension doctor

Older unpacked copies could acquire a new ID when an update added the Web Storepublic key to their manifest. Chrome then retained both IDs for one folder andskipped that folder on its next startup. Updates now preserve existing IDs.For an installation already affected by this exact conflict:

npx -y mcp-browser-extension@latest extension repair
# Review the preview, fully quit Chrome and other Chromium browsers, then:
npx -y mcp-browser-extension@latest extension repair --apply

The repair backs up the original preferences under~/.mcp-browser-extension/extension-repair-backups/, removes only the confirmed obsoleteregistration, and preserves the current extension and its saved settings. Itrefuses to write while a Chromium browser is running. Reopen Chrome afterwards.Use --user-data-dir <directory> for a custom browser data location, orextension doctor --json for a diagnostic report. Reports contain extensionpaths and registration status, but no browsing history, cookies or pairing tokens.

A Web Store installation with no matching stale-ID conflict is left unchanged.If the diagnostic finds missing files, a disabled Store copy, or no registration,that needs its own diagnosis; this command does not assume the cause or resetthe browser profile. When moving from an unpacked copy to the Store version,remove the unpacked copy first, then install from the Store.

Running more than one session

Every MCP host session (each Claude terminal, tab or window) starts its ownMCP Browser Extension server, and they all share your Chrome at once. The first one to start ownsthe bridge port and the extension connections — the hub. Each later sessionfinds the port held by a live MCP Browser Extension server and joins it as a peer: its toolcalls are relayed through the hub to the same browsers, so every session keepsworking side by side. Nobody is disconnected.

When the hub's session ends, its peers race for the port; one takes it over(with the same token, so the extension re-pairs by itself within a few seconds)and the rest join the new hub. A call that was in flight at that moment failsonce with EXTENSION_DISCONNECTED and is retried automatically when it is safeto repeat.

Peers authenticate with the pairing token from the 0600 handshake file, so onlyyour own OS user can join. A server too old to share the port is replaced asbefore: it is verified to be an MCP Browser Extension server, then stopped. Anything that isn't averified MCP Browser Extension server is never touched — a port held by some other program isreported, never killed.

Sessions share one browser, so they also share its tabs: two sessions drivingthe same tab at the same moment will step on each other. Give each session itsown tabs (tab_new), or its own Chrome profile (below).

Each session can also drive several browsers at once: load the extensionin each Chrome profile and they all pair to the same server, each under its ownprofile name. Tools act on the active profile — pick it with --profile <name>at startup or the profile_use tool at runtime.

Naming is automatic. Chrome won't tell an extension which profile it runs in, soeach install keeps a random id and the server names it: the first browser isdefault, the next profile-2, then profile-3, and so on. Names are stored in~/.mcp-browser-extension/profiles.json, so a browser keeps its name across restarts.chrome_status lists every paired browser (with its active tab as a hint), andprofile_rename gives one a friendly name (profile-2 → work). To pin a nameyourself instead, type it into the extension's Options → Profile; that always wins.

Without --port, each server binds an ephemeral port (no conflict ever), but theport changes every boot — so you'd re-pair the extension each time. Pin --portplus --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 serverwithout a shell, which cannot execute a .cmd. So "command": "npx" fails tostart. Wrap it in cmd /c:

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

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

For normal use, install the extension from the Chrome Web Store as describedabove, then click Connect. Do not use Load unpacked as part of the normalWindows setup. The unpacked developer build has the same extension ID as theStore build, so loading it can replace the Store installation; Chrome may thendisable the non-Store copy. If you are developing mcp-browser-extension, load%USERPROFILE%\mcp-browser-extension in a separate Chrome profile.

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 cantarget 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 theOS dialog (requires --enable-uploads).

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

Use it from anywhere — --tunnel

Drive the Chrome on your computer from claude.ai, the Claude mobile app, ChatGPT,Claude Code on another machine, or an agent running on a server.

brew install cloudflared            # once (Windows: winget install --id Cloudflare.cloudflared)
npx -y mcp-browser-extension@latest --tunnel --allow-domain example.com --enable-mutations

It prints a connector URL like https://<random>.trycloudflare.com/mcp/<key>:

  • claude.ai / Claude mobile: Settings > Connectors > Add custom connector, paste the URL.
  • ChatGPT (web): Plugins > Add > Add custom MCP server. Choose Server URL,paste the complete connector URL (including /mcp/<key>), and choose Noauthentication. Create and install the personal plugin, then select it with@ in a new chat. Keep the tunnel running.
  • Claude Code elsewhere / agents: use <url>/mcp with Authorization: Bearer <key>;the exact claude mcp add line is printed too.

The key (32 random bytes, kept in ~/.mcp-browser-extension/remote-key) guards everyrequest; a wrong or missing key gets a bare 404. Anyone holding the URL can driveyour browser within the allowed sites, so keep it private and keep the allowlisttight. --rotate-remote-key cuts every client off. The quick-tunnel URL changes oneach start; --http alone serves on 127.0.0.1:38018 (--http-port) for yourown tunnel (ngrok, Tailscale Funnel, a reverse proxy). The server runs until Ctrl-C.

Proxy per Chrome profile — proxy_status

Chrome applies a proxy to a whole profile, never one tab, so keep a Chrome profileper IP: set the proxy on the extension's Settings page in that profile, name theprofile there, and switch with profile_use. A dead proxy fails pages rather thanfalling back to your real IP. proxy_status reports the exit IP and country; theagent cannot change the proxy or read its password.

Driving several tabs at once — batch

batch runs many tool calls in one request — parallel (default) orserial (with optional stopOnError). Each sub-op goes through the samepolicy 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 — theactive-tab default is unsafe under concurrency, so it's rejected rather thansilently mis-routed. (tab_new, tabs_list, chrome_status are exempt.)

The agent works in its own tab, not yours. tab_new opens in thebackground and that tab becomes the agent's tab: calls without a tabId gothere. navigate with no tab of its own yet opens one rather than replacingyour page, and tab_new only reuses blank tabs it opened itself, never a NewTab page of yours. Before the agent opens anything, reads see your active tab,so "summarize this page" still works; to act on your page, the agent passes itstabId from tabs_list. Pass active: true to bring a tab to the front.

Reaching into iframes and shadow roots

A selector that "should" match but doesn't almost always means the element issomewhere your selector cannot reach: inside an <iframe> (checkout widgets,OAuth consent screens, embedded editors) or inside a web component's shadow root.

Shadow roots are handled for you — every selector and every ref now resolvesthrough open shadow roots, so anything snapshot shows you is something you canclick. (It used to show you elements no click could reach: the snapshot walkedshadow roots, the actions did not.)

Frames are opt-in, because reaching into one is a decision:

frames_list {}                                  // what frames exist, and their URLs
click { "selector": "#pay", "allFrames": true } // find it in whichever frame has it
get_text { "frameId": 7 }                       // pin one frame

Every frame is authorized against its own URL before anything runs in it, soan allowlisted page embedding a third-party iframe does not become a way to readthat third party. Frames whose origin isn't on your allowlist are skipped.

Seeing why a page broke — console_logs, network_log, dialogs

Reading the DOM tells you what a page looks like after it failed, not why. With--enable-observers, an in-page hook records console output, uncaught errors,and fetch/XMLHttpRequest traffic, and intercepts native dialogs:

console_logs { "level": "error" }        // the exception the page swallowed
network_log  { "failedOnly": true }      // the 500 behind the blank screen
dialogs      { "policy": "accept" }      // answer confirm() with true from here on

It is off by default and deliberately so: the hook patches console,fetch, XMLHttpRequest and the dialog functions on every allowlisted page inyour real browser. When it's on, it is registered only for the domains on yourallowlist, at document_start (so it catches load-time failures), and nothing itrecords leaves the page until a tool call reads it — through the same gate as anyother page read.

Dialog interception is also a fix, not just an observation: alert/confirm/beforeunload block the renderer, so a click that opened one used to hang everyinjected script until the command timed out and reported TIMEOUT with nothingto point at. With observers on, the dialog is answered (dismiss by default:confirm → false, prompt → null) and recorded.

What network_log sees: the requests page code makes — fetch andXMLHttpRequest, with method, URL, status and duration — plus Resource Timingentries (scripts, images, styles) when you ask for them. Not the documentrequest, redirects, or headers. That is the cost of not holding a debuggersession open on your browser.

Only what changed — snapshot { diff: true }

A snapshot is the most expensive read in the tool surface, and the loop that usesit most (snapshot → click → snapshot) re-sends a page that is mostly identicalevery time. Ask for the delta instead:

snapshot { "diff": true }                       // added / removed / changed only
click { "selector": "#save", "snapshotAfter": true }   // what the click changed

Nodes are matched across snapshots by role + accessible name, not by ref —refs renumber in document order on every snapshot, so diffing on them wouldreport an unchanged button as removed-and-re-added the moment anything above itappears.

Targeting by role and name

Actions accept a locator instead of a CSS selector, so you don't need a snapshotfirst just to learn a ref:

click { "role": "button", "name": "Sign in" }
type  { "role": "textbox", "name": "Email", "text": "[email protected]" }

Resolution is server-side and refuses to guess: an ambiguous locator fails withthe candidates listed rather than acting on the first one (pass nth to pick).

Did the session expire? — auth_check and failOnAuthWall

Reusing a signed-in Chrome removes the login step, but a session cookie canstill expire mid-run. Without a distinct signal the next step fails asSELECTOR_NOT_FOUND or TIMEOUT, and an eval harness scores the run as anagent failure when it was an auth failure. Every snapshot now carries anauthWall verdict when the page looks like a sign-in wall, and there is adedicated probe:

auth_check {}                                   // { authRequired, confidence, signals }
auth_check { "failOnAuthWall": true }           // [AUTH_REQUIRED] error instead
navigate  { "url": "https://app.example.com/dashboard", "failOnAuthWall": true }
snapshot  { "failOnAuthWall": true }

For a harness, set it once instead of per call:

npx -y mcp-browser-extension@latest --allow-domain app.example.com --enable-mutations --fail-on-auth-wall

With the flag on, every step that can move the tab (navigate, click, type,select_option, press, fill_form, back, forward, reload) checks thepage it landed on and fails with [AUTH_REQUIRED] if that page is a sign-inwall, and a wait_for that times out on such a page reports [AUTH_REQUIRED]instead of [TIMEOUT]. Each guarded step costs one extra snapshot round-trip;with the flag off the cost is zero. [AUTH_REQUIRED] is where a harness pausesfor a human to sign in again in the same Chrome, then retries the step. MCP Browser Extensionnever re-authenticates on its own: it holds no credentials, by design.

Detection reads only what the snapshot already has: the URL (sign-in routes,identity-provider hosts such as accounts.google.com, login.microsoftonline.com,Okta, Auth0), the title, password fields, and sign-in controls. highconfidence needs two independent cues (a password field plus a sign-in button,say); a lone password field or a bare /auth/... URL is medium. A header"Sign in" link on an ordinary page never counts. failOnAuthWall fires only onhigh, so a harness can bucket [AUTH_REQUIRED] separately from every otherfailure while a settings page with a "current password" field carries on.

Printing — print_pdf

print_pdf { "landscape": true }

Renders through Chrome's own print pipeline and saves to the task's results/dir, returning the path and size. The bytes themselves are never returned — aPDF is megabytes of base64 no model can read.

Paying less per turn — --tools

Every MCP server you connect costs context before you ask it anything: the hostsends the whole tool catalog to the model on every turn. MCP Browser Extension's 40tools are 28 KB of JSON Schema, about 7.1k tokens, on each one.

Most runs need a handful of them. --tools advertises only those:

npx -y mcp-browser-extension@latest \
  --allow-domain app.example.com --enable-mutations \
  --tools tabs_list,tab_new,navigate,snapshot,click,type,get_text

That surface is 6.0 KB, ~1.5k tokens — a 78% cut against the full catalog,for a run that was never going to print a PDF or upload a file.

  • Comma-separated and repeatable: --tools navigate,get_text --tools click.
  • A tool left out is hidden from tools/list and refused if called — abatch op naming it fails the same way an unknown tool does. Hiding a tool isa real restriction, not a display filter. It is not a substitute for thepolicy gate, though: --tools eval still does nothing without--unsafe-enable-eval.
  • An unknown name fails at startup and prints the catalog, so a typo can neverquietly drop click from the surface.
  • mcp-browser-extension --help prints the full catalog of 42 names to pick from.

Status

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

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

  • 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/policyresolution), build + test harness.
  • Phase 1 — MCP server + StubExecutor: mcp/server.ts (clean-stdoutstdio), 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.
  • 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).
  • 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 elseCDP). CLI now starts the bridge, writes the 0600 handshake, and serves areal backend. Adds playwright.
  • 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/. Verifiedby a live --load-extension smoke (pair → navigate → get_text). Addsesbuild + @types/chrome.
  • 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-loopharness (hitl/ — npm run test:hitl [-- --include-mutating]) with pure,unit-tested gating. 50 automated tests.
  • 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 atarball 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:

mcp-browser-extension --allow-domain example.com --enable-mutations
mcp-browser-extension --policy ./policy.json          # see policy.example.json
mcp-browser-extension --unsafe-all-domains            # loud footgun
mcp-browser-extension --enable-observers              # console/network/dialog capture (patches page globals)
mcp-browser-extension --redact                        # scrub secret-shaped strings out of page reads

Allowing a site without a restart. When a call is refused because its siteis not allowed, the extension's toolbar icon shows a ?. Click it: the popuplists the blocked site with an Allow button, shows whether the site you are onis allowed (with its own Allow), and lists the allowed sites. The Options page(Settings in the popup) also has a box to allow any other site. An allowed site applies at once to every session sharing that Chrome, andis saved to ~/.mcp-browser-extension/allowed-sites.json so it survives restarts. Sitesallowed there can be removed there; sites from --allow-domain stay until theflags change. The catch-all * can only come from --unsafe-all-domains. Theagent cannot approve a site itself: MCP Browser Extension never reads or drives theextension's own pages. Run with --no-site-grants to keep the allowlist exactlywhat the flags say.

What comes back is gated too. The allowlist decides which pages may be read;it says nothing about what is on them. A logged-in page routinely renders asession token into a script tag or an API key onto a settings screen.

  • Password field values are always suppressed — in get_html, and insnapshot, where the field still appears (so you can type into it) flaggedsecret: true with no value. No flag, no opt-in: nobody wants those characters.
  • --redact additionally scrubs secret-shaped strings — JWTs, AWS/GitHub/Slack/Google keys, Bearer headers, private-key blocks — out of get_text,get_html, read_as_markdown and eval. It is opt-in because a pattern willeventually fire on something you actually wanted. --redact-pattern <regex>adds your own (and implies --redact); an invalid one fails at startup ratherthan silently never matching.
  • Redaction runs before the output cap, so a truncated read cannot leak whata full one would have hidden.

Every call is recorded to the task's history.jsonl with the URL it touched, thepolicy verdict (allowed/denied), how long it took, how many bytes came back,and how many secrets were scrubbed — so "what did the agent do in my browser" hasan answer after the fact.

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

Updates

You don't have to do anything to stay current. The setup snippets above use@latest, and on top of that an installed copy (npx or a global install) checksnpm when it starts: if a newer version is published, it starts that version inits place, with the same arguments, so an MCP config written months ago stillruns the newest release. The check gives up after 1.5 s when you are offline,and a copy run from a git checkout never updates itself. The new server alsorefreshes the unpacked extension in ~/mcp-browser-extension, which reloadsitself; the Chrome Web Store copy updates through Chrome as usual. Turn it offwith --no-auto-update or MCP_BROWSER_EXTENSION_AUTO_UPDATE=0.

Telemetry

The MCP Browser Extension server (not the extension) sends anonymous usage statistics to PostHog, so theproject can see how many installs are active, which versions and platforms arein use, and which tools fail most. A notice is printed the first time it runs.

What is sent: a random install id (kept in ~/.mcp-browser-extension/telemetry.json), theserver version, OS, CPU architecture and Node major version, whether thesession owns the bridge port or shares it, how many browsers are paired, thepaired extension's version and whether it is the store or an unpacked copy, andper-tool call and error counts with error codes. The first batch goes out aminute after the first tool call, then every 10 minutes. For PostHog's MCPAnalytics view, each tool call is also sent as one $mcp_tool_call eventcarrying only the tool name and its group (reading, navigation...), itsduration, whether it failed and the error code, the MCP client's name andversion (such as claude-code), the MCP protocol revision, the AI model when theclient names it in its own request metadata (Codex does), and a random sessionid that changes after 30 idle minutes. When a client connects, one event recordsthat handshake and the names of the tools offered. Nothing an agent sends orreads: no arguments, results, error messages or intents. Batched every 15seconds. One pair_check eventa minute after startup says whether a browser paired and, if not, which of a fewfixed reasons applies (no_extension, token_mismatch, version_mismatch,profile_mismatch), so setup problems can be fixed where they happen.

What is never sent: URLs, domains, tool arguments, page content, screenshots,cookies, profile names, tokens, file paths, or anything you type. Events arepersonless and GeoIP lookup is disabled.

The browser extension sends nothing — it only ever talks to 127.0.0.1.

Turn it off with any of:

MCP_BROWSER_EXTENSION_TELEMETRY=0     # or false / off
DO_NOT_TRACK=1
--no-telemetry             # server flag

Learn more

The full documentation, setup guides for each MCP client and the security modellive on the website: MCP Browser Extension docs.

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

Published on the Chrome Web Store asMCP Browser Extension.Extension versions move only when extension/ changes, so the listed build cansit a release behind the npm package; the two negotiate capabilities onconnect, so an older extension loses features rather than breaking.

For normal use, install from the Chrome Web Store above. The following is onlyfor local development; do not load this unpacked copy in the same Chrome profileas the Store extension. On Windows, Chrome may disable a non-Store build.

extension/ builds (esbuild) to extension-dist/, loaded viachrome://extensions → Load unpacked → select ~/mcp-browser-extension, themirror the server refreshes from extension-dist/ on every boot (loadingextension-dist/ directly also works). It pairs itself from the pairing.jsonthe server writes into that folder; the Options page paste of port +token from ~/.mcp-browser-extension/handshake.json (runnpx -y mcp-browser-extension@latest --print-pairing to get the path) is onlythe fallback.

Reads/interaction use chrome.scripting/chrome.tabs — no "is beingdebugged" banner, CSP-safe reads (isolated world), testable under Playwright.chrome.debugger is used only where it's needed and worth it: trusted: trueinput (real OS-level events on React/Vue inputs) and screenshot (captures aspecific tab without activating it — safe under parallel batch — withtrue full-page and element capture). Those ops show the debug banner whileattached; the session lingers 1.5s after the last op so a burst of themattaches once. Screenshots are JPEG (quality 70) at CSS-pixel size bydefault — pass format: "png", quality, or scale to change that.

MCP Server · Populars

MCP Server · New

    pipelab-org

    pipelock

    Open-source AI agent firewall for MCP security and agent egress. Scans mediated HTTP, MCP, A2A, and WebSocket traffic for exfiltration, SSRF, and prompt injection, and emits mediator-signed action receipts: verifiable audit evidence from outside the agent.

    Community pipelab-org
    zachshallbetter

    adobe-mcp

    MCP server that turns Adobe InDesign into an AI design automation platform — 135+ tools for layout, styles, export, and publishing workflows

    Community zachshallbetter
    obsei

    obsei

    Obsei is a low code AI powered automation tool. It can be used in various business flows like social listening, AI based alerting, brand image analysis, comparative study and more .

    Community obsei
    colorjs

    color-space

    Open collection of color spaces

    Community colorjs
    chaitin

    百智云 Agent Toolkit

    MCP Server

    Community chaitin