Control and inspect Chrome from Claude Code via the Model Context Protocol: capture screenshots, navigate and interact with pages, read console output, and evaluate scripts. Perfect for visual theme verification, UI regression testing, design review, and end-to-end browser automation - Claude sees and drives exactly what your users see.
┌──────────────┐ WebSocket ┌──────────────┐ stdio/MCP ┌──────────────┐
│ Chrome │─────────────▶│ MCP Server │◀────────────▶│ Claude Code │
│ Extension │ localhost │ (Node.js) │ │ │
│ (MV3, s.w.) │ :8770 └──────────────┘ └──────────────┘
| Component | Path | Purpose |
|---|---|---|
| Chrome extension (Manifest V3) | extension/ |
Captures screenshots and drives the active tab on demand |
| MCP Server | server/ |
Bridges extension → Claude Code |
| Screenshots | screenshots/ |
Saved PNGs + diff images |
cd server
npm install- Open Chrome and go to
chrome://extensions - Enable Developer mode (top-right toggle)
- Click Load unpacked
- Select the
extension/folder - The camera icon appears in your toolbar
The extension's background logic runs as a service worker, so it can be unloaded by Chrome when idle and woken back up automatically (via an alarm and on-reconnect logic) - there's nothing to keep open or pin.
Run this from the project root:
claude mcp add chrome-devtools \
--scope user \
-- node /ABSOLUTE/PATH/TO/chrome-devtools-mcp/server/index.jsReplace /ABSOLUTE/PATH/TO/ with the real path on your machine.
Or add it manually to ~/.claude.json:
{
"mcpServers": {
"chrome-devtools": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/chrome-devtools-mcp/server/index.js"],
"env": {
"CHROME_DEVTOOLS_WS_PORT": "8770"
}
}
}
}Open a new Claude Code session and run:
/mcp
You should see chrome-devtools listed with 26 tools.
| Tool | Tier | Description |
|---|---|---|
capture_screenshot |
read | Capture the visible viewport of the active tab |
capture_full_page |
read | Capture the full scrollable page (scroll-and-stitch) |
list_screenshots |
— | List all saved screenshots with timestamps |
get_screenshot |
— | Retrieve a saved screenshot by filename |
compare_screenshots |
— | Pixel-diff two screenshots, outputs a diff image |
navigate |
write | Load a URL in the active tab |
go_back / go_forward |
write | Move through tab history |
reload_page |
write | Reload the active tab, optionally bypassing cache |
get_current_url |
read | Return {url, title} of the active tab |
get_element_text |
read | Get an element's textContent |
get_element_attribute |
read | Get an element's attribute value |
query_elements |
read | Find elements matching a selector, with summary info — good for exploring a page |
click_element |
write | Click an element |
fill_input |
write | Fill an input/textarea; dispatches input/change so React/Vue notice |
fill_form |
write | Fill multiple inputs in one call, optionally clicking a submit element after |
select_option |
write | Select a <select> option by value or visible label |
set_checkbox |
write | Set a checkbox/radio's checked state |
hover_element |
write | Dispatch mouseover/mouseenter on an element |
focus_element |
write | Focus an element |
press_key |
write | Dispatch a keydown/keyup pair (e.g. Enter, Tab, Escape) |
wait_for_selector |
write | Poll until an element is visible/attached/hidden, or time out |
read_console_logs |
read | Read buffered console output from the active tab (since it last navigated) |
clear_console_logs |
read | Clear the active tab's console buffer |
get_page_errors |
read | Return uncaught errors and unhandled promise rejections since page load |
evaluate_script |
execute | Run JavaScript in the page's own MAIN-world context - full access to page globals |
navigate/go_back/go_forward/reload_page/get_current_url/capture_* and all the
interaction/inspection/console tools act on the active Chrome tab and are gated by the site
permission allowlist (see Permissions below). list_screenshots,
get_screenshot, and compare_screenshots work on locally saved files and aren't tab-scoped.
Console capture only ever runs on tabs the allowlist already grants read access to - the
extension doesn't monkey-patch console on every site you browse, only ones you've explicitly
allowed. If a page's Content-Security-Policy blocks the capture script, other tools on that
page keep working; read_console_logs will just come back empty.
Element-targeting tools accept a selector_type of css (default), xpath, or text
(case-insensitive substring match against leaf elements), plus an optional nth to pick
among multiple matches.
evaluate_script is the most powerful - and most dangerous - tool here: it runs arbitrary
JavaScript in the page's own MAIN-world context, with full access to page globals, not just
the DOM. Code is wrapped in an async function body (return produces a value, await works
directly). It requires the execute tier, which no site gets by default - see
Permissions below. Every invocation logs a warning to the extension's service
worker console and the MCP server's stderr.
Once connected, just talk naturally in Claude Code:
> Take a screenshot of the page I have open and check if the dark
> mode theme matches our design tokens.
> Capture screenshots before and after my CSS change, then compare
> them to see what shifted.
> List all the screenshots we've taken and show me the latest one.
> Compare screenshot_1721900000.png with screenshot_1721901000.png
> using a threshold of 15 - I want to catch subtle color changes.
- Claude Code calls an MCP tool (e.g.
capture_screenshot) - The MCP server sends a WebSocket message to the Chrome extension's service worker
- The extension uses
chrome.tabs.captureVisibleTab()to grab a PNG - The PNG (base64) is sent back through WebSocket → MCP server → Claude Code
- Claude Code sees the actual screenshot and can analyze it visually
For capture_full_page, the extension scrolls the page in viewport-sized
increments, capturing a PNG at each step (throttled, since Chrome itself
rate-limits captureVisibleTab), and the server stitches the slices into
one image with sharp, positioned at each slice's real (post-clamp) scroll
offset. This is a genuine full-page capture, not a viewport fallback - the
one known artifact is that fixed/sticky headers or footers will appear once
per slice, since each slice is an independent screenshot of whatever was in
the viewport at that scroll position.
For compare_screenshots, the server uses sharp to do a pixel-level diff and
generates a highlighted diff image where changed pixels appear in red.
| Env var | Default | Description |
|---|---|---|
CHROME_DEVTOOLS_WS_PORT |
8770 |
WebSocket port the extension connects to |
The extension's background.js hardcodes the matching port (ws://localhost:8770); if you
change the env var, update WS_PORT at the top of extension/background.js to match and
reload the extension.
A typical visual QA loop looks like this:
- Open your app in Chrome with the light theme → ask Claude to capture
- Toggle to dark theme → capture again
- Ask Claude to compare the two and verify contrast ratios, color tokens, etc.
- Claude reports exactly which regions changed and whether the theme looks correct
Extension shows "Disconnected"
→ Make sure the MCP server is running. Claude Code starts it automatically when
you use a tool, but you can also run cd server && node index.js manually. If it was
running and still shows disconnected, click Reconnect in the popup - Chrome may have
put the service worker to sleep and it can take a moment to reconnect on wake.
NO_ACTIVE_TAB
→ Either no tab is focused, or the focused tab is a privileged browser page
(chrome:*, chrome-extension:*, edge:*, about:*, devtools:*) that the extension
can never automate, regardless of the allowlist. Focus a regular webpage
tab and retry.
TAB_CHANGED
→ The active tab switched to a different tab between the permission check and the
action actually running (you alt-tabbed, or clicked a different tab, while a tool
call was in flight). The extension re-verifies the active tab immediately before
every action - this is a real permission check catching a real race, not a bug -
and aborts rather than risk acting on a page it never checked. Just retry once
you're back on the intended tab. (If the tab merely navigated to a different URL
rather than losing focus, you'll see PERMISSION_DENIED instead if the new page
isn't allowlisted.)
Screenshots are blank, black, or full-page capture looks off
→ Some pages block captureVisibleTab (e.g. DRM-protected video). For full-page capture,
infinite-scroll pages are capped at 40 slices, and lazy-loaded content that hasn't
rendered yet by the time a slice is captured may be missing - try capturing again after
the page has fully settled.
ELEMENT_NOT_FOUND / ELEMENT_NOT_INTERACTABLE
→ The selector matched zero elements (or nth was out of range), or it matched an
element that's disabled or has no visible layout box (display:none, zero size, etc.).
Try query_elements first to see what's actually on the page.
"Site not allowlisted" / PERMISSION_DENIED
→ Every action is denied by default. If auto-prompt is on (the default), a native
notification appears and the popup shows an inline "Allow this site?" panel - click the
toolbar icon and hit Allow. Otherwise, click ⚙ Manage Permissions and add the
site's URL pattern (e.g. http://localhost:*/*) with the tier the action needs.
RATE_LIMITED
→ More than rate_limit_per_second actions (default 20) hit the same tab within one second -
usually a runaway loop. Wait a moment and retry, or raise the limit in ⚙ Manage
Permissions → Settings.
SCRIPT_ERROR / INJECTION_BLOCKED
→ evaluate_script: SCRIPT_ERROR means your code threw - the message is the page's own
error. INJECTION_BLOCKED means the page's Content-Security-Policy stopped the
Function constructor from running; nothing can be done short of the page relaxing its CSP.
Deny by default: Claude Code can only act on a tab whose URL matches an entry in the extension's allowlist. Manage the allowlist, settings, and audit log from the popup's ⚙ Manage Permissions link, which opens the options page (Sites / Settings / Audit Log tabs).
- read -
capture_screenshot,capture_full_page,get_current_url,get_element_text,get_element_attribute,query_elements,read_console_logs,clear_console_logs,get_page_errors - write - everything in
read, plusnavigate,go_back,go_forward,reload_page,click_element,fill_input,fill_form,select_option,set_checkbox,hover_element,focus_element,press_key,wait_for_selector - execute - everything in
write, plusevaluate_script- arbitrary JS in the page's MAIN-world context. The Sites tab shows a red warning before you can grant it; it's off by default and worth granting only to sites you fully trust and control.
Patterns follow a match-pattern syntax (https://example.com/*,
*://*.example.com/*, http://localhost:*/*). <all_urls> is rejected -
it's too permissive for automation.
Settings tab: auto_prompt_new_sites (native notification + inline popup panel on first
denied attempt), log_all_actions (audit log on/off), max_console_buffer (per-tab entry
cap), rate_limit_per_second (per-tab action cap). Danger zone: clear the allowlist or
reset all extension data - both irreversible.
Audit log tab: every action attempt (allowed or denied), filterable by site/action/result, with a clear button.
This extension targets Manifest V3 (minimum_chrome_version: 116):
- The background page is a non-persistent service worker. It reconnects its WebSocket
on load, on
chrome.runtime.onStartup/onInstalled, and via achrome.alarmsheartbeat (every 60s) that also pings the socket to keep it from going idle. - DOM interaction and script evaluation use
chrome.scripting.executeScriptwith inline, self-contained functions (no persistent injected content scripts beyond the one document_start console-capture script) -world: "MAIN"is used forevaluate_scriptand the console/error patcher,world: "ISOLATED"(the default) for DOM queries/clicks/fills. - No
chrome.debuggerusage anywhere, so you'll never see Chrome's "This extension is debugging this browser" banner.
MIT