Skip to main content

MCP server for stock Tor Browser automation via geckodriver and Marionette

Project description

tor-browser-mcp

the first real MCP server for Tor Browser

PyPI version Python versions License: MIT CI

Drives the stock Tor Browser via geckodriver + Marionette, preserves the anonymity properties pages depend on (RFP, letterboxing, FPI, isolated circuits), and exposes Tor control (NEWNYM, exit pinning, circuit observation), MITM-over-tor, and a full browser-automation surface as MCP tools.

No browser fork. No Firefox patch maintenance.

What you get

  • Stock Tor Browser, driven from MCP. The bundle the Tor Project ships, automated through Marionette. You upgrade on Tor Browser's schedule, not ours.
  • Anonymity properties preserved by default. Resist-fingerprinting (RFP), letterboxing, first-party isolation, and per-origin circuit isolation stay on. Driving the browser does not weaken what the browser hardened.
  • Tor control built in. NEWNYM (new identity), tor_set_exit_country / tor_set_exit_nodes (exit pinning), live circuit and stream observation via stem.
  • MITM-over-tor, opt-in. Decrypted HTTP/1.1, HTTP/2, and WebSocket traffic over a tor-bound mitmproxy, with a captured-flow buffer, save-to-disk, and replay. For adversary emulation and protocol reversing, not stealth.
  • Capability-gated tool surface. Six default capability groups for everyday automation; eight opt-in groups for vision, PDF, helper extension, raw chrome-context JS, and more. Servers expose only what you ask for.

Requirements

Python 3.11+ and an extracted Tor Browser bundle. The proxy-intercept extra additionally requires Python 3.12+ because of mitmproxy 11's runtime floor.

Verified on Windows x86_64 against Tor Browser 15.0.13 and Linux x86_64 against Tor Browser 15.0.14 (Firefox ESR 140.10.2, geckodriver 0.36.0). macOS is not supported in 0.1.0.

A compatible geckodriver binary is also required. The server resolves which binary to use in this order: --geckodriver-path if you supplied one; then <tbb_root>/Browser/geckodriver if it exists and is executable (covers older TB releases that shipped it in the tarball); then a geckodriver on PATH; then a version matching the bundle's Firefox ESR downloaded into ~/.cache/tor-browser-mcp/geckodriver/<version>/ on first run. Subsequent sessions reuse the cached binary. To avoid the on-first-run download (air-gapped or hostile network), pre-populate the cache directory from an out-of-band channel or pass --geckodriver-path to point at a binary you already have.

Install

pip install torbrowser-mcp
pip install torbrowser-mcp[proxy-intercept]

If you prefer to manage the geckodriver yourself, download the version matching Tor Browser's Firefox ESR from https://github.com/mozilla/geckodriver/releases (TB 15.0.x ships Firefox 140 ESR, which works with geckodriver v0.36.0) and pass its path via --geckodriver-path or place it on PATH. See docs/development.md for the full version map and cache layout.

Getting started

Standard MCP mcpServers config:

{
  "mcpServers": {
    "torbrowser": {
      "command": "torbrowser-mcp",
      "args": [
        "--tbb-root", "/path/to/tor-browser",
        "--output-dir", "/path/to/outputs"
      ]
    }
  }
}

Any stdio MCP client wires up the same way. --tbb-root may also come from the TBB_ROOT environment variable.

On Windows, escape the backslashes in the JSON: "C:\\path\\to\\Tor Browser".

Run torbrowser-mcp --help for the full flag set, including --caps, --allowed-root, --profile-mode, --tool-module, --socks-port, --control-port, --headless, and --unsafe.

Capabilities

Tools are organised into capability groups; each group is either enabled by default or opt-in via --caps.

Group Default? What it adds
core yes navigate, click, type, fill, scroll, snapshot, wait, screenshot, evaluate, frames, tabs, downloads
state yes cookies, localStorage, sessionStorage, storage-state save / restore
extract yes structured text / links / readable / table / form extraction from the live DOM
diagnostics yes session config, version info, capability listing
tor yes NEWNYM, circuit and stream observation, entry guards, GETINFO allowlist
network-observe yes passive request log from the page's perf timeline
vision opt-in coordinate-based mouse and keyboard, viewport screenshots
pdf opt-in save current page as PDF
highlight opt-in persistent on-page element highlight overlays
tor-routing opt-in pin exit country, pin exit nodes
http-over-tor opt-in pure-fetch GET/HEAD over the bundled tor (no browser navigation)
helper-extension opt-in per-session MV2 helper extension: network capture, request routing, init scripts
proxy-intercept opt-in embedded mitmproxy chained out through the bundled tor; HTTP(S) and WebSocket decryption, replay
unsafe opt-in (also --unsafe) RCE-equivalent escape hatches: chrome-context JS, server-process exec, raw tor control

Opt-in groups can be combined: --caps vision,pdf,helper-extension (comma-separated).

Helper extension capability

The helper-extension capability installs a per-session temporary MV2 WebExtension into Tor Browser and runs a localhost HTTP long-poll bridge between the driver and the extension's background page. It exposes nine tool methods covering bridge status, network observation, document-start init scripts, and declarative request routing.

Why it is opt-in

The helper installs an unsigned MV2 extension via chrome-context Marionette and grants it internal:privateBrowsingAllowed so its background page runs under Tor Browser's permanent private browsing. This is not a stealth capability by design: pages and scripts inside the same Tor Browser session can in principle observe that a WebExtension is loaded. The capability also sets extensions.webextensions.remote=false so webRequest listeners run in the same process as the background page; this is a small additional fingerprint signal but is consistent with the cap's opt-in stance. Tor Browser's permanent private browsing isolation (cookies, storage, FPI) is preserved.

Tool methods

Tool Purpose
browser_extension_status Snapshot of install / bridge state and counts of active captures and registered init scripts.
browser_network_capture_start / browser_network_capture_stop Observe requests matching WebExtension match patterns; returns per-request envelopes on stop.
browser_add_init_script / browser_remove_init_script Register and unregister document_start content scripts across all frames.
browser_route / browser_unroute / browser_route_list Install, remove, and inspect declarative routing rules (mock body, redirect URL, or header rewrite).
browser_network_state_set Toggle a simulated offline mode that cancels new requests at onBeforeRequest.

Known limitations

  • Response body capture is JS-initiated only. Bodies for fetch and XMLHttpRequest calls made from page JavaScript are captured via a page-world override the helper extension injects at document_start; matched entries surface with source: "merged" (webRequest envelope plus page-world body) or source: "page" (page-only request, no webRequest counterpart). Subresources initiated by the document parser - <img> src, <link> href, <script> src, top-level navigations - are still captured by webRequest for envelope metadata (URL, method, status, headers, peer IP, timing) but their bytes never reach the page-world override, so response_body stays empty on those entries. The underlying gap is that webRequest.filterResponseData's ondata callback fires onstop without delivering payload bytes on Tor Browser 15.0.13 / Firefox 140 ESR. For unconditional wire-level body capture regardless of how the request was initiated, enable proxy-intercept.
  • Mock-mode responses require proxy-intercept and are synthesised inline on the original URL. browser_route(..., body=...) registers the supplied body, status, and headers on the embedded mitmproxy substrate, which sets flow.response for matching requests in place; the browser receives the configured response on the original navigation target and window.location is preserved (Playwright page.route().fulfill() semantics). Mock-mode is rejected with ProxyInterceptError when proxy-intercept is not enabled. The synthesised response never crosses tor because mitmproxy serves it before chaining upstream. Document-parser-initiated subresources and WebSocket frame payloads are matched at the same layer.
  • Offline mode does not abort in-flight requests. browser_network_state_set("offline") cancels new requests at onBeforeRequest; requests already past that hook continue to completion. navigator.onLine is not toggled.
  • Manifest V2. The capability relies on blocking webRequest and will need to be revisited if Tor Browser moves past MV2.

Coexistence with proxy-intercept

When both capabilities are enabled they share responsibility cleanly: the helper extension owns redirect-mode routes and header rewrites at the browser layer, while proxy-intercept owns mock-mode fulfillment on the wire. A mock-mode route registers with the embedded mitmproxy addon, which synthesises flow.response for matching requests on the original URL; the helper extension does not see those requests at all, since the proxy answers them before they would reach the WebExtension's blocking listeners.

Proxy intercept capability

The proxy-intercept capability boots an embedded mitmproxy on a daemon thread chained out through the bundled tor's SOCKS port, installs a per-session MITM CA into the Tor Browser install via policies.json, and reconfigures Firefox to use the intercept proxy as its HTTP(S) upstream. Decrypted request and response bodies for HTTP/1.1, HTTP/2, and WebSocket traffic land in a bounded in-memory buffer that the six observation tools listed below read against.

Why it is opt-in

Enabling this capability changes what Tor Browser looks like on the wire and disables one of its anonymity properties:

  • Tor Browser's per-first-party circuit isolation is disabled for the session: every flow is multiplexed through the same upstream proxy connection before being demultiplexed by mitmproxy onto tor circuits, so first-party isolation no longer holds.
  • The local intercept proxy sees every page's plaintext. Decrypted bodies live in memory in the driver process and are written to disk verbatim when browser_intercept_save is called.
  • A per-session MITM CA is installed into the Tor Browser install directory. The driver writes (or deep-merges into) <tbb_root>/Browser/distribution/policies.json and restores the prior state on teardown. This is destructive in the sense that it mutates the on-disk Tor Browser bundle for the lifetime of the session.
  • The session is trivially distinguishable from default Tor Browser via TLS client fingerprint, ALPN/HTTP-2 settings, and the proxy negotiation pattern. This is not a stealth mode; use it for adversary emulation, detection engineering, and protocol reversing against content you control or are authorised to inspect.
  • Python 3.12+ is required for the optional extra. pip install torbrowser-mcp[proxy-intercept] pulls in mitmproxy>=11,<13, which transitively requires mitmproxy-rs>=0.12. That wheel ships only cp312-abi3 builds (Windows x86_64, manylinux x86_64, manylinux aarch64, macOS universal2). The core install stays at Python 3.11+; only this capability raises the floor further.

When the cap is in the enabled set, the MCP server emits the warning above (verbatim) to stderr at build_server time so a misconfigured deployment cannot accidentally start the server without the user seeing the trade-off.

Tool methods

Tool Purpose
browser_intercept_start Confirms the substrate is running and returns the recorder's monotonic cursor plus the CA fingerprint (SHA-256 of the DER) so callers can tail new flows.
browser_intercept_stop Clears the recorder buffer and resets the cursor; the daemon thread and proxy stay running for the rest of the session.
browser_intercept_flows Lists captured flows with optional since, host, and status_code filters, a result limit, and optional inlined bodies capped at max_body_bytes.
browser_intercept_flow Returns one captured flow by its mitmproxy-assigned id, with bodies inlined by default.
browser_intercept_save Persists the current buffer as a native mitmproxy flow archive under the configured output directory.
browser_intercept_replay Deep-copies a captured flow, applies optional request modifications (method, URL, headers, body, HTTP version), and replays it through the live intercept proxy; the replay surfaces as a new entry in the recorder buffer.

Known limitations

  • HTTP/3 / QUIC is not intercepted. mitmproxy's classic interception path covers HTTP/1.1 and HTTP/2. Firefox normally falls back to HTTP/2 against an HTTP-proxy upstream; if a destination ends up speaking HTTP/3 anyway, the resulting traffic is invisible to the recorder.
  • HSTS-preloaded hosts cannot be MITM'd. Firefox enforces the HSTS preload list independent of policy-installed CAs, so cert errors on preloaded hosts (Google properties, GitHub, Cloudflare, the social-network majors, etc.) are non-overridable. The capability records these as synthetic error flows; bodies are not available.
  • No flow persistence across sessions. browser_intercept_save writes an archive, but a fresh session cannot replay or re-load it through the tool surface in this slice.

Coexistence with helper-extension

Both capabilities can be enabled together; the helper observes and routes at the browser layer, the intercept proxy operates on wire traffic. Mock-mode routes (browser_route(..., body=...)) are fulfilled here, on the wire, by synthesising flow.response against the original request URL; the helper extension never sees those flows. Redirect-mode and header-rewrite routes still flow through the helper extension and surface on the proxy as their rewritten form.

Unsafe capability

The unsafe capability is an opt-in escape hatch for trusted local research workflows. It is enabled via --unsafe (or by adding unsafe to --caps) and adds three RCE-equivalent tools:

Tool What it exposes
browser_chrome_evaluate_unsafe Runs arbitrary JavaScript in the Firefox chrome (browser-UI) context via Marionette. Chrome-context scripts can read arbitrary preferences, drive the browser UI, and reach into XPCOM.
browser_run_python_unsafe execs arbitrary Python in the running MCP server process, with the driver, the Selenium handle, the stem controller, and the path policy bound as globals. Stdout is captured into the response.
tor_control_command_unsafe Sends a raw control command to the bundled tor, bypassing the tor_get_info allowlist. Accepts any verb the controller will honour, including SETCONF, SIGNAL HALT, and EXTENDCIRCUIT variants that can crash or partition tor.

Each of these is RCE-equivalent in its respective layer: page-trust, host-trust, and tor-trust all collapse to "whatever the MCP client asks for, the server does." Never expose the unsafe capability to an untrusted MCP client. It exists so a researcher driving the server locally can poke at the chrome context, prototype a new primitive without restarting the server, or experiment with tor control verbs that the curated surface deliberately omits.

Filesystem policy

Tool calls that read or write files are resolved through a path policy: outputs land under --output-dir, and reads are restricted to MCP roots, the server cwd, and any --allowed-root directories. --allow-unrestricted-file-access disables the guardrail. This is a convenience boundary, not a sandbox.

Limitations

  • Automation is detectable. Default WebDriver mode leaves navigator.webdriver === true; pages and scripts inside the session can see they are being driven.
  • Not a stealth tool. TLS client fingerprint, ALPN settings, and the proxy negotiation pattern distinguish a driven session from default Tor Browser use even before any capability adds further signals.

Layout

  • torbrowser_driver/ - launch recipe, capability registry, and capability-tagged driver primitives.
  • torbrowser_mcp/ - MCP server that walks the driver's capability registry and exposes each method as a tool.
  • tests/ - unit tests plus an opt-in integration smoke suite (pytest -m integration).

Contributing

git clone https://github.com/Boti-Ormandi/tor-browser-mcp
cd tor-browser-mcp
pip install -e .[dev]
pre-commit install
pytest                       # unit suite
pytest -m integration        # live Tor Browser smokes (needs --tbb-root configured)

Issues and pull requests welcome at https://github.com/Boti-Ormandi/tor-browser-mcp.

License

MIT - see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

torbrowser_mcp-0.1.0.tar.gz (219.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

torbrowser_mcp-0.1.0-py3-none-any.whl (153.7 kB view details)

Uploaded Python 3

File details

Details for the file torbrowser_mcp-0.1.0.tar.gz.

File metadata

  • Download URL: torbrowser_mcp-0.1.0.tar.gz
  • Upload date:
  • Size: 219.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for torbrowser_mcp-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b2dc59f4b8e9e5c52148e3302dfaba2ee07794aebf50982c925be6f02c7a1b6a
MD5 58bf014c96b1930ec0c0bf10ffef4112
BLAKE2b-256 8340fd7159987b96c9bd97f77264d5e039c49efb9807cadd51bac4f904c168cb

See more details on using hashes here.

Provenance

The following attestation bundles were made for torbrowser_mcp-0.1.0.tar.gz:

Publisher: release.yml on Boti-Ormandi/tor-browser-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file torbrowser_mcp-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: torbrowser_mcp-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 153.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for torbrowser_mcp-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7a6242ba27cdf0067457c323f4974c2fc58cace532616570c9520438edd97283
MD5 ce93fd8e2672433574e78084a680529c
BLAKE2b-256 a262f7125f1adbce45f919b4dd1ff3897669ff02404489f5a13fac0cbb3c9bf0

See more details on using hashes here.

Provenance

The following attestation bundles were made for torbrowser_mcp-0.1.0-py3-none-any.whl:

Publisher: release.yml on Boti-Ormandi/tor-browser-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page