Skip to main content
Glama
HabaAndrei

custom-chrome-dev-mcp

by HabaAndrei

Custom Chrome Dev MCP

A local-only MCP (Model Context Protocol) server that lets an MCP client - Claude Code, or anything else that speaks MCP - drive your real Chrome browser the way a person would. No telemetry, no third-party services, no cloud: everything runs on your machine behind a shared token.

It exposes 45 tools across navigation, tabs, perception, interaction, trusted input, observability, and capture.


Where this came from

This project is inspired by Chrome's official browser MCP - the Chrome DevTools MCP server published by the Chrome DevTools team, which first made the case that an AI agent should drive a browser through the DevTools Protocol rather than through scraped HTML.

We are replicating and imitating that idea, not shipping it. What we borrowed:

  • The premise - expose the browser to an agent as a set of MCP tools.

  • Accessibility-first perception - hand the model a compact a11y outline with stable element refs instead of a wall of raw HTML.

  • The Chrome DevTools Protocol as the input layer - real, trusted events instead of synthetic ones a page can spot and ignore.

Where this project deliberately diverges:

Chrome DevTools MCP

Custom Chrome Dev MCP

Browser

By default launches its own Chrome with a dedicated user-data-dir; can also attach to a running instance via --browser-url

Only ever drives the Chrome you already have open

Attachment

Connects to the browser over the DevTools Protocol endpoint

A Chrome extension living inside the browser, pointed at whatever tab you choose

Primary goal

Debugging, inspecting, and profiling a page

Behaving like a human using that page

That last row is the whole point of this repo. Chrome DevTools MCP is a debugging tool that happens to drive a browser; this is an imitation-of-a-person tool that happens to be useful for debugging.

⚠️ Not affiliated with, endorsed by, or supported by Google or the Chrome team. This is an independent reimplementation built to learn from and imitate their design. Use the official server if you want the supported thing.


Related MCP server: monkeysee

It deliberately keeps the styling of a human

Most browser automation is trivially detectable: synthetic events with isTrusted=false, focus that never really moves, text that appears in a field all at once, a pristine automation profile with no history. Every one of those is a signal.

This project tries to remove those signals:

  • Your real profile. Actions run in the Chrome you already use - your cookies, logins, extensions, and history. Nothing to fingerprint as "fresh automation".

  • Trusted input. realClick, realType, press, hover, and drag dispatch through the DevTools Protocol, so the page receives events with isTrusted=true - the same flag a physical mouse and keyboard produce.

  • Genuine focus. Clicking to focus a field really moves focus, in order, rather than assigning .value behind the page's back.

  • Real keystrokes. press emits proper rawKeyDown / char / keyUp sequences with correct key codes and modifiers, not a single synthetic input event.

  • Read-back verification. fill confirms the field actually holds the text, so the agent notices when a page silently rejected the input - as a person would.

The goal: a page should behave for the agent exactly as it behaves for someone sitting at the keyboard.

Fast synthetic tools (click, type) are still there - they're quicker and work on most sites. When a page ignores them, reach for the trusted equivalents.


How it works

One transport. The MCP client talks to the server over stdio; the server relays to a Chrome extension over a local WebSocket owned by a small long-lived hub process.

MCP client 1 (Claude) <-stdio-> bin/custom-chrome-dev-mcp.js ─┐
MCP client 2 (Claude) <-stdio-> bin/custom-chrome-dev-mcp.js ─┼─ src/hub.js (127.0.0.1:9876)
MCP client N (Claude) <-stdio-> bin/custom-chrome-dev-mcp.js ─┘              │
                                                                            │ WebSocket
                                                                            ▼
                                                              Chrome extension -> active tab

Why a separate hub process. Only one process can own port 9876, but you may have several Claude sessions open and all of them may want the browser. So the socket lives in src/hub.js rather than inside any one session. Each session connects to the hub as role:"mcp", the extension connects as role:"extension", and the hub multiplexes between them. The first session to start spawns the hub detached, so it outlives that session; later sessions find it already listening.

Inside the extension there are three layers:

  1. Walker (page/walker.js) - injected into the page's ISOLATED world. Owns element resolution, the stable eN ref map, and the fast synthetic DOM ops.

  2. CDP (cdp/) - chrome.debugger for trusted input, page-context evaluate, full-page screenshots, and the console/network buffers.

  3. Recording (recording/) - CDP screencast frames encoded to .webm by a MediaRecorder in an offscreen document.

🔒 The extension authenticates to the hub with a shared token (AUTH_TOKEN, identical in src/config.js and extension/src/config.js). The hub drops any peer that presents a different value.


Prerequisites

Requirement

Check

Node.js

18 or newer (developed on 22)

node --version

Chrome

Google Chrome or Chromium, any recent version

chrome://version

An MCP client

Claude Code, or anything else that speaks MCP over stdio

claude --version

No global installs, no build step, no service to sign up for. Two runtime dependencies (@modelcontextprotocol/sdk and ws) and everything stays on 127.0.0.1.


Local setup

Four steps, then a verification pass. Budget five minutes.

1. Clone and install

git clone <your-fork-url> custom-chrome-dev-mcp
cd custom-chrome-dev-mcp
npm install

Confirm the tree is healthy before wiring anything to Chrome - the offline lane needs no browser and takes under a second:

npm test

You want 22 passed. If that fails, fix it before continuing; nothing downstream will work.

2. Load the extension into Chrome

  1. Open chrome://extensions.

  2. Turn on Developer mode (top-right toggle).

  3. Click Load unpacked and select the extension/ folder - the folder itself, not manifest.json inside it.

  4. Custom-chrome-dev-mcp appears in the list.

⚠️ Load it into the Chrome profile you actually browse in. Chrome keeps extensions per profile, so an extension loaded into "Profile 4" is invisible to the window running under "Default". If tools later report no tabs, or the hub never logs extension connected, this is the first thing to check. chrome://version shows the active Profile Path.

The extension ID is pinned by the public key in extension/manifest.json, so it is identical on every machine - nothing to copy between setups.

3. Register the MCP server with your client

Use the CLI - substitute the absolute path where you cloned the repo (pwd in the project root prints it):

claude mcp add -s user custom-chrome-dev-mcp -- node /ABSOLUTE/PATH/TO/custom-chrome-dev-mcp/bin/custom-chrome-dev-mcp.js
  • -s user registers it for all your projects; -s local limits it to this one.

  • Register bin/custom-chrome-dev-mcp.js - that file is the entry point. Pointing at src/server.js will not work.

  • The path must be absolute. A relative path resolves against whatever directory the client happened to launch from.

  • Confirm with claude mcp list - you want a ✔ Connected next to it.

⚠️ Do not hand-edit ~/.claude.json. It is large, and one misplaced comma breaks Claude Code entirely. The command above edits it safely.

Add the server under mcpServers:

{
  "mcpServers": {
    "custom-chrome-dev-mcp": {
      "command": "node",
      "args": ["/ABSOLUTE/PATH/TO/custom-chrome-dev-mcp/bin/custom-chrome-dev-mcp.js"]
    }
  }
}

4. Restart the MCP client

MCP clients enumerate tools once, at startup - a server registered mid-session is invisible until you restart. Restart Claude, and the 45 tools appear.

On restart the client launches the server, which spawns src/hub.js if nothing is already listening on 127.0.0.1:9876.

The stack is client → server → hub → extension → tab. Check it end to end rather than guessing which link is down.

# The hub is up, and the extension found it:
tail -f "$TMPDIR/custom-chrome-dev-mcp-hub.log"
#   [hub] listening on 127.0.0.1:9876
#   [hub] extension connected      <- this line is the handshake succeeding

# Who owns the port (should be src/hub.js from THIS repo):
lsof -nP -iTCP:9876 -sTCP:LISTEN

Then ask your client for listTabs. A JSON array of your open tabs means every link works. Follow it with screenshot - a PNG lands in ~/Downloads and comes back inline.

For the extension's own console: chrome://extensionsCustom-chrome-dev-mcpservice workerInspect. That is where extension-side errors surface; they never reach the MCP client.

6. Change the shared token before real use

AUTH_TOKEN ships with a default value, defined identically in src/config.js and extension/src/config.js. It is the only thing stopping another process on your machine from driving your logged-in browser. Pick your own value, change it in both files (an offline test asserts they match), and reload the extension.


After you change code

The two halves reload differently, and getting this wrong wastes more time than anything else in the project:

You edited

To pick it up

Anything under extension/

Click reload ↻ on the extension in chrome://extensions. Chrome keeps running the previously loaded build until you do.

Anything under src/

Restart the MCP client. The server process is long-lived and holds the old tool schemas.

src/hub.js

pkill -f src/hub.js - the next tool call respawns it.


Troubleshooting

Symptom

Cause

Fix

claude mcp list shows ✘ Failed to connect

Wrong path, or not the bin/ entry point

Re-register with the absolute path to bin/custom-chrome-dev-mcp.js

Tools missing from the client entirely

Registered mid-session

Restart the MCP client

Hub log never says extension connected

Extension not loaded, loaded in a different Chrome profile, or AUTH_TOKEN differs between the two config.js files

Check chrome://version → Profile Path; confirm both tokens match

A tool call hangs, then times out

The service worker died, or an extension-side exception

Open the service worker console; click reload ↻

Port 9876 owned by an unexpected process

A hub from another clone of this project is squatting the port

lsof -nP -iTCP:9876 -sTCP:LISTEN, then kill that PID

An edit to extension/ "did nothing"

Chrome is still running the old build

Click reload ↻

URL is banlisted

BANLIST in extension/src/config.js blocks that host

Edit the list - it ships with placeholder entries

refusing to act: … does not contain expectUrl

The expectUrl guard fired, correctly

Drop the guard, or point it at the real URL

Screenshot path rejected

Writes are confined to the capture directory

Use a filename or a path inside it

A tool targets the wrong tab

A background tab stole focus

Pin the working tab with useTab


Configuration

Both are optional environment variables read at startup by src/config.js.

Variable

Default

What it does

CUSTOM_CHROME_DEV_MCP_CAPTURE_DIR

~/Downloads

The only directory screenshots and recordings may be written to.

CUSTOM_CHROME_DEV_MCP_WS_PORT

9876

Hub port. Change it in extension/src/config.js too, or they won't find each other.

Also worth changing for real use: AUTH_TOKEN, defined identically in src/config.js and extension/src/config.js. Pick your own value - it is what stops another local process from driving your browser.


Available tools (45)

Elements are targeted three ways: selector (CSS), ref (a stable eN id from snapshotA11y), or name (accessible name, e.g. a button's label). "target" below means any one of those three.

Universal params, accepted by every tool:

  • tabId - act on a specific tab instead of the ambient active one.

  • frameId (from listFrames) - act inside a specific frame, including cross-origin iframes the top document cannot script.

  • expectUrl - a guard: refuse the action unless the tab's URL contains this substring.

Pin a working tab for the whole session with useTab so a background tab (an autoplaying video, a notification popup) can't steal focus and misdirect an action.

Navigation

Tool

Args

Description

navigate

url

Point the tab at a URL (replaces the page).

newtab

url

Open a URL in a new foreground tab, leaving the current page intact.

back / forward

-

History back / forward.

reload

hard?

Reload, optionally bypassing the cache.

getUrl / getTitle

-

The tab's URL / title (works on internal pages too).

waitForLoad

timeout?

Block until the tab finishes loading.

Tabs & frames

Tool

Args

Description

listTabs

-

Every open tab across all windows (id, title, url, active, pinned).

activateTab

tabId

Focus a tab and its window.

closeTab

tabId

Close a tab by id.

useTab

tabId?

Pin the working tab so every later tool targets it regardless of OS focus. Omit tabId to pin the current one.

unpinTab

-

Release the pin; tools revert to the active tab.

listFrames

-

Every frame incl. cross-origin as {frameId, parentFrameId, url, origin}.

Perception

Tool

Args

Description

snapshotA11y

-

Compact accessibility outline of visible interactive elements as role "name" ref=eN. Prefer this over snapshot. Refs expire on navigation or re-snapshot.

snapshot

-

Raw outerHTML of <body>, truncated to 50k. Use when you need exact markup.

getText

target

innerText of one element, trimmed.

getAttribute

target, attr

An attribute, falling back to the live DOM property (value, checked, href).

queryAll

selector, limit?

text/href/value/visible for every match at once.

viewport

-

devicePixelRatio, CSS viewport, scroll offset - how you map screenshot px → CSS px.

Interaction - synthetic, fast

Untrusted events dispatched by the walker. Quick, and enough for most sites.

Tool

Args

Description

click

target

Bubbling MouseEvent click; also focuses the element; .click() fallback. Returns {focused}.

type

target, text

Set a field's value via the native setter (handles <input>, <textarea>, and contenteditable). Returns {value}.

fill

target, text, verify?

Focus + set + read back. Throws if the text didn't stick. The reliable text-entry path - prefer it over click-then-type.

assert

target, text?, value?

Verify text (substring) and/or exact value without a screenshot → {ok, checks}.

scroll

target?, direction?, amount?

Scroll an element into view, or the window (top/bottom jump to extremes).

select

target, value? / label?

Choose a <select> option by value or visible label.

check

target, checked

Set a checkbox/radio, clicking only if it isn't already there.

submit

target

requestSubmit() the owning form - for forms with no clickable button.

waitForSelector

target or text, timeout?

Poll until an element resolves or a text substring appears.

Trusted input & emulation - CDP

Real events with isTrusted=true. These attach chrome.debugger, which shows a persistent yellow "being debugged" banner on the tab.

Tool

Args

Description

realClick

target / x,y, button?, clickCount?

Trusted click, including right-click and double-click.

realType

target?, text

Trusted text insertion, focusing the target first if given.

press

keys, target?

Trusted keys and combos: "Enter", "Tab", "Meta+c", ["ArrowDown","Enter"].

hover

target / x,y

Move the real mouse over an element to fire :hover (reveals menus and tooltips).

drag

from, to

Trusted press-move-release drag & drop.

uploadFile

selector, paths[]

Set files on an <input type=file>, bypassing the OS picker. Absolute paths.

setViewport

width, height, deviceScaleFactor?, mobile?, userAgent?

Emulate a viewport / device for responsive checks.

handleDialog

accept?, promptText?

Pre-arm an answer for the next alert/confirm/prompt. Set it before the action that triggers the dialog.

detach

-

Detach the debugger and clear the banner. Re-attaches on the next CDP call.

Observability - CDP, buffered per tab

Capture starts when the debugger attaches, so reload the page after the first CDP call if you want load-time activity.

Tool

Args

Description

getConsole

level?, limit?, clear?

Buffered console logs, warnings, errors, and uncaught exceptions.

listNetworkRequests

urlContains?, status?, failedOnly?, limit?

Buffered requests: method, url, status, type, timing.

getNetworkRequest

requestId, includeBody?

One request in full; includeBody also fetches the (truncated) response body.

evaluate

expression

Run JS in the page's real context via CDP - bypasses the content-script CSP that blocks eval. Awaits promises. Not available on chrome:// pages.

Capture

Saved into the capture directory (~/Downloads by default - see Configuration).

Tool

Args

Description

screenshot

path?, format?, tabId?

Visible viewport as PNG/JPEG - saved to disk and returned inline with {devicePixelRatio, cssViewport}, so the model sees it in one call.

fullPageScreenshot

path?, tabId?

The entire scrollable page beyond the viewport, via CDP.

record

action, path?, tabId?

start / stop / status tab recording → .webm. Fully MCP-driven - no toolbar click or user gesture needed. Records the tab, not the desktop.

path is a filename or a path inside the capture directory. Missing subfolders are created; anything resolving outside the directory is refused.


A first real run

Setup step 5 proves the wiring. This proves the interesting part - that a page sees a person rather than a script. Point your client at any page and ask for:

  1. snapshotA11y - the compact outline, with eN refs to target.

  2. realClick {ref:"e3"} - a trusted click. The tab grows a yellow "being debugged" banner; that is the CDP attach, and it is meant to be visible.

  3. evaluate {expression:"'ok'"} - page-context JS, bypassing the content-script CSP.

  4. screenshot - a PNG in your capture directory and returned inline.

  5. record {action:"start"}record {action:"stop", path:"clip.webm"} - a .webm of the tab. No toolbar click and no user gesture needed; the toolbar icon is inert by design and starts nothing.

  6. detach - clears the banner.

To see the difference the trusted path makes, install a listener and compare:

// via evaluate
window.__e = []; document.querySelector("button")
  .addEventListener("click", e => window.__e.push(e.isTrusted));

click reports false; realClick reports true. That contrast is the whole point of the project, and the browser test lane asserts on it directly.


Running the tests

The suite has two lanes, and the split is the point.

Offline lane - no browser, runs in CI

npm test        # node test/run.mjs --lane=offline

Completes in well under a second and needs nothing but Node. It runs a real MCP handshake in-process against src/server.js (via the SDK's in-memory transport), so it asserts on the surface the server actually publishes:

  • every published tool has an extension handler, and vice versa - the failure the mirrored architecture invites

  • no tool name is claimed by two handler groups (they merge by spread, so a duplicate would silently lose)

  • every tool carries a real description and the universal tabId/frameId/expectUrl scope

  • every tool is exercised by at least one test - add a tool without a test and CI fails, no browser required

  • the capture path allowlist really refuses .., deep .., absolute paths, and symlinked escapes, tested against the real resolver

  • the ban list is checked by behaviour - it blocks what it claims to and doesn't over-block ordinary sites

  • the hub binds loopback only, the tokens match on both sides, the manifest requests no over-broad permissions, the toolbar icon is inert, and no *.pem is committed

Browser lane - drives real Chrome

# 1. Disconnect the MCP client (close Claude Code, or disable this server for the run)
# 2. Free port 9876 - the hub is long-lived and outlives the session that spawned it
pkill -f src/hub.js
# 3. Start the suite; it binds 9876 itself and waits for the extension
npm run test:browser
# 4. Reload the extension in chrome://extensions so it connects to the suite

⚠️ Step 1 is not optional. A connected MCP client respawns the hub every ~1.2s whenever it finds the socket gone, so it takes port 9876 straight back and the suite dies with EADDRINUSE. Killing the hub while a client is still attached does not help - the client just starts another one.

The suite stands up a fixture server and a bridge speaking the same wire protocol as the real hub, so a passing run exercises the actual message contract. Each suite mirrors a tool group, and every test starts from a reset fixture page - no test inherits another's mutations.

At the end it prints tool coverage and fails if any of the 45 tools went unexercised.

Options

Command

Effect

npm test

offline lane only - the CI gate

npm run test:browser

browser lane only

npm run test:all

both

npm run test:list

list every suite and test without running

node test/run.mjs --grep=fill

only tests whose suite/name matches

Test layout

test/
├── run.mjs                    # CLI: lanes, filtering, coverage, reporting
├── lib/
│   ├── runner.js              # suite registry, isolation, timeouts
│   ├── assert.js              # assertions with diagnostic messages
│   ├── wait.js                # eventually() - polling, not fixed sleeps
│   ├── mcp-probe.js           # real in-process MCP handshake
│   ├── bridge.js              # stands in for the hub; tracks tool coverage
│   ├── fixture-server.js      # serves the fixture pages
│   └── page.js                # the browser session + per-test reset
├── fixtures/
│   ├── index.html             # the fixture page (a real file, with __reset())
│   └── frame.html             # child frame, for frameId targeting
└── suites/
    ├── 01-contract.suite.js   # offline
    ├── 02-security.suite.js   # offline
    ├── 10-navigation.suite.js
    ├── 20-tabs.suite.js
    ├── 30-perception.suite.js
    ├── 40-interaction.suite.js
    ├── 50-trusted-input.suite.js
    ├── 60-observability.suite.js
    └── 70-capture.suite.js

Security notes

This extension can drive your logged-in browser. Read this section.

  • Loopback only. The hub binds 127.0.0.1, so it is not reachable from the LAN - only from processes on this machine.

  • Token handshake. A peer must present AUTH_TOKEN on connect or the hub drops it. Change it from the shipped default (identical constant in src/config.js and extension/src/config.js) - it is what stops another local process from driving your browser.

  • File writes are confined to the capture directory. src/capture/capture-path.js resolves every requested path and refuses anything outside it, including via .. traversal and via symlinked subdirectories. This matters more than it looks: arbitrary-path writes are effectively code execution.

  • Host ban list. BANLIST in extension/src/config.js blocks navigation and scripting on sensitive domains (banking, PayPal, Gmail). Adjust it to your needs. Note: screenshots and recording capture rendered pixels and are not filtered by the ban list.

  • The debugger banner is a feature. CDP tools attach chrome.debugger, showing a persistent yellow "being debugged" bar. That is your visible signal that something is driving the tab. detach removes it.

  • evaluate runs arbitrary JS in the page's real context.

  • Internal pages are off limits - the extension cannot script chrome:// or chrome-extension:// URLs.

  • The signing key is not in this repo. The extension ID is pinned by the public key in extension/manifest.json; the matching private key must stay outside version control (.gitignore blocks *.pem). It is only needed to re-pack a .crx under the same ID - loading unpacked does not use it.


Architecture

The server and the extension are mirrored. Every tool group in src/tools/ has a handler file of the same name in extension/src/handlers/. Adding a tool means touching exactly that pair - its schema and docs on one side, its implementation on the other.

Group

Server (schema + docs)

Extension (implementation)

navigation

src/tools/navigation.js

extension/src/handlers/navigation.js

tabs

src/tools/tabs.js

extension/src/handlers/tabs.js

perception

src/tools/perception.js

extension/src/handlers/perception.js

interaction

src/tools/interaction.js

extension/src/handlers/interaction.js

trusted input

src/tools/trusted-input.js

extension/src/handlers/trusted-input.js

observability

src/tools/observability.js

extension/src/handlers/observability.js

capture

src/tools/capture.js

extension/src/handlers/capture.js

Everything else is supporting infrastructure:

  • bin/custom-chrome-dev-mcp.js - the executable you register with your MCP client. It does nothing but start the server.

  • src/config.js / extension/src/config.js - every tunable, one file per side. AUTH_TOKEN and the port must match across the two.

  • src/relay/hub-client.js - connects to the hub as role:"mcp", spawns it when absent, and turns each tool call into a request/response over the socket.

  • src/hub.js - the long-lived relay owning ws://127.0.0.1:9876. Holds the one extension socket plus every session's client and multiplexes between them. Re-tags ids on the wire (they can collide across sessions) and self-exits if a hub already owns the port.

  • src/capture/capture-path.js - the write allowlist. Every capture path goes through it.

  • extension/src/connection.js - the hub socket plus the heartbeat. An MV3 service worker is torn down after ~30s idle, which silently drops the socket; a sub-30s heartbeat keeps both alive, and an alarm revives the worker after a hard kill.

  • extension/src/tabs.js - which tab a call acts on (explicit tabId > pinned tab > active tab), the expectUrl guard, and the ban list check.

  • extension/src/walker-bridge.js + extension/src/page/walker.js - the injected ISOLATED-world script with the stable element-ref system, and the only module that knows how to reach it.

  • extension/src/cdp/ - session.js (attach/detach, cdp(), element centres), keyboard.js (key names → CDP key events), dialogs.js (native dialog policy), buffers.js (console + network ring buffers, capped at 500/tab).

  • extension/src/recording/ - chrome.tabCapture needs a user gesture an MCP call never has, so recording uses CDP screencast instead: JPEG frames relayed to an offscreen MediaRecorder (the service worker has no DOM).

  • test/ - two-lane suite: an offline CI gate that needs no browser, and a browser lane that drives real Chrome. See Running the tests.


Project layout

.
├── bin/
│   └── custom-chrome-dev-mcp.js   # executable entry - register THIS with your client
├── src/
│   ├── server.js                  # composes config + relay + tool registry
│   ├── config.js                  # port, token, capture dir, timeouts
│   ├── hub.js                     # long-lived relay owning :9876
│   ├── relay/
│   │   └── hub-client.js          # session -> hub socket; call()
│   ├── capture/
│   │   └── capture-path.js        # write allowlist for screenshots/recordings
│   └── tools/                     # ONE FILE PER TOOL GROUP - the public surface
│       ├── index.js               # the registry
│       ├── schemas.js             # shared arg shapes + passthrough helper
│       ├── navigation.js
│       ├── tabs.js
│       ├── perception.js
│       ├── interaction.js
│       ├── trusted-input.js
│       ├── observability.js
│       └── capture.js
├── extension/                     # Chrome MV3 extension
│   ├── manifest.json
│   └── src/
│       ├── background.js          # service worker entry - wiring only
│       ├── config.js              # token, banlist, buffer caps, asset paths
│       ├── connection.js          # hub socket + MV3 keepalive heartbeat
│       ├── tabs.js                # tab resolution, pinning, ban check
│       ├── walker-bridge.js       # channel to the injected page script
│       ├── cdp/
│       │   ├── session.js         # attach/detach, cdp(), element centres
│       │   ├── keyboard.js        # key names -> CDP key events
│       │   ├── dialogs.js         # native alert/confirm/prompt policy
│       │   └── buffers.js         # console + network ring buffers
│       ├── recording/
│       │   ├── recorder.js        # CDP screencast -> offscreen encoder
│       │   ├── offscreen.html
│       │   └── offscreen.js       # MediaRecorder host
│       ├── page/
│       │   └── walker.js          # injected DOM driver (ISOLATED world)
│       └── handlers/              # MIRRORS src/tools/ - one file per group
│           ├── index.js           # the handler table + dispatch
│           ├── navigation.js
│           ├── tabs.js
│           ├── perception.js
│           ├── interaction.js
│           ├── trusted-input.js
│           ├── observability.js
│           └── capture.js
└── test/                          # two lanes: offline (CI) + browser
    ├── run.mjs                    # CLI entry
    ├── lib/                       # runner, assertions, bridge, fixtures, session
    ├── fixtures/                  # the fixture pages, as real files
    └── suites/                    # one suite per tool group

Credits

Inspired by Chrome DevTools MCP from the Chrome DevTools team. Independent reimplementation, not affiliated with, endorsed by, or supported by Google.


License

MIT. Copyright (c) 2026 Haba Andrei.

Use it, fork it, ship it. The only condition is that the copyright notice and the permission notice travel with any substantial copy.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP clients to drive a real, logged-in Chrome browser for web automation tasks like navigation, clicking, typing, and screenshotting.
    1
    1
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Drive your real, signed-in Chrome browser from any MCP client, enabling browser automation such as navigation, clicking, typing, and screenshots through standard MCP tools.
    1
  • A
    license
    C
    quality
    A
    maintenance
    MCP server for browser automation that drives Chrome via an extension, preserving login state and offering 45 tools for navigation, interaction, scraping, and screenshots.
    53
    4
    MIT

View all related MCP servers

Related MCP Connectors

  • Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.

  • Hosted real Google Chrome MCP with per-user persistent state. Navigate, click, type, screenshot.

  • Stealth web browser for agents: search, fetch, click, download and type in persistent MCP sessions.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HabaAndrei/custom-chrome-dev-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server