marionette-mcp
This MCP server drives a Firefox instance (via its native Marionette wire protocol over loopback TCP) so an AI agent can inspect and act on real web pages.
Navigate & inspect:
fx_navigateopens a URL,fx_pagereturns the current URL/title, andfx_snapshotproduces a numbered map of interactive elements (refs) to act on.Interact with the DOM:
fx_click,fx_type(clears first unlesskeep:true),fx_select(by value or label),fx_toggle(checkbox/radio state), andfx_upload(file input, path restricted toFX_MCP_FILE_ROOTS) — targeting elements byrefor CSSselector.Run scripts & wait:
fx_evalexecutes JS in the page (function body, may return a value) andfx_waitblocks until visible text or a CSS selector appears (default 10s, max 30s).Capture output:
fx_screenshotwrites a PNG to an allowed path under the configured file roots.Windows & dialogs:
fx_windowslists windows (focused one marked),fx_windowswitches by handle, andfx_alert_state/fx_alert_accept/fx_alert_dismissread and resolve native dialogs.Session info:
fx_statusreports connection, session, current page, and thenavigator.webdriverflag;fx_cookieslists current-origin cookie names/domains only.Constraints by design: loopback-only connectivity, file access rooted to allowed directories, and native OS dialogs/CAPTCHAs are not automatable — the model is human-in-the-loop, not headless stealth.
Note: the README describes a larger toolset (29 tools, including
fx_launch,fx_connect,fx_shutdown,fx_form,fx_field,fx_answer,fx_scroll,fx_gates,fx_links,fx_extract,fx_search), but the provided schema exposes only the 18 tools listed above.
Enables control of a Firefox browser via the native Marionette protocol, providing tools for navigating, snapshotting interactive elements, clicking, typing, selecting, toggling checkboxes, uploading files, running JavaScript, waiting for conditions, taking screenshots, managing windows, and handling native dialogs.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@marionette-mcp Go to example.com, snapshot the page, then click Sign in"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
firefox-mcp-marionette
Zero-dependency Model Context Protocol server that drives a Firefox with Marionette enabled — the instance you launched with --marionette (the MCP asks before attaching to a running one), or a dedicated instance the MCP starts for you on request — via its native wire protocol.
AI agents get a precise DOM actuator: snapshot the interactive elements of a page, then click, type, select, toggle checkboxes, upload files, wait for conditions, run JS, and screenshot — all against the browser session you control (your profile, your logins, your kill switch).
Why not Playwright/CDP?
Playwright's browser-automation channels (CDP, extension mode) target Chromium, or force the library to launch and own a pinned browser build. Marionette is different:
You own the browser. The server connects over loopback TCP (default port 2828) to the Firefox you launched, in whatever profile you chose — but never silently: on the first browser call of a session it probes the endpoint, and if a running Firefox answers, the tool asks you whether to attach to it, launch a new dedicated instance, or do something else (nothing attaches on its own). If nothing is reachable, it asks instead of failing — and if you choose it,
fx_launchstarts a fresh, dedicated, loopback-only instance with a clean profile. Nothing in this repo downloads or upgrades a browser.Zero dependencies. No
npm installof runtime deps, no browser downloads, no CDP shim. One Node runtime (>= 20) plus your existing Firefox.Native protocol. Frames are length-prefixed JSON over TCP — the same protocol Selenium's Firefox driver speaks. No protocol translation, no version drift.
Related MCP server: Foxhand
Install
Prerequisites: Node.js >= 20 and Firefox — that's all; there are no runtime dependencies and nothing ever downloads or upgrades a browser. And the mental model: the server runs as the child process of your MCP client over stdio — the client spawns it per session, so there is no daemon to install, start, or stop yourself.
Node MCPs are usually distributed through the npm registry, run via npx from the client config (the same pattern as @playwright/mcp and the official @modelcontextprotocol/server-* packages). This server is published that way: firefox-mcp-marionette — the whole install is one command, no git needed (npx ships with Node). Three ways to get the code on your machine:
npm (recommended).
npx -y firefox-mcp-marionette— one-shot run straight from the registry (that is all step 1 of the Quick start is); ornpm i -g firefox-mcp-marionetteto keep it installed.Source zip — no git, no npm. Download the archive in a browser: main.zip, unzip it, and point your client config at the extracted
src/server.mjsinstead.git clone (development/contributing).
git clone https://github.com/bornmw/firefox-mcp-marionette;npm linkoptionally puts the bin on PATH.
Copying the repo around? The minimal footprint is the whole src/ directory (4 files: server.mjs, marionette.mjs, protocol.mjs, evalwrap.mjs) — they import each other by relative path, so a single server.mjs alone will not work. scripts/ (smoke tests) and test/ are optional; nothing in src/ imports them.
Quick start
The whole setup is: register the server in your MCP client → have a Firefox to attach to (or let the MCP start one) → use the fx_* tools.
1. Register the server with your MCP client (it spawns the server process for you, per session):
opencode (opencode.json):
{
"mcp": {
"marionette": {
"type": "local",
"command": ["npx", "-y", "firefox-mcp-marionette"],
"environment": {
"FX_MARIONETTE_PORT": "2828",
"FX_MCP_FILE_ROOTS": "/tmp,/your/projects"
},
"enabled": true
}
}
}Pin a version with "command": ["npx", "-y", "firefox-mcp-marionette@0.8.0"]; developing from a local checkout instead? Use "command": ["node", "/absolute/path/to/firefox-mcp-marionette/src/server.mjs"] — the same environment block works for both.
2. Have a Firefox listening on port 2828 — or don't. Either launch one yourself:
firefox --marionette # dedicated profile recommended; default port 2828 matchesor start your agent session — on the first browser call the MCP probes the endpoint non-invasively (no session is opened by the probe): a reachable Firefox → you're asked whether to attach to it, launch a new dedicated instance instead, or do something else; nothing reachable → the same kind of question (a new instance via fx_launch, connect to one you provide, or your own direction). FX_MCP_AUTO_LAUNCH=1 skips the question and auto-launches when nothing is listening.
3. Smoke test (optional):
npm run e2e:liveThe fx_* tools are now available in-session.
First trigger in a session: the MCP probes, then asks
The stdio server lives with one automation session (one opencode session). On the first browser call it probes the configured endpoint (FX_MARIONETTE_HOST:PORT) with a short, non-invasive connection: open TCP, expect the Marionette hello frame, close the socket — the probe never opens a session, so it cannot count as the browser's active client and cannot displace another client. Then:
A Firefox answers → nothing attaches. Every browser tool returns a structured
browser-detecteddecision, and the agent asks you:Attach to the detected browser —
fx_connect {host, port}. This commits that endpoint for the session; later reconnects (e.g. after a browser restart) are silent.Start a new dedicated instance instead —
fx_launch: the server picks a free port, writes that port into a fresh profile'suser.jsprefs (user_pref("marionette.port", N)+marionette.enabled— there is no--marionette-portCLI flag), runsfirefox --marionette --no-remote -profile <dir>, and attaches to it. The detected instance keeps running, untouched. The new instance starts empty (no cookies/logins). Stop it later withfx_shutdown.Something else — your direction (point at a different endpoint, reconfigure, stop that browser first, …).
Nothing reachable → instead of a raw
ECONNREFUSED, every browser tool returns a structuredneed_bootstrapdecision with three options, and the agent asks you:Start a new dedicated instance —
fx_launch(as above).Connect to an already-running instance —
fx_connect {host, port}with details you provide.Something else — your direction (launch it yourself, reconfigure the endpoint, …).
Something answers but drops the handshake →
busy-other-client: a Marionette browser there likely already holds another active client (Marionette serves ONE client per browser). The agent asks how to proceed — free the other client, point elsewhere, or launch new.
Every decision payload carries an explicit instruction field: the browser choice belongs to you, not the agent — the agent must present the question and options to you and wait for your explicit choice before calling any listed tool (never launch/attach on its own judgment).
Once an endpoint is committed (you chose fx_connect for a detected browser, fx_launch started an instance, or FX_MCP_AUTO_LAUNCH=1 booted one when nothing was listening), later reconnects within the session are silent — the question is asked at most once per endpoint per session. fx_status never opens a session itself: with no committed endpoint it only probes and reports (connected, endpoint vs configured, the probe result, and the matching options — never an error); it attaches and reports live state only for committed endpoints. It also reports launched/launchedCurrent when this server started the browser. FX_MCP_AUTO_LAUNCH=1 auto-boots option 1 transparently in the nothing-reachable case (the tool runs in the same call, result tagged auto_started) and never auto-attaches to a detected browser.
Matching the port (attach only when you choose it; launch on request)
The server attaches over loopback TCP to a firefox --marionette instance on FX_MARIONETTE_HOST:PORT (you launched it, or fx_launch started it for this session). So the browser's Marionette port must equal the endpoint of the MCP:
Default port 2828 (both sides) → just
firefox --marionette. No extra config needed.Custom port → there is no
--marionette-portCLI flag. The port is always set through the profile'suser.jsprefs. Letfx_launch {port: 2829}do the whole procedure (fresh profile dir + prefs + launch + attach), or do it by hand:PROFILE=~/.mozilla/firefox/mcp-2829 # any dir mkdir -p "$PROFILE" printf 'user_pref("marionette.enabled", true);\nuser_pref("marionette.port", 2829);\n' > "$PROFILE/user.js" firefox --marionette --no-remote -profile "$PROFILE"Then point the MCP at it via
FX_MARIONETTE_PORT(opencode config) — or, if the MCP is already running, re-point it at runtime withfx_connect {host, port}(no restart needed).Verify / diagnose:
fx_statusreports both the activeendpointand theconfiguredendpoint. If it can't connect, the error names the port it tried and how to launch Firefox there (e.g.ECONNREFUSED 127.0.0.1:2829→ nothing listening; launch as above). Remember Marionette serves one active client per browser — don't leave another firefox-mcp-marionette (or another automation) attached to the same instance.
Typical flow:
fx_navigateto the pagefx_snapshot→ numbered map of interactive elements (refs)fx_click/fx_type/fx_select/fx_toggle/fx_uploadbyref(or CSSselector)Forms:
fx_form→ field map (index/label/context/value), thenfx_field(set by index/id/label) andfx_answer(Yes/No or radio/checkbox questions by question text + option label);fx_scrollbefore clicking elements obscured by fixed headersfx_waitfor the next state;fx_screenshot+ your own vision pass to verify what the DOM can't
Form-tool gotchas (from live ATS/portal forms): re-renders can silently drop checked boxes — re-verify all fields after any state change; a free-text location field is often separate from a city checkbox group; required radio groups are sometimes not wrapped in labeled field containers — audit fx_form.groups (and a final screenshot) instead of assuming the labeled fields are the whole form; DOM checked ≠ the framework's form state — trust the tools' confirmed/verified output (a stale pre-selected option is the classic failure: fx_answer handles it via the toggle cycle). Long application forms (e.g. Google) hide mandatory consent/attestation checkboxes ("…hereby certify that…", "I understand that the information I submit…") that gate the whole submit/apply: the button is left hard-disabled or the click silently no-ops until the box is ticked — that is client-side enablement, not bot protection; fx_gates surfaces these boxes (plus the disabled button and any alert banner) so you can find and check the actual gate. Material-style rows put the real <input> visually hidden under its own li/button chrome, so a direct input click can be reported "not clickable … obscured" — fx_click/fx_field/fx_answer recover by clicking the obscuring same-widget topmost and report it via overlay-top:….
Architecture
AI agent (e.g. opencode)
│ stdio · newline-delimited JSON-RPC 2.0
▼
firefox-mcp-marionette (src/server.mjs) ── tools: fx_* (29)
│ loopback TCP · <byteLen>:<json> frames
▼
your Firefox (firefox --marionette) ── your profile, your cookiessrc/protocol.mjs— pure wire codec (frame encode/parse, element-ref unwrap). No I/O, fully unit-tested.src/marionette.mjs— async Marionette client (one socket, one session, pending-command map).src/server.mjs— MCP stdio server + tool implementations.
Design notes (bugs that cost real debugging time)
Frames are pure ASCII.
JSON.stringifydoes not escapeU+E000/U+E001(W3C file markers) or any char ≥0x7F; in UTF-8 those are multi-byte, while the length prefix is computed from string length. That desynchronizes the stream for the rest of the connection. Every frame is\uXXXX-escaped so declared length always equals actual bytes (regression-tested).Element refs are unwrapped.
FindElementreplies wrap the uuid ({ "element-…": "uuid" }); subsequent commands (ElementClick,ElementSendKeys, …) take the bare uuid.File uploads use the raw absolute path in
ElementSendKeys— this protocol generation has no W3C base64 file encoding (those codepoints are the legacy SeleniumNULL/CANCELkeys there).Script bodies must
return. W3CExecuteScriptbodies are function bodies: a bare expression statement evaluates and is discarded.Marionette never awaits returned Promises. A
return (async () => { … })()body would serialize tonullimmediately, sofx_evalruns the body through a synchronous wrapper and pollswindowuntil the Promise settles (two-phase protocol;wait_msbounds it, default 30 s).#idCSS selectors with digit-leading ids are invalid (e.g. Ashby's UUID ids#56d78818-…).fx_click/fx_typeauto-rewrite them to[id="…"]and report the rewrite (used); unsupported CSS (e.g.:has()) is caught in-page before the driver call with an actionable error.DOM
checked≠ framework form state. Frameworks (notably Ashby) register a choice only on a real change.fx_answertherefore detects a stale pre-selected option (or an ineffective click) and runs a toggle cycle — click another option, then the target — on exclusive (radio/button) groups, re-verifying afterwards;fx_formaggregates radio/checkbox inputs into choice groups (question context + per-option state) so required groups can be audited in one call.Marionette keeps a persistent session across reconnects; a crashed automation client can leave stale session state — relaunch the browser if commands queue forever.
A single command must always settle. Commands are serialized and the browser's main thread can stall (modal dialog, hung navigation), so
send()bounds every command viaFX_MCP_CMD_TIMEOUT_MS(default 120 s). On expiry the connection is poisoned (socket destroyed, session cleared) and the next command reconnects fresh — without that, one unanswered command wedges the entire server forever.Socket events are per-socket. The
error/closehandlers only act whenthis.sock === s. A superseded socket (dropped during a command-timeout poison) can emit late events after we've reconnected; reacting to them would destroy the fresh, healthy socket.The first trigger is a decision, not an error, and not a silent attach. The server process == one automation session. Its first browser call runs a non-invasive probe (open the endpoint, expect the Marionette
hello, close — the probe never opens a session, so it cannot steal the active-client slot): a live browser → abrowser-detecteddecision (attach viafx_connect/ launch new viafx_launch/ user-directed); nothing listening → theneed_bootstrapdecision (launch new / connect / user-directed;FX_MCP_AUTO_LAUNCH=1collapses it to auto-launch); a dropped handshake →busy-other-client(Marionette serves one client per browser — a held instance drops a second client's socket cleanly, which is a distinct probe outcome, not a classified-away error). Committing an endpoint (user-chosenfx_connect,fx_launch, or auto-launch) makes later in-session reconnects silent.fx_statusnever opens a session — it only probes and reports.The launch port lives in prefs, never on the command line. Firefox has no
--marionette-portflag, sofx_launchcreates a fresh profile and writesuser_pref("marionette.port", N)+user_pref("marionette.enabled", true)to itsuser.jsbeforefirefox --marionette --no-remote -profile <dir>. The started pid is recorded (memory +<profile>/.firefox-mcp-marionette-launched.json) sofx_shutdownkills exactly that instance — a user-launched browser is never touched.
Tools
Tool | Purpose |
| Connection, active |
| Bootstrap option 1: start a NEW dedicated Firefox — fresh profile ( |
| Stop an instance this server started via |
| Bootstrap option 2 / detected-browser approval: (re-)point the MCP at an already-running loopback endpoint |
| Go to a URL |
| Current URL + title |
| Interactive-element map with refs (incl. visible |
| Click (ref or selector; digit-leading |
| Type text (clears first unless |
| Set |
| Set checkbox/radio state |
| Set file input (raw path, must be under |
| Dump visible form fields: index, type, label, name, context, value, options, files + aggregated choice groups (question context, per-option state); scopes to a CSS |
| Set a field by index (from |
| Answer a grouped choice question (Yes/No buttons, radio/checkbox options) by question text + option label; re-reads and reports the selection state; runs a toggle cycle on exclusive groups when a stale pre-selection (or ineffective click) is detected; self-heals to clicking the visible text-matching wrapper when option labels are unreadable ( |
| Scroll an element into view (e.g. under a fixed header), wait, return its top coordinate |
| Consent/attestation gate audit (read-only): visible checkboxes with nearby text — flagging certify/understand/agree/consent/attest/terms/privacy wording — plus disabled buttons (a dead Submit/Apply) and visible alert banners. Run it whenever a submit click does nothing or a submit button stays disabled; the fix is usually an unchecked consent checkbox, not bot protection |
| All hyperlinks of the current page: text + absolute href (optional |
| Structured page read ("scrape" without JS): one row per container |
| Search without JS: navigate to engine results (google/bing/duckduckgo presets; overridable |
| Run JS in the page (function body; |
| Wait for visible text or CSS selector (≤ 30 s) |
| Full-page PNG (not just the viewport) to a file under an allowed root |
| List / switch windows |
| Native dialogs |
| Current-origin cookies (names/domains only) |
Environment
Variable | Default | Meaning |
|
| Marionette endpoint (loopback only, by design) |
|
| Firefox's |
|
| Comma-separated roots that |
|
| Per-command bound. A command that never settles (modal dialog, hung page) poisons the connection and auto-reconnects on the next command, so one stuck page can't wedge the whole server |
| off | When the first-call probe finds no reachable Firefox, |
| auto-detect | Firefox binary for |
|
| Base directory for the per-port profiles |
Security
Loopback only. The client connects to
127.0.0.1— there is deliberately no network path.File access is rooted. Uploads and screenshots reject paths outside
FX_MCP_FILE_ROOTS.Use a dedicated profile for automation, and keep the browser visible: a human-in-the-loop is the expected model, not headless stealth. Native OS dialogs (e.g. the file picker) and CAPTCHAs are not automatable by design — stop and let the human handle them.
Launch is opt-in and self-contained.
fx_launch/auto-launch only create a NEW profile underFX_MCP_PROFILE_DIR(never touching your daily profile), bind to127.0.0.1only, and record the started pid sofx_shutdowncan stop exactly that instance — it never kills a browser the user launched.Attach is confirmed, not automatic. A running Marionette Firefox found on the first call of a session is never attached to without your say-so: the tool returns a
browser-detecteddecision (attach / launch new / other) instead of opening a session.
Testing
Zero-dependency test suite (built-in node:test):
npm test # or: node --test test/*.test.mjstest/protocol.test.mjs— frame codec, parser resilience, element-ref unwrapping (pure unit tests).test/marionette.test.mjs— the real client against an in-process fake Marionette server that verifies every frame's byte integrity (non-ASCII payloads included).test/server.test.mjs— spawns the real MCP server and drives it end-to-end (JSON-RPC plumbing, all tool paths, framing-safety under Unicode input, stdin-EOF shutdown); includes the fresh-session gate: a running browser is probed, tools ask before attaching, andfx_connectcommits the endpoint.test/bootstrap.test.mjs— first-trigger behavior against a stand-in "firefox" binary (test/helpers/fake_firefox.mjs): the no-browser decision payload, the running-browser decision (non-invasive probe → connect/launch question →fx_connectcommits, then calls run), the busy-handshake classification (dropped connection →busy-other-client, not a raw error), the prefs-based launch (port flows only throughuser.js), attach/reuse,fx_shutdownpid lifecycle, andFX_MCP_AUTO_LAUNCHinline bootstrap.
Live-browser tests (start your own firefox --marionette first; note Marionette serves one active client at a time — no other firefox-mcp-marionette client may be attached):
npm run e2e:live— connection, navigation, screenshot through the real wire protocol.npm run e2e:forms— the form primitives against a self-generated test page (labels, option/state round-trips, click fallbacks, negative cases).
CI: .github/workflows/ci.yml — syntax check + full test suite across Node 20/22/24.
License
GPL-2.0 (see LICENSE).
Available Tools
18 toolsfx_alert_acceptA
Accept (OK) the open native dialog
Inputs: no named parameters.
fx_alert_dismissA
Dismiss (Cancel) the open native dialog
Inputs: no named parameters.
fx_alert_stateB
Text of an open native dialog
Inputs: no named parameters.
fx_clickB
Click element by ref (from fx_snapshot) or CSS selector
Inputs: ref, selector.
fx_cookiesA
Cookies of the current origin (names/domains only)
Inputs: no named parameters.
fx_evalA
Execute JS in the page (script = function body, may return a value)
Inputs: js.
fx_pageA
Current URL and title
Inputs: no named parameters.
fx_screenshotC
View PNG to a file under an allowed root (or explicit allowed path)
Inputs: path.
fx_selectB
Set by option value or visible label
Inputs: ref, label, value, selector.
fx_snapshotB
Interactive-element map with refs (use refs in fx_click/fx_type/...)
Inputs: no named parameters.
fx_statusB
Health: connection, session, current page, navigator.webdriver flag
Inputs: no named parameters.
fx_toggleC
Set checkbox/radio state
Inputs: on, ref, selector.
fx_typeA
Type text into element (clears first unless keep=true)
Inputs: ref, keep, text, selector.
fx_uploadB
Set a file input value (raw-path sendKeys); path must be under FX_MCP_FILE_ROOTS
Inputs: ref, path, selector.
fx_waitA
Wait until visible text (or CSS selector exists). Default 10s, max 30s
Inputs: text, selector, timeout_ms.
fx_windowC
Switch active window by handle
Inputs: handle.
fx_windowsB
List windows (focused marked)
Inputs: no named parameters.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 18 tools
Each tool targets a distinct browser automation concern: navigation, page state, element actions, windows, alerts, cookies, and evaluation. There is minimal overlap, and similar-sounding tools like fx_window vs fx_windows are clearly differentiated by singular/plural usage and descriptions.
All tools share the fx_ prefix and use lowercase names without mixed case or underscores, which is a predictable pattern. There is a slight mix of verb-style names like fx_navigate and fx_click with noun-style names like fx_page and fx_cookies, but the overall convention remains coherent.
Eighteen tools is slightly above the typical 3-15 range, but each tool covers a distinct and necessary browser automation action. The count feels justified for the scope, though a few could be consolidated without much loss.
The set covers core browser automation workflows: navigation, interaction, snapshots, waiting, screenshots, window management, alerts, cookies, and JS evaluation. Minor gaps exist such as no dedicated back/forward/refresh or cookie mutation, but fx_eval can compensate for many advanced needs.
Maintenance
Related MCP Connectors
Control a real browser from AI agents: navigate, click, type, screenshot, read the page.
Stealth web automation for AI agents. Login, signup, navigate, screenshot.
Stealth web automation for AI agents. Login, signup, navigate, screenshot.
Let your AI agent operate any website through a real, logged-in browser session.
Related MCP Servers
- AlicenseAqualityBmaintenanceEnables AI agents to control a real web browser (Firefox) via Selenium WebDriver, supporting page navigation, interaction, and inspection through natural language.434MIT
- AlicenseNot gradedqualityBmaintenanceMCP server for the Firefox you actually use: an add-on plus a small native helper, so an agent gets your real profile, tabs and logged-in sessions instead of a blank headless browser. 45 tools for tabs, accessibility snapshots with refs, clicking, typing, screenshots, bookmarks, history and downloads.8Mozilla Public 2.0
- AlicenseNot gradedqualityBmaintenanceEnables Claude Code to drive a local Firefox browser: opening and managing tabs, navigating, reading page structure and text, clicking, typing, filling forms, running JavaScript, taking screenshots, and inspecting console messages and network requests. Everything runs over a local Unix socket with no network traffic, and actions are restricted to tabs explicitly placed under Claude's control.8 npmGPL 3.0
- AlicenseBqualityBmaintenanceLet AI agents use the Firefox, Zen, or LibreWolf browser you already have open, with your logins, cookies, and extensions intact. No Playwright, Chromium, or WebDriver. Agents work in background tabs while you keep browsing: read pages, click, fill forms, take screenshots, and capture network traffic. A WebExtension plus a local bridge lets several agents share one browser.27302 PyPI39MIT