Skip to content

Zek21/firefox-cdp

Repository files navigation

firefox-cdp — a CDP-style Tab for Firefox, over WebDriver BiDi

A minimal Python library for driving a real Firefox over WebDriver BiDi, with a flat, CDP-style API — navigate / evaluate / screenshot / type / click over a single websocket, in ~700 lines. One dependency (websocket-client), the rest is the standard library. No Selenium, no geckodriver, no Playwright server.

Read this first (what it is / isn't): this is a Python library you import — a CDP-style API, named for the ergonomics you already know. It is not a network proxy and not a CDP server: it does not accept incoming CDP connections from Puppeteer or other clients, and it does not restore CDP in Firefox. Under the hood it speaks Firefox's WebDriver BiDi; "CDP-style" describes the shape of the API, not the wire protocol.

Live-verified on Firefox Developer Edition 153 (Windows 11).

python firefox_bidi_worker.py --selftest --json
# launch → BiDi session → navigate → read title → screenshot, all green

Why this exists

Firefox shipped an experimental Chrome DevTools Protocol implementation in 2021 (FF 86). Mozilla then chose to standardize on the W3C's WebDriver BiDi and retire the CDP it didn't control:

  • Firefox 129 (May 2024): CDP deprecated — off by default, opt-in via the remote.active-protocols pref.
  • Firefox 141 (June 2025): CDP completely removed, pref and all. 141 is the first Firefox where you simply cannot use CDP.

So on any current Firefox, point --remote-debugging-port at it expecting CDP and the /json/* discovery endpoints return HTTP 404. The Remote Agent is still there — it just speaks WebDriver BiDi now.

BiDi isn't "CDP with new names." It's a different, standardized, bidirectional protocol — a superset of what Firefox's CDP did. The one thing that carried over is the transport shape: JSON-RPC over one websocket ({id, method, params}{id, type, result}). That's what lets a CDP-style Tab map onto it with almost no structural change.

"Why not just use Playwright / Selenium?"

Scope, not competition. Playwright, Selenium and Puppeteer are frameworks — driver binaries, big dependency trees, an execution model. If you're writing end-to-end tests, use them.

This is for the other case: a small automation script or agent that already thinks in terms of a minimal Tab and shouldn't pull in a whole framework just to get a Firefox backend. Raw websocket, one pip dependency, uses the Firefox already on the machine, and reads top-to-bottom as a clean reference for how BiDi actually works. A library, not an ecosystem.

The four things that will 403 or 404 you

The adapter handles all of these; they're here so the protocol isn't a black box.

  1. The endpoint has a path. The agent logs WebDriver BiDi listening on ws://127.0.0.1:<port> — with no path. The bare root is just an HTTP server. The real websocket is at ws://<port>/session.
  2. session.new is mandatory before any browsingContext.* / script.* call. Its reply carries the real browserVersion.
  3. There's an Origin allowlist. websocket-client sends Origin: http://127.0.0.1:<port>, so launch with -remote-allow-origins http://127.0.0.1:<port> or the handshake dies with 403 "incorrect Origin header".
  4. script.evaluate returns a BiDi remote value, not plain JSON. A small deserialize_bidi() walks it back into native Python.

Install

git clone https://github.com/Zek21/firefox-cdp
cd firefox-cdp
pip install -r requirements.txt      # websocket-client (+ Pillow/pytest for the bonus kit & tests)

External: Firefox (Developer Edition preferred; set FIREFOX_BIN if it's non-standard).

Usage

from firefox_bidi_worker import launch_firefox, FirefoxBiDi

proc, ws_url, profile = launch_firefox(port=9227)   # isolated, visible Firefox
ff = FirefoxBiDi(ws_url)                             # auto-creates the BiDi session

ff.navigate("https://example.com")
print(ff.browser_version)                            # "153.0"
print(ff.evaluate("document.title"))                 # "Example Domain"
ff.screenshot("shot.png")

# real, trusted input (React-safe) — not synthetic .click()
ff.type_text("hello world")
ff.click_xy(640, 400)
ff.press_keys("")                              # Enter

FirefoxBiDi gives you navigate, current_url, evaluate (clean native Python via deserialize_bidi), screenshot, activate, type_text, press_keys, click_xy, new_tab, and the raw rpc(method, params) escape hatch. FirefoxSession adds the Chrome-CDP tab model: one window, many named tabs, open_tab / switch.

python firefox_bidi_worker.py --navigate https://x.com --screenshot out.png
python firefox_bidi_worker.py --tabs "https://a.com,https://b.com" --cycles 2
python firefox_bidi_worker.py --trace-chrome --duration 120   # mirror a live Chrome tab into Firefox

Isolation & safety

Every launch uses -no-remote + a dedicated -profile, so it's a fully isolated instance — it never attaches to, mutates, or kills the Firefox you already have open. Cleanup kills only processes whose command line contains that unique profile path. A unit test asserts those flags can't be quietly removed. Optional: launch_firefox(seed_profile=...) copies only auth cookies read-only from an existing profile so the driven instance is logged in, without touching your running Firefox.

Proof (reproduce it)

python firefox_bidi_worker.py --selftest --json   # 5/5: launch→session→nav→title→screenshot (FF 153)
pytest -q                                          # 19 passed — pure-logic + protocol-contract guards

Bonus: a small livestream kit

Built alongside the worker (and kept here for convenience, not the main event): stream_kit.py (YouTube/Twitch SEO, a center-safe lower-third overlay, auto-highlight clips, A/B thumbnails), obs_stream_control.py (an obs-websocket v5 client — scenes/sources/stream, with a self-test against an in-process mock server), and youtube_edit_firefox.py (a worked example of driving a real logged-in site through the BiDi worker). Each has its own --selftest. See docs/PROTOCOL.md for the full BiDi-vs-CDP notes.

Layout

firefox_bidi_worker.py    # the core: BiDi Tab primitive + launcher + Chrome mirror
tests/                    # fast, deterministic protocol-contract guards (no browser/network)
docs/PROTOCOL.md          # the full WebDriver-BiDi-vs-CDP protocol notes
stream_kit.py             # bonus: stream SEO / overlays / clips / thumbnails
obs_stream_control.py     # bonus: obs-websocket v5 client
youtube_edit_firefox.py   # bonus: worked "drive a logged-in site" example

License

MIT — see LICENSE. Built by Exzil Calanza. Issues welcome, especially if the launch handshake behaves differently on your Firefox version.

About

A ~700-line Python library to drive Firefox over WebDriver BiDi with a simple, CDP-style API (navigate/evaluate/screenshot/type/click). Not a CDP proxy. No Selenium/geckodriver/Playwright. Firefox removed CDP in v141.

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages