Skip to main content
Glama

firefox-jev-mcp

An MCP server that lets Claude drive Firefox, with a fast decision model for clicks: Claude plans, Jev (TypeSafe) picks the element to act on, and Claude only takes over when Jev hesitates.

Why

With a classic browser MCP server, the LLM rereads the page and decides on every click: one model turn per action, so a few seconds each time. Most clicks are simple decisions ("which link leads to Paris?"). firefox-jev-mcp hands them to Jev, a model that generates no text and returns one probability per page element, in about a second. Claude keeps what it does best: understanding the request, planning, and settling ambiguous cases.

Related MCP server: jev-reflex

Results

Same task, same prompt, same model (Claude Opus 5.5 through claude -p), two races run in parallel: start from the Wikipedia article "Titanic" and reach "Eiffel Tower" by clicking links only (no search, no typing, no typed URL, no going back).

Wikipedia race

firefox-jev-mcp

Mozilla firefox-devtools-mcp

Total time

14.7 s

23.4 s

of which Claude thinking

7.9 s

19.1 s

Claude turns

4

12

Claude tokens

20.8k

88.5k

Cost per race, Claude and Jev included

€0.033

€0.050

Over 4 races, firefox-jev-mcp finished first every time: 17.2, 19.2, 22.7 and 14.7 s, against 23.5, 20.2, 27.8 and 23.4 s (the first ones used earlier versions of the server). At those rates, 1,000 races cost €32.74 against €49.60.

Basic actions, measured without an LLM on 4 real pages (median of 4 runs after a warm-up run):

Basic action

firefox-jev-mcp

Mozilla firefox-devtools-mcp

Google chrome-devtools-mcp

Playwright MCP

Playwright MCP

Browser

Firefox

Firefox

Chrome

Chrome

Firefox

Click, until the next page has loaded

181 ms

290 ms [1]

326 ms

958 ms

1,257 ms

Navigate to a URL

186 ms

129 ms [2]

280 ms

248 ms

388 ms

Snapshot of a Wikipedia article, in tokens [3]

3.7k

1.1k [4]

215k

209k

209k

  1. Median over 3 pages: its snapshot does not show the Hacker News navigation links.

  2. Returns as soon as the HTML is parsed (DOMContentLoaded), before loading finishes.

  3. Estimate: characters divided by 4.

  4. Truncated to 100 lines by default.

Prices used: public API prices (Claude Opus 5.5, Jev 1.13 at $0.042 per million input tokens, output free), ECB rate of 2026-09-23. Method, raw data and scripts to reproduce everything are in bench/ (documented in French).

How it works

Claude Code
   |  MCP tools (stdio)
   v
Local MCP server (server/, TypeScript)
   |-- Jev API: one Choice over the page elements + one Noul "goal reached?",
   |            then verification Nouls when the Choice hesitates
   |
   |  WebSocket ws://127.0.0.1:8765 (moz-extension:// origin only)
   v
Firefox extension (extension/)
   |-- content.js: extracts interactive elements, clicks, types, waits for rendering to finish
   '-- background.js: WebSocket bridge, tabs, navigation, iframes, network tracking, copies of received PDFs

Before each read (browser_snapshot, browser_read, steps of browse_goal), the extension waits for the page to finish rendering: the tab's data requests are done, the structure has been stable for 250 ms, and no loading indicator is visible ("Loading...", aria-busy, a progress bar without a value). It waits 4 s at most and reports an indicator that is still visible. A page that has already rendered is read without delay.

The browse_goal tool runs on its own: page snapshot, Jev's decision, action, and so on until the goal. It stops when the goal is reached, when Jev hesitates, or when the action is sensitive (paying, deleting, sending), and then gives Claude the best candidates, ready to use. Claude remains the orchestrator: the extension never contacts Claude, which works with a Claude subscription, without an Anthropic API key.

How Jev decides, step by step

  1. Duplicates are merged. Links to the same URL (menu, card, footer) become a single option. The probabilities of a Choice always add up to 1: without merging, Jev splits its probability across equivalent links, and its confidence drops even though it has no doubt about the destination.

  2. Used elements are removed. An element already clicked or filled on the current page is no longer offered, which avoids loops.

  3. Text to type is known to Jev. With typeText, Jev knows a search is possible. Without typeText, search fields are not offered to it.

  4. Choice + Noul "goal reached?" in a single request.

  5. Verification when the Choice hesitates. Below minConfidence (0.6), a second call asks one Noul per candidate among the top 3: "does this action move towards the goal?". A Choice is relative and shares its probability across several valid paths. A Noul is absolute. The loop acts if the chosen candidate reaches verifyThreshold (0.8), otherwise it hands back to Claude.

  6. Arrival check. After each action, a Noul sees the previous page, the action taken and the current page, for goals defined by the starting page ("the 3rd story", "the cheapest book").

  7. Dropdowns. A Choice over the options picks the value to select. A list drawn by the page (ARIA combobox: Angular Material, React Select...) often only has its options once open: the loop clicks it first, then select clicks the option and closes the list.

  8. Typing. typeText is typed only once, and Enter is pressed only in a search field.

Above 250 options (the API limits a Choice to 255), the decision takes two passes: one Choice per slice of 200, in parallel requests, then a final Choice over the top 5 of each slice.

Getting started

Requirements: Node.js 20 or later, Firefox 142 or later, a TypeSafe API key (console.typesafe.ai), and pdftotext to read PDFs (package poppler-utils).

npm install
cp .env.example .env   # then set TYPESAFE_API_KEY
  1. Connect the server to Claude Code for all projects:

    claude mcp add firefox-jev --scope user -- "$(pwd)/node_modules/.bin/tsx" "$(pwd)/server/src/index.ts"

    Or run claude from this folder: the .mcp.json file is detected. The server only opens port 8765 on the first tool call, so sessions that do not use Firefox do not block it.

  2. Install the /firefox command, available in all projects:

    ln -s "$(pwd)/skills/firefox" ~/.claude/skills/firefox
  3. Ask Claude, for example: /firefox open the API documentation on example.com. The command starts Firefox if needed (browser_launch tool), then chains the tools.

To start Firefox by hand, with the extension, in the dedicated profile (./firefox-profile, created on first launch): npm run firefox. The extension badge shows ON when a Claude Code session is connected.

Driving your everyday Firefox

The extension can also run in your everyday Firefox profile, to act on pages you already have open (resuming a form from an email, for example). It is off by default there: one click on its button gives control to Claude (badge ON), a second click takes it back. While it is on, it takes priority over the dedicated profile, which reconnects as soon as it is turned off.

Claude works in its own tab, in the background: the tab you are looking at stays displayed (a stream keeps playing), and tools without a tabId target the tab Claude is working in. On the first call, Claude only takes over the active tab if it is empty, otherwise it opens its own. To see its page, ask it to display it (activate).

Firefox only installs extensions signed by Mozilla permanently. "Unlisted" signing is free and does not publish the extension:

  1. Create keys on addons.mozilla.org/developers/addon/api/key and set them in .env (WEB_EXT_API_KEY, WEB_EXT_API_SECRET).

  2. Run npm run sign: the signed .xpi file lands in web-ext-artifacts/. Each new signature requires raising the version in extension/manifest.json. Since 0.3.0, the extension also asks for access to network requests (webRequest), to follow page loading and keep a copy of PDFs: Firefox asks you to accept it on update.

  3. Open it in Firefox (drag the file into a window, or about:addons then "Install Add-on From File").

To try it without signing: go to about:debugging, "This Firefox", "Load Temporary Add-on" and choose extension/manifest.json. Loaded this way, the extension behaves as in the dedicated profile: it connects by itself, and disappears when Firefox restarts.

MCP tools

Tool

Role

browser_launch

Starts Firefox with the extension if none is connected

browse_goal

Autonomous navigation towards a goal: snapshot, Jev's decision, action, in a loop

jev_rank

Ranks the page elements with Jev for a goal, without acting

browser_snapshot

Visible interactive elements, each with a ref (e1, e2...)

browser_act

click, type (with submit for Enter) or select on a ref

browser_upload

Drops local files (resume, letter...) into a file field or a drag-and-drop zone

browser_save

Saves to disk the PDF shown in the tab, a link's target or a URL, without overwriting anything

browser_navigate

Opens a URL or goes back, in the background; activate shows the tab

browser_read

Visible page text, iframes included, or the text of the PDF shown in the tab

browser_scroll

Scrolls by one screen

browser_status

Connection to the extension, presence of the Jev key, tabs (displayed, Claude's), recent PDFs

browse_goal returns one of these statuses:

  • done: Jev estimates that the page meets the goal (goalThreshold, 0.85 by default).

  • need_decision: Claude has to decide. reason explains why and candidates gives the top 5 refs.

  • max_steps: the number of steps was reached (maxSteps, 6 by default).

  • error: a technical incident. The steps already done are kept.

Limitations

  • Synthetic clicks: Firefox offers extensions no equivalent of Chrome's debugger API. Events have isTrusted = false, which some sites (anti-bot, payment, OAuth) ignore.

  • Iframes are explored 3 levels deep, even across origins, and the content of a modal window comes first. An iframe link that targets the whole page (target="_top") is opened by the tab, because Firefox blocks that navigation without a real click. Open and closed Shadow DOMs are supported.

  • PDFs: Firefox's viewer is closed to extensions, so the extension keeps a copy of the last 10 PDFs received (30 MB each at most, in memory, forgotten when it is turned off). browser_read extracts their text and browser_save saves them, even when a site serves a document only once or another PDF has replaced it in the tab.

  • Background tabs: Firefox slows pages down there. The extension counts its waits with its background script, which is not slowed down, but a few sites only render their content once visible (load on scroll, render on next frame). In that case show the tab with activate.

  • At most 1,000 elements per snapshot: on a very long article, the last links are not offered.

  • One session at a time drives Firefox: the last one to call a tool takes control, and a request in progress in the other session fails.

  • Planning: Jev excels when the target link is on the page, less so when choosing an intermediate page. It then hands back to Claude.

  • Language: Jev is mostly trained in English. Goals phrased in English give better results.

  • Privacy: the URL, title, element labels and the beginning of the text of pages visited by browse_goal or jev_rank are sent to the TypeSafe API. The /firefox command goes through Jev for all clicks, including in your everyday profile, on pages where you are logged in.

License

MIT, see LICENSE.

Related MCP Connectors

Related MCP Servers