Control and inspect Firefox 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 ┌──────────────┐
│ Firefox │─────────────▶│ MCP Server │◀────────────▶│ Claude Code │
│ Extension │ localhost │ (Node.js) │ │ │
└──────────────┘ :8769 └──────────────┘ └──────────────┘
| Component | Path | Purpose |
|---|---|---|
| Firefox WebExtension | extension/ |
Captures screenshots on demand |
| MCP Server | server/ |
Bridges extension → Claude Code |
| Screenshots | screenshots/ |
Saved PNGs + diff images |
cd server
npm install- Open Firefox and go to
about:debugging#/runtime/this-firefox - Click Load Temporary Add-on…
- Select
extension/manifest.json - The 📸 icon appears in your toolbar
Run this from the project root:
claude mcp add firefox-devtools \
--scope user \
-- node /ABSOLUTE/PATH/TO/firefox-devtools-mcp/server/index.jsReplace /ABSOLUTE/PATH/TO/ with the real path on your machine.
Or add it manually to ~/.claude.json:
{
"mcpServers": {
"firefox-devtools": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/firefox-devtools-mcp/server/index.js"],
"env": {
"SCREENSHOT_WS_PORT": "8769"
}
}
}
}Open a new Claude Code session and run:
/mcp
You should see firefox-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 |
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 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 Firefox 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 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 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 Firefox extension
- The extension uses
browser.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 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 |
|---|---|---|
SCREENSHOT_WS_PORT |
8769 |
WebSocket port the extension connects to |
A typical visual QA loop looks like this:
- Open your app in Firefox 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.
NO_ACTIVE_TAB
→ Either no tab is focused, or the focused tab is a privileged browser page
(about:*, moz-extension:*, chrome:*, resource:*) 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 or black
→ Some pages block captureVisibleTab (e.g. DRM-protected video). Try a
different tab.
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
script-evaluation bridge from loading; 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 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 Firefox 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.
MIT