A from-scratch Chrome extension that replaces the bundled mcp-chrome-bridge
extension. It connects to the same native host (com.chromemcp.nativehost)
shipped by mcp-chrome-bridge,
so any MCP client pointed at http://127.0.0.1:12306/mcp (or your configured
port) gets full browser control with zero changes to the host or the MCP server.
Built against the protocol reverse-engineered from mcp-chrome-bridge@1.0.31
(see Protocol below).
Companion CLI: app/mcpctl — a zero-dependency standalone CLI packaged as a
single Windows .exe via Node SEA (see Standalone CLI).
+--------------------------------- YOUR MCP CLIENT ---------------------------------+
| mcpctl.exe (SEA, app/) | Claude Desktop | Cherry Studio | other MCP tools |
+----------------------------------------+-------------------------------------------+
|
streamable HTTP -> http://127.0.0.1:12306/mcp
|
+----------------------------------------v-------------------------------------------+
| NATIVE HOST (Node + Fastify) |
| mcp-chrome-bridge@1.0.31 | singleton MCP server (1 session) |
| forwards tool calls to the extension over native messaging |
+----------------------------------------+-------------------------------------------+
|
native messaging (stdio) -> com.chromemcp.nativehost
|
+----------------------------------------v-------------------------------------------+
| EXTENSION (MV3 -- this repo) |
| background.js service worker: native port mgmt + tool dispatcher |
| lib/protocol.js wire protocol constants, tool registry, result helpers |
| lib/cdp.js promisified chrome.debugger (CDP eval, capture) |
| lib/tabs.js tab resolution + executeScript helpers |
| tools/*.js 56 tools: tabs | windows | content | network | |
| media | data | injection | instance | misc |
| content/ MAIN-world console capture (document_start) |
| popup/ status + connect/disconnect UI |
| alarms: 'bridge-reconnect' (retry every 0.5 min) |
| 'bridge-keepalive' (ping host every 30 s) |
+-----------+--------------------------------------------+--------------------------+
| |
chrome.* APIs chrome.debugger (CDP)
| |
+-----------v------------------+ +--------------v---------------------------+
| TABS / WINDOWS / | | PAGE -- DOM, network capture, |
| history / bookmarks / | | console, screenshots, JS eval |
| downloads / dialogs | +-----------------------------------------+
+------------------------------+
manifest.json MV3 manifest with pinned identity (key → stable ID)
background.js Service worker: native-messaging client + tool dispatcher
lib/
protocol.js Wire-protocol constants, tool registry, result helpers
instance.js Multi-browser identity (instanceId, label, own MCP port)
cdp.js chrome.debugger promisified helpers (CDP eval, capture)
tabs.js Tab resolution + multi-tab id resolution helpers
gif-encoder.js Dependency-free GIF89a encoder (median cut + LZW)
published-tools.js Descriptors exposing new tools to MCP clients (flow.*)
tools/
browser.js Tab toolkit: open/duplicate/reload/discard/pin/mute/move,
groups, search, details, extended close + navigation
windows.js New/close/manage/arrange windows + tab zoom
content.js chrome_get_web_content, chrome_get_interactive_elements
interaction.js chrome_click_element/fill_or_select/keyboard/javascript
network.js chrome_network_request + chrome_network_capture
screenshot.js chrome_screenshot (viewport, full-page, element via CDP)
console.js chrome_console (buffer from content script)
data.js chrome_history, chrome_bookmark_*
data-ext.js chrome_cookies, chrome_downloads, cross-tab content search
perf.js Real CDP performance tracing (start/stop/analyze)
gif.js Animated GIF recorder (fixed-FPS + action-driven)
flows.js record_replay_flow_run dispatcher for published tools
inject.js chrome_inject_script, chrome_send_command_to_inject_script
misc.js chrome_read_page, chrome_computer, dialogs, uploads,
element selection
instance.js bridge_get_instance_info, bridge_set_instance_label
(multi-browser identity — see Multi-browser)
content/
console-capture.js document_start content script that buffers console
console-capture-main.js same, running in the MAIN world
popup/ Status + connect/disconnect UI
app/
mcpctl.js Standalone CLI (zero deps) — full command list in app/README.md
build.js Builds app/dist/mcpctl.exe via Node SEA + postject (default packaging)
build-launcher.js Alternative packaging: a ~1 KB node launcher (mutually exclusive with the exe)
bin/mcpctl.js npm `bin` entry, so `npm link` also exposes `mcpctl`
scripts/
make-icons.js Generates the PNG icons (pure Node, no deps)
register-host.js Adds this extension's ID to the native host allowed_origins
mcp.js / mcp-cli.js Legacy in-repo CLI (superseded by app/mcpctl.js)
mcp-batch.js Batch runner for the legacy CLI
build-exe.js Builds dist/mcp.exe from scripts/mcp.js via Node SEA
host-roundtrip-test.js / live-feature-test.js / unit-sim-console.js
Test harnesses (native host round-trip, live tools, sim console)
test-gif-encoder.js Unit tests for the GIF encoder (structure + LZW round-trip)
test-instance.js / test-mcpctl-instances.js / test-launcher.js
Unit tests for instance identity, CLI targeting, packaging
— these three are what `npm test` runs
probe-cdp.js Diagnostic: exercises trace + GIF recorder directly and
dumps raw MCP responses (also self-heals a wedged host)
-
Install the native host (once):
npm install -g mcp-chrome-bridgethenmcp-chrome-bridge register. (If it's already installed, skip this.) -
Grant this extension access to the host — the registered host manifest only allows the official extension ID, so ours must be added:
node scripts/register-host.js --apply
Then fully restart Chrome (the native-messaging manifest is read at browser start).
-
Open
chrome://extensions, enable Developer mode, click Load unpacked and select this folder (chrome-mcp-extension/). -
Click the extension icon → Connect. The MCP server starts on this instance's own port —
12306for the first browser, and the next free port in12306–12335for each additional one (configurable in the popup; see Multi-browser).
The extension's ID is stable because manifest.json pins a key (an RSA
public key). The ID derived from it is agfodficabgggjoapjaphagdcpnoeggc, which
is what register-host.js whitelists. Re-loading or moving the folder never
changes it.
Point any MCP client (Claude Desktop, Cherry Studio, bridge-raw.js, etc.) at:
http://127.0.0.1:12306/mcp
Example one-shot check with the repo's raw client:
node bridge-raw.js chrome_get_windows_and_tabs '{}'
node bridge-raw.js chrome_navigate '{"url":"https://example.com"}'Load the same extension folder in several browsers (Chrome, Edge, Brave, Chromium, Opera) or several profiles, and each one becomes its own bridge instance with its own MCP port — no port fights, no cross-talk:
- Each instance gets a stable
instanceId(generated once, stored inchrome.storage.local) and a deterministic default port in the12306–12335range (hash of the instanceId), persisted once chosen so restarts keep the same endpoint. - On a port collision (
EADDRINUSEfrom another instance) the extension automatically moves to the next free port instead of squatting on the wrong browser's server — you never accidentally drive browser A while pointing at browser B's endpoint. - Give instances friendly labels in the popup (e.g.
chrome-work,edge-victim) so you can target them by name.
Discover and target instances with mcpctl:
mcpctl browsers # list every live browser + the selector to use for it
mcpctl browsers --json # same, machine-readable
mcpctl browsers --range 12306-12340
mcpctl --browser chrome status # target one browser by name
mcpctl --browser edge:12311 tabs # name pinned to a port (always unique)
mcpctl --browser port:12311 eval 'document.title'
mcpctl --browser id:3f2a1c9d tabs
mcpctl --port 12311 shot --out edge.png
mcpctl --browser edge label work # name that browser...
mcpctl --browser work tabs # ...then target it by namebrowsers (alias instances, list) needs no session and no lock, so it
still works when the CLI's own default port is down — which is exactly when you
need to find the other browsers. It scans the range in parallel (~50 ms) and
prints, for each live instance, the browser/version, its label, its
instanceId, its endpoint, and the exact --browser selector to use.
--browser accepts a label, name:port, port:<n>, id:<prefix> or a plain
browser name. If a plain name matches several instances the CLI lists the
candidates instead of guessing; if only one Chrome (say) is live, --browser chrome still resolves. --port always wins over --browser.
Because the host serves a singleton MCP session, an instance whose session
is currently held by another client (an IDE, or a parallel mcpctl) cannot be
queried for its name. The CLI keeps a small identity cache in the temp dir
keyed by port, so a busy instance is still listed and still targetable; and if
the loaded extension is older than the CLI (no identity tool), browsers falls
back to describing each instance by its tabs and tells you to reload the
extension.
Every browser that should run the bridge needs the native host registered for
it (run node scripts/register-host.js --apply — it now covers Chrome,
Chromium, Microsoft Edge, and the Brave variants), and the extension loaded
in that browser with Connect pressed. mcpctl browsers only shows
browsers that are actually running.
Typical multi-browser workflows: race-condition testing with two sessions, role-A-vs-role-B acting as two users in separate browsers, simulating a victim browser for CSWSH / stored-XSS verification, or keeping your primary hunting browser clean while a second browser holds the authenticated session.
The extension speaks the native-messaging protocol used by the official
extension (reverse-engineered from the installed mcp-chrome-bridge package):
- Host name:
com.chromemcp.nativehost(stdio native messaging, 4-byte little-endian length framing handled by Chrome). - On connect the extension sends
{type:"start", payload:{port:12306}}; the host boots the Fastify MCP server and repliesserver_started. - The MCP server forwards tool calls to the extension as
{type:"call_tool", payload:{name, args}, requestId}. - The extension replies with
{responseToRequestId, payload:{status:"success", data}}wheredatais an MCP result ({content:[{type:"text",text}]}), or{responseToRequestId, payload:{status:"error", error:"…"}}. rr_list_published_flowsis answered with the descriptors inlib/published-tools.js— this is how MCP clients discover the tab/window toolkit (each item shows up as aflow.<slug>dynamic tool). Calls to those tools are proxied back asrecord_replay_flow_runand dispatched to the real tool bytools/flows.js.- Liveness: the worker pings the host (
ping_from_extension) via a keepalive alarm every 30 s, and reconnects with unlimited retries via abridge-reconnectalarm (service-worker-safe — a busy service worker could otherwise be idle-killed, which drops the native port and kills the host). - Ports: the extension asks the host to bind its own port (see
Multi-browser below).
EADDRINUSEfrom the host is treated as a collision with another instance and triggers an automatic move to the next free port, not as "already running". - Identity: the extension registers a
bridge_get_instance_infotool (browser name/version,instanceId, label, own port) that the CLI uses for instance discovery.
Instance: bridge_get_instance_info (multi-browser discovery — browser
name/version, instanceId, label, own MCP port) and
bridge_set_instance_label (name this browser from the CLI via
mcpctl [--browser <sel>] label <name>, or from the popup). Both are dispatched
through the host like any other tool; they are intentionally absent from
tools/list, which the host builds from a static schema list.
Tabs — get_windows_and_tabs, chrome_open_tabs (one or many URLs, active /
background / pinned / window / index), chrome_duplicate_tabs,
chrome_reload_tabs (optionally bypassing the cache), chrome_discard_tabs
(suspend), chrome_pin_tabs / chrome_unpin_tabs, chrome_mute_tabs /
chrome_unmute_tabs, chrome_move_tabs (reorder or move between windows),
chrome_group_tabs / chrome_ungroup_tabs / chrome_tab_groups,
chrome_search_tabs (by title/URL/host), chrome_tab_details,
chrome_close_tabs (ids, URL, domain, whole windows, all, allExcept),
chrome_switch_tab, chrome_go_back_or_forward, chrome_navigate (plus
newTab to open in a new tab).
Windows — chrome_new_window (urls, incognito, size, state),
chrome_close_windows, chrome_manage_window (minimize/maximize/fullscreen/
focus/resize/move), chrome_arrange_windows (tile in a grid, vertical,
horizontal, or cascade on the primary display), chrome_zoom (get/set/reset a
tab's zoom factor).
Content & interaction — chrome_get_web_content,
chrome_get_interactive_elements, chrome_read_page (ref-based element tree),
chrome_click_element, chrome_fill_or_select, chrome_keyboard,
chrome_javascript, chrome_computer (screenshot / click / type / key /
scroll / scroll_to / wait / resize_page / hover / fill / fill_form subset),
chrome_search_tabs_content (search the visible text of open tabs).
Network — chrome_network_request (page-context fetch with cookies),
chrome_network_capture (start/stop; webRequest entries, optional response
bodies via the debugger API).
Media & performance — chrome_screenshot (viewport via
captureVisibleTab; full-page and element captures via CDP),
chrome_gif_recorder (record a tab as an animated GIF: fixed-FPS or
action-driven auto-capture, then stop/export; stop returns metadata by
default — pass includeBase64:true to get the GIF data inline, which is
trimmed automatically if it would exceed the native host's 16MB per-message
cap), performance_start_trace / performance_stop_trace /
performance_analyze_insight (real CDP tracing with trace JSON export and
summaries).
Data — chrome_history, chrome_bookmark_search, chrome_bookmark_add,
chrome_bookmark_delete, chrome_cookies (get/getAll/set/delete/deleteAll),
chrome_downloads (list/cancel/pause/resume/erase/open/show/removeFile).
Injection & misc — chrome_inject_script,
chrome_send_command_to_inject_script, chrome_console (snapshot/buffer),
chrome_handle_dialog, chrome_handle_download, chrome_upload_file (CDP
file input), chrome_request_element_selection (click-to-pick overlay),
record_replay_flow_run (flow.* dispatcher).
The recommended client. Zero-dependency Node script, packaged as a single Windows executable via Node SEA — no runtime installs.
# dev usage (any OS with Node)
node app/mcpctl.js status
# build the standalone exe (Windows; needs Node v24 + `npm i -g postject`)
node app/build.js
app/dist/mcpctl.exe statusDon't want to carry an 87 MB unsigned binary that AV heuristics may scan or
block on launch? npm run build:launcher installs a ~1 KB node launcher into the
same PATH directory instead — it requires node on PATH but re-reads the CLI
source on every run, so there is no rebuild step. The two packagings are mutually
exclusive; each build removes the other's files, because cmd.exe and Git Bash
would otherwise disagree about which one mcpctl means.
Covers the bridge end-to-end with automatic host recovery: it detects a wedged or dead host (kills it, waits for the extension to respawn it) and transparently retries mid-call failures caused by service-worker idle-kills. It also manages MCP sessions properly (init + DELETE on exit) so you never hit the bridge's single-session limit.
mcpctl status [--wait N] real bridge health (host + extension roundtrip)
mcpctl ping verify a session + tool roundtrip (latency)
mcpctl doctor full environment report (host, browser, manifest,
extension-ID match) — run when something won't start
mcpctl ensure [--wait N] bring the bridge up: wait / launch browser / restart
stale host until a real session roundtrip works
mcpctl eval "40+2" run JS in the active tab
mcpctl tabs | active windows/tabs, active tab info
mcpctl switch <tabId> switch to tab
mcpctl shot --out t.png screenshot the active tab
mcpctl read [--depth N] read page structure / interactive elements
mcpctl click "Button text" click an element
mcpctl fill "Search" "value" fill an input
mcpctl keys "Ctrl+l" keyboard shortcuts
mcpctl nav https://... navigate the active tab
mcpctl net start --bodies capture network activity
mcpctl console --errors poll console messages
mcpctl tools list the bridge's MCP tools
mcpctl call <tool> <json> call any bridge tool directly
mcpctl batch file.json run a JSON-array / JSONL batch in one session
mcpctl repl interactive REPL (!tool {json} for raw calls)Global flags: --json, --tab <id>, --port <n>, --host <h>, --timeout <sec>,
--lock-timeout <sec>; env vars MCP_PORT / MCP_HOST. Full command reference:
app/README.md or mcpctl help.
Health is measured with a real extension tool roundtrip — never a bare TCP check — so a standalone host without the extension reports
state: broken, not a false "ok". Session commands fail fast (exit3) when the bridge is down, and self-serialize via a lockfile: concurrent mcpctl invocations queue instead of colliding and triggering host restarts. The command name itself is validated before any bridge work, so a typo is always a usage error (exit2) rather than a misleading "bridge down" when no browser happens to be running.
The original single-command CLI (mcp launcher: scripts/mcp.js, mcp.cmd,
bash mcp, or npm link). Superseded by app/mcpctl but still functional;
uses the same host-recovery and session-cleanup logic. It can also be compiled
to a standalone dist/mcp.exe with node scripts/build-exe.js.
mcp status # bridge health + host PID
mcp restart # kill stuck host, wait for respawn
mcp tabs # windows + tabs
mcp active # active tab
mcp switch <tabId>
# ---- full tab management ----
mcp open 'https://a.com' 'https://b.com' # one or many new tabs
mcp open 'https://…' --bg --pin --window 3 # background/pinned/window
mcp close 12 13 # close tabs by id
mcp close url:https://x # close by exact URL
mcp close domain:github.com
mcp close others # close every tab except the active one
mcp close all # close every tab in every window
mcp close window 4 # close whole window(s)
mcp dup 12 # duplicate tab(s)
mcp reload --all --cache # reload everything, bypass cache
mcp discard 12 # suspend a tab
mcp pin 12 | mcp unpin 12
mcp mute 12 | mcp unmute 12
mcp move 12 13 --window 4 --index 0
mcp group 12 13 --name Work --color blue
mcp ungroup 12 | mcp groups
mcp search 'github' # find tabs by title/url/host
mcp content-search 'token expired' # search text inside open tabs
# ---- windows ----
mcp window new 'https://…' --incognito --w 1200 --h 800
mcp window close 4 --current --all
mcp window state 4 fullscreen
mcp window resize 4 --w 1280 --h 720
mcp window focus 4
mcp window arrange --layout grid
mcp zoom 1.5 | mcp zoom reset
# ---- cookies & downloads ----
mcp cookies list 'https://example.com'
mcp cookies set 'https://example.com' theme dark --httpOnly
mcp cookies delete 'https://example.com' theme
mcp downloads list --query report
mcp downloads cancel 12
# ---- recording & tracing ----
mcp gif start --fps 5 | mcp gif status | mcp gif stop
mcp gif start --auto # capture a frame after every tool action
mcp gif stop --out clip.gif # save the recording to a file
mcp gif stop --base64 # also print the GIF data inline
mcp trace start --reload --auto --duration 8000
mcp trace stop --name page-load | mcp trace analyze
# ---- page interaction ----
mcp read --interactive # interactive elements with ref_ ids
mcp eval 'document.title' # run JS expression in the page
mcp run 'return 1+1' # run a JS block (async body, must return)
mcp click '#button' # or ref_12
mcp fill 'input' 'value'
mcp keys 'Enter'
mcp nav 'https://…' # url | back | forward | reload (--new-tab)
mcp shot --full --out page.png
mcp tools # list bridge tools (incl. flow.* published tools)
mcp call <tool> '{"k":1}' # raw tool call
mcp storage # localStorage/sessionStorage/cookies/IndexedDB
mcp repl # interactive session (single persistent MCP session)
mcp helpFlags: --json, --tab <id>, --depth N, --ref ref_X, --out file.png.
- Regenerate icons:
node scripts/make-icons.js - Re-run host registration dry-run:
node scripts/register-host.js - Unit tests (no browser needed) —
npm testruns the first three:node scripts/test-instance.js— instance identity layer (stable id, deterministic port, labels) against a stubbedchromenode scripts/test-mcpctl-instances.js— CLI discovery,--browsertargeting and the packaging contract, against mock bridges on real socketsnode scripts/test-launcher.js— packaging self-consistency (exactly one packaging installed) plus shim,cmd.exeand exit-code behaviournode scripts/test-gif-encoder.js— GIF encoder structure + bit-exact LZW round-trip across all code-size transitions and dictionary resetsnode scripts/unit-sim-console.js— console-capture relay chain
- Live end-to-end tests (need the bridge running + extension connected):
node scripts/live-feature-test.js— baseline tools (content, JS, console, network, fill, click)node scripts/live-tabs-test.js— new tab/window toolkit: open, search, details, pin/unpin, mute/unmute, duplicate, groups, move, zoom, reload, content-search, cookies, downloads, window manage/arrange, trace, GIF. It only touches tabs/windows it creates and cleans up after itself.node scripts/probe-cdp.js— focused diagnostic for the trace + GIF CDP paths; dumps raw responses and always closes its MCP session
- Build the standalone CLI executables:
npm run build(ornode app/build.js) — producesapp/dist/mcpctl.exe(standalone CLI);npm run build:launcherinstalls the node launcher insteadnode scripts/build-exe.js --run— producesdist/mcp.exefrom the legacyscripts/mcp.jsCLI (same SEA technique;dist/is git-ignored)
- There is no build step for the extension — plain JS (MV3 service worker with
importScripts). Reload it fromchrome://extensionsafter edits.