Skip to main content
Glama
README.md
# foxwire

foxwire lets an MCP client such as Claude Code drive your real, logged-in Firefox: your tabs, cookies and sessions,
in the browser you already use. It is a WebExtension, not a WebDriver session, so Firefox is started normally,
`navigator.webdriver` stays `false`, and nothing about the browser's configuration changes. Marionette-based routes
(such as `@mozilla/firefox-devtools-mcp --connect-existing`) need Firefox relaunched with remote-control flags. That
sets `navigator.webdriver = true`, shows the remote-control icon in the URL bar, and can trip bot checks on sites behind
Cloudflare and similar services; see [docs/WHY-NOT-WEBDRIVER.md](docs/WHY-NOT-WEBDRIVER.md).

## Status

Version 0.1.1. Requires Firefox 140 or later. Tested on Linux only. Single author, in daily use by that author;
expect rough edges. Not listed on addons.mozilla.org: build it yourself and load it temporarily or sign your own copy.

## How it works

```
MCP client ──stdio──▶ foxwire-mcp ──Unix socket──▶ foxwire-broker ◀──ws://127.0.0.1── Firefox extension
```

- **Extension** (MV2, persistent background): dials out to the broker, runs each command with `browser.tabs.*`, and
  injects short-lived scripts into pages only for the duration of a call.
- **Broker**: one long-lived host process per user. It owns the loopback WebSocket and multiplexes any number of MCP
  clients onto the one extension connection. The MCP server spawns it on demand.
- **MCP server**: one stdio process per client session, holding only that session's selected tab.

The full design, including the wire protocol, snapshot/uid model and detectability review, is in
[docs/DESIGN.md](docs/DESIGN.md).

## Security model

- The broker binds `127.0.0.1` only. The extension connects out to it; nothing listens inside Firefox.
- Pairing uses a 32-byte secret generated by the broker on first run and stored with mode 0600. You paste it into the
  extension's options page once. Each connection proves knowledge of it with an HMAC challenge-response; the secret
  is never sent over the socket.
- The broker accepts WebSocket connections only with a `moz-extension://` `Origin`, so ordinary web pages cannot
  connect to it.
- Page access requires a per-origin host permission that you grant by clicking, either in the options page or when
  foxwire asks just in time. Without a grant, tools fail with `NO_GRANT`; they never widen access by themselves.
  Tab titles and URLs are visible without grants, with query strings and fragments stripped for ungranted tabs.
- `evaluate_script` (arbitrary JavaScript) is off by default and must be switched on in the options page.
- No content scripts are declared, no `web_accessible_resources` exist, and injected code runs in the extension's
  isolated world and cleans up after each call.

What this does not protect against: **any process running as your user that can reach the broker's Unix socket can
drive every site you have granted, with your logged-in sessions.** That includes every MCP client you register
foxwire with, and whatever those clients' models decide to do. Grant only the sites you want driven, and keep
`evaluate_script` off unless you need it. Actions use synthetic DOM events, so interactive bot checks such as
"I am human" checkboxes still need your own click.

## Install from source

Requires Node 22 or later and Firefox 140 or later.

1. Build and test:

   ```
   npm install
   npm run build
   npm test
   ```

2. Run the broker once in a terminal to generate the pairing secret. It prints the secret once, then keeps running;
   stop it with Ctrl-C afterwards (the MCP server starts it again on demand).

   ```
   npm run broker
   ```

   The secret is stored in `~/.config/foxwire/secret` (or `$XDG_CONFIG_HOME/foxwire/secret`).

3. Load the extension, either:
   - **Temporarily**: `about:debugging#/runtime/this-firefox` → **Load Temporary Add-on** → `extension/manifest.json`.
     It is dropped when Firefox restarts.
   - **Permanently**, by self-signing an unlisted build on addons.mozilla.org with your own API keys:
     `scripts/sign.sh` (needs `AMO_JWT_ISSUER` and `AMO_JWT_SECRET` in the environment). First change the gecko id in
     `extension/manifest.json`: `foxwire@vidr.cc` is tied to the author's AMO account and cannot be signed by anyone
     else. See [docs/RELEASE.md](docs/RELEASE.md).

4. Open the extension's options page (`about:addons` → foxwire → Preferences), paste the secret, Save, and grant the
   sites you want driven. "Grant all sites" is available but not required, except for screenshots.

5. Register the MCP server. For Claude Code, at user scope:

   ```
   scripts/install-mcp.sh
   # equivalent to:
   claude mcp add -s user foxwire -- node /path/to/foxwire/mcp/dist/server.js
   ```

   Other MCP clients: configure a stdio server with the command `node /path/to/foxwire/mcp/dist/server.js`.

6. Check with the `status` tool: it should report `paired: yes` and list your grants.

Environment variables `FOXWIRE_PORT`, `FOXWIRE_SOCKET` and `FOXWIRE_CONFIG_DIR` override the defaults.

### Firefox as a Flatpak

The Firefox Flatpak cannot read arbitrary host directories, so "Load Temporary Add-on" may not see your checkout.
Copy the built extension somewhere it can read, such as `~/Downloads`, and load it from there. Loopback networking is
shared with the host, so the broker connection works without native messaging or Flatpak overrides.
[docs/RELEASE.md](docs/RELEASE.md) has the exact commands.

## Tools

| Tool | What it does |
|---|---|
| `status` | Health check: pairing, extension and Firefox versions, grants, option switches, selected tab |
| `list_pages` | List open tabs, optionally filtered by a title/URL substring (no site grant needed) |
| `select_page` | Choose the tab later calls act on, by index, tab id, or URL/title substring |
| `new_page` | Open a URL in a new tab and select it |
| `navigate_page` | Load a URL in the selected tab |
| `navigate_history` | Go back or forward |
| `close_page` | Close a tab |
| `take_snapshot` | Accessibility-style text tree with a `uid` on each interactable element; frames are merged in |
| `get_page_text` | The page's visible text, or the text of one uid's subtree or of a CSS selector's matches in any frame |
| `click_by_uid` | Click an element (optionally double-click) |
| `hover_by_uid` | Hover an element |
| `fill_by_uid` | Set a field's value, or check/uncheck a checkbox or radio |
| `type_text` | Type text key by key into an element or the focused element, optionally pressing Enter after |
| `press_key` | Press a key with optional modifiers |
| `select_option` | Choose options of a `<select>` by value or label |
| `upload_file_by_uid` | Attach local files to a file input |
| `screenshot_page` | PNG of the viewport or full page |
| `screenshot_by_uid` | PNG of one element |
| `wait_for` | Wait for text, a selector or a uid to appear, or for the page's or an element's text to change, returning what was added |
| `sleep` | Pause without touching the browser (up to 60 s) |
| `handle_dialog` | Pre-answer the next `alert`/`confirm`/`prompt` |
| `evaluate_script` | Run a JavaScript function in the page (off by default) |

Names and semantics follow `@mozilla/firefox-devtools-mcp` where they overlap. Uids look like `37kqx` in the top
frame and `f2_37kqx` inside iframe `f2`; the letters are a per-document tag, so a uid from before a navigation or
extension reload fails with `STALE_UID` instead of hitting the wrong element. Actions that change a page (click,
fill, type, press, select, upload) end with an `after:` note describing the visible change, or saying there was none
within a moment. Every call has a timeout and fails with a named error code.

## Intent bubble and just-in-time grants

Tools that act on a tab accept an optional `intent`: a short note of what the model is doing and why. foxwire shows
it as a small thought bubble on the page, drawn with a user stylesheet on the root element's pseudo-elements, so no
DOM nodes or page scripts are added. It can be switched off in the options page. The toolbar popup lists recent
calls with their intents and results.

When a tool needs a site you have not granted, the toolbar button shows an orange `?` and the popup asks you to Allow
or Deny that one origin. Allow opens Firefox's own permission prompt for exactly that origin, and the waiting call
then continues. A Deny is remembered for 10 minutes. All-sites access is never requested this way.

## Limitations

- Firefox only, Manifest V2.
- Input is synthetic DOM events (`isTrusted` is `false`). Most sites accept them; some editors need `type_text`
  rather than `fill_by_uid`, and interactive bot checks need a human click. Synthetic clicks cannot open pop-ups:
  foxwire opens a `target=_blank` link's address itself, but a `window.open` from a click handler stays blocked
  unless pop-ups are allowed for that site.
- Typed text arrives as trusted `beforeinput`/`input` events, but key events are untrusted. A widget that only
  reacts to trusted keystrokes (some bank address lookups) will take the text and not open its suggestions.
- Screenshots need the all-sites grant, because `tabs.captureTab` requires `<all_urls>`.
- No console or network capture, downloads, cookies, viewport resizing or PDF export.
- File uploads are capped at 15 MB in total; `wait_for` waits at most 60 seconds.
- While the thought bubble is visible, a page could detect it by reading the computed style of the root element's
  `::before`/`::after`. Turn the bubble off to remove even that.

## Development

| Command | Purpose |
|---|---|
| `npm run build` | esbuild bundles for extension, broker and MCP server |
| `npm run watch` | Rebuild on change |
| `npm run typecheck` | `tsc --noEmit` |
| `npm run lint` | `web-ext lint` on the extension |
| `npm test` | Unit tests with `node --test` |
| `npm run run:scratch` | Launch a throwaway Firefox profile with the extension |

The codebase is meant to stay small enough to read in a sitting, with no frameworks and no runtime dependencies
beyond `@modelcontextprotocol/sdk` and `ws`. [CLAUDE.md](CLAUDE.md) is the brief for contributors and coding agents;
[test/E2E.md](test/E2E.md) is the manual acceptance checklist. foxwire was built with Claude Code.

## Licence

MIT, see [LICENSE](LICENSE).

Maintenance

ActivityMaintained
ResponsivenessNo issues