custom-chrome-dev-mcp

custom-chrome-dev-mcp

Enables MCP clients to drive your already-open Chrome browser like a human, using 45 tools for navigation, perception, capture, and trusted input that pages receive as genuinely user-generated.

Category
Visit Server

README

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.


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.

<details> <summary>Alternative - manual JSON (only if you can't use the CLI)</summary>

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"]
    }
  }
}

</details>

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.

5. Verify all three links in the chain

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.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured