Skip to content

GabriWar/umbra

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

umbra

umbra

Stealth Chrome MCP server for AI agents. Real Chrome • 31/31 sannysoft • 0% creepjs • 77 tools • multi-browser • proxy pools • encrypted sessions • live human handoff over Cloudflare tunnel.

umbra — the darkest part of a shadow, where light is fully blocked.

Built by merging the best parts of obscura 536072b (per-session fingerprint payload) + fantoma 86f20eb (zero-mouse ARIA driver) + stealth-browser-mcp def424d (nodriver + MCP surface) — and filling in their gaps: the Page.enable() injection bug, real-GPU headless via --headless=new + ANGLE Vulkan, dynamic UA-CH version pinning, mDNS-aware WebRTC SDP filter, MCP token-efficient minification, and the _untrusted: true cognitive-separation flag on every page-sourced response.


⚡ at a glance

umbra
bot.sannysoft.com 31 / 31 ✓ (perfect)
creepjs headless 0 % (matches vanilla Chrome)
creepjs stealth 0 %
headless real GPU via --headless=new + ANGLE Vulkan (no SwiftShader tell)
CDP automation tells stripped webdriver cdc_* $cdc_ _phantom _selenium __webdriver_* __nightmare ...
TLS / JA3 / JA4 real Chrome stack + optional curl_cffi for raw HTTP
WebRTC mDNS-aware SDP filter (real-Chrome behavior, no LAN IP leak)
MCP tools 77 — broad primitives + batch flagship + proxy_pool_* (not 95 narrow ones)
token efficiency 75% tokens / 83% bytes saved vs raw output, measured over 79 calls
proxy support pool w/ 5 rotation strategies, CDP auth (any provider), sticky sessions, geo filters
handoff live remote-view via cloudflared Quick Tunnel (works VPS → home laptop)
CDP schema drift resilient — survives Chrome field churn (e.g. dropped sameParty) without hangs

🚀 quick start

git clone https://github.com/GabriWar/umbra.git
cd umbra
pip install -e ".[all]"
python -m umbra.server   # ctrl+c after a few seconds — verify tools register

requirements: Python 3.10+, a Chromium-based browser (Chrome / Chromium / Edge — auto-detected).

extra enables install
(default) core 50 tools, encrypted sessions, proxy pool pip install -e .
[markdown] extract_markdown (readability + markdownify) pip install -e ".[markdown]"
[tls] tls_fetch (curl_cffi w/ Chrome JA3+JA4) pip install -e ".[tls]"
[playwright] optional Playwright backend pip install -e ".[playwright]"
[test] pytest + asyncio for regression suite pip install -e ".[test]"
[all] everything above pip install -e ".[all]"

recommended companion: cloudflaredhandoff_* exposes a live remote-view of the browser via a Cloudflare Quick Tunnel (no signup, no auth). Without it, handoff falls back to localhost-only.

sudo pacman -S cloudflared       # arch / cachyos
sudo apt install cloudflared     # debian / ubuntu
brew install cloudflared         # macos
# else: github.com/cloudflare/cloudflared/releases/latest

🤖 MCP setup

Wire into Claude Code (or any MCP client w/ the same shape):

claude mcp add-json umbra '{
  "type":"stdio",
  "command":"/full/path/to/your/python",
  "args":["-m","umbra.server"],
  "env":{
    "UMBRA_CONTAINER":"1",
    "PYTHONPATH":"/full/path/to/umbra/src"
  }
}'

Restart Claude Code → /mcp shows umbra w/ 77 tools. For Cursor / Claude Desktop / Cline / others, edit their mcp_servers config with the same shape.


🎯 recipes

batch ⭐ flagship — N tools in one MCP round-trip

batch([
  {"tool": "navigate",        "args": {"tab_id": "t0", "url": "https://news.ycombinator.com"}},
  {"tool": "wait_for_text",   "args": {"tab_id": "t0", "text": "Hacker News"}},
  {"tool": "aria_snapshot",   "args": {"tab_id": "t0"}},
  {"tool": "extract_links",   "args": {"tab_id": "t0", "limit": 30}},
  {"tool": "extract_markdown","args": {"tab_id": "t0"}},
])
# → {"results":[...5 entries with ok/data/ms each...],
#    "elapsed_ms":1840, "ok_count":5, "fail_count":0}

Serial in declared order, single MCP round-trip. Saves protocol framing per call AND composes with cross-call dedup (identical re-calls inside the batch return _unchanged_since instead of full payloads). Use it whenever you have ≥2 calls in mind — it's almost always the right choice.

stop_on_error=True short-circuits on first failure (default: keep going + report fail_count).

proxy_pool_* — multi-provider rotation, any provider, any format

5 rotation strategies, rolling health, geo + tag filters, sticky sessions. Plugs into spawn(use_proxy_pool=True) — picks one entry per browser process (Chrome locks proxy per-process; for parallel distinct egress IPs use multiple browser_ids).

input formats — auto-detected, mix-and-match in same load:

# standard URL (auth optional, scheme optional, defaults to http://)
http://user:pass@gateway.provider.com:8080
socks5://1.2.3.4:1080

# provider IP-list export (host:port:user:pass — webshare, IPRoyal, Decodo, ...)
31.59.20.176:6754:user:pass

# sticky-session gateway (one URL, N session-suffixed users)
gw.bright.com:22225:user-session-abc123-country-US:pass

# inline metadata for filtering
http://gw.proxy.com:8080#country=US,tags=residential|sticky

rotation strategiesround_robin (default), random, least_used, best_health, sticky_browser (same browser_id always gets same entry).

creds-stripped flag + CDP auth — Chrome's --proxy-server= silently strips inline creds; umbra feeds Chrome a creds-free URL and answers proxy auth challenges via CDP Fetch.authRequired. Works for HTTP / HTTPS proxies w/ Basic auth — Bright Data, Oxylabs, Smartproxy/Decodo, IPRoyal, SOAX, NetNut, Webshare, ProxyMesh, etc.

SOCKS5 + auth caveat — Chromium has no support for SOCKS5 username/password auth (RFC 1929) — open since 2014, effectively wontfix. CDP Fetch.authRequired is HTTP-layer only; SOCKS5 auth is a TCP-subnegotiation that completes BEFORE any HTTP fires, so the Fetch domain never sees it. Workaround matrix:

transport auth works in umbra?
http://host:port none
http://user:pass@host:port basic ✓ (via CDP)
socks5://host:port none
socks5://user:pass@host:port RFC 1929 ✗ — unfixable in chrome

If u need SOCKS5 + auth, run a local HTTP→SOCKS5 forwarder (gost -L=http://:8080 -F=socks5://user:pass@upstream) and point umbra at the local HTTP port instead.

# MCP usage
proxy_pool_load(data="/path/to/proxies.txt", rotation="round_robin")
proxy_pool_health_check(timeout_s=8.0, parallel=8)   # parallel probe, updates rolling health

spawn(url="https://target.com", browser_id="us-1",
      use_proxy_pool=True, proxy_country="US", proxy_tag="residential")
# → {"tab_id":"t0", "proxy":{"id":"a3b1...", "host":"http://1.2.3.4:8080",
#                            "country":"US", "tags":["residential"], "health":1.0}}

proxy_pool_remove("a3b1...")   # bad rep? drop it

7 MCP tools: proxy_pool_load, proxy_pool_add, proxy_pool_remove, proxy_pool_clear, proxy_pool_list, proxy_pool_health_check, proxy_pool_export.

route through Tor (free, multi-exit, no provider)

Tor's SOCKS5 supports stream isolation — different SOCKS user/pass = different circuit = different exit IP. One tor daemon, N distinct exits, zero provider cost:

sudo systemctl enable --now tor   # binds 127.0.0.1:9050
# ~/.umbra/proxies.txt — each line = one isolated circuit (user/pass arbitrary)
socks5://circ1:x@127.0.0.1:9050#tags=tor
socks5://circ2:x@127.0.0.1:9050#tags=tor
socks5://circ3:x@127.0.0.1:9050#tags=tor
socks5://circ4:x@127.0.0.1:9050#tags=tor
socks5://circ5:x@127.0.0.1:9050#tags=tor
proxy_pool_load(data="~/.umbra/proxies.txt")
spawn(use_proxy_pool=True, proxy_tag="tor")

caveats — Tor exits are publicly listed (check.torproject.org/exit-addresses); most anti-bot stacks (Cloudflare Bot Mgmt, DataDome, Akamai, PerimeterX) blocklist them. Useful for archive sites / IP-leak testing / gov forms. Useless against hardened scraping targets. Slow: ~3–10s per first req per circuit, 1–3s after warm. Pin exit country via ExitNodes {us} in /etc/tor/torrc + systemctl reload tor.

handoff_start — captcha / 2FA wall? hand the wheel back

agent → handoff_start("t0", "solve recaptcha")
         → returns https://random.trycloudflare.com/h-XYZ/
agent → tells user: "open this URL"
user  → opens URL on phone/laptop, sees live page, clicks/types
user  → hits "I'M DONE"
agent → handoff_wait("t0")  blocks until done, returns post-handoff URL+title
agent → continues automation

Built on Cloudflare Quick Tunnels (no signup, instant). URL contains a 192-bit auth token in the path → URL knowledge = auth. Forces HTTP/2 for sustained WebSocket reliability.

extract_markdown — page → clean markdown (firecrawl-style)

extract_markdown('t0')
# → {"_untrusted": True,
#    "title": "Web Scraping - Wikipedia",
#    "markdown": "# Web Scraping\n\nMethod of extracting data...",
#    "source_html_len": 87432}

Mozilla Readability + markdownify. Falls back to <body> for list pages (HN, reddit) where readability gives up.

session_save / session_load — log in once, skip auth forever

session_save('t0', 'github-me', passphrase='hunter2')
# → encrypted blob in ~/.local/share/umbra/sessions/github.com/github-me.fern

# Next time:
session_load('t0', 'github-me', passphrase='hunter2')
# → cookies + localStorage injected, you're logged in

Fernet (AES-128-CBC + HMAC-SHA256) + PBKDF2-HMAC-SHA256 200k iterations. Per-(domain, name) namespace, path-traversal-safe.

tls_fetch — skip the DOM entirely for JSON APIs

tls_fetch('https://api.example.com/users')
# → {"status": 200, "body": "{...}"}

curl_cffi pinned to running Chrome version — JA3+JA4+HTTP/2 SETTINGS frames match Chrome exactly. ~50ms vs ~500ms via spawn+navigate.

multi-browser orchestration

spawn(url='...', browser_id='alice')
spawn(url='...', browser_id='bob')
# alice and bob have fully isolated cookies, profiles, identities
list_browsers()
# → [{"browser_id":"alice","tab_count":3}, {"browser_id":"bob","tab_count":1}]

request interception graph (route_* + HAR record/replay)

Match DSL: url_pattern, url_regex, method, resource_type, header_match, status_min/max. Actions: block (14 custom error_reasons; response-stage block synthesizes 5xx via fulfill), fulfill (status+headers+body|body_b64), continue (request rewrite: new_url/new_method/new_post_data/headers — headers MERGED w/ originals, not replaced), modify (response-stage getResponseBodybody_replace=[[regex,repl],...] or outright body/status/headers override), tee (pure spy: pass-through + capture body), redirect (synth 302 + Location). Per-rule delay_ms (latency injection), times (auto-disable after N hits), priority (higher fires first), capture (cross-stage body buffer for any action), enabled (pause without remove). HAR-1.2 record/replay (loose URL-only mode for query-string drift). Tracker/resource blocking from StealthOptions(block_trackers=, block_resources=) integrated into the same engine — single Fetch handler, no double-fire race. Engine: src/umbra/driver/intercept.py.


🧰 the 77 tools

                  ┌─ browser            spawn / close / list_browsers / close_browser /
                  │                     navigate / list_tabs / switch_tab / back / forward /
                  │                     reload / kill_all
                  │
                  ├─ ARIA               aria_snapshot / aria_click / aria_type / find_by_text
                  │  (zero mouse)       fill_form / current_state
                  │
                  ├─ input (CDP)        click_at / press_key / scroll / drag / hover /
                  │  humanized          paste_text / select_option / wait_for / wait_for_text
                  │
                  ├─ extraction         extract_text / extract_links / grep_text / dom_query /
                  │  _untrusted=true    inspect_element / extract_markdown / clone_element
                  │
                  ├─ visual             screenshot / screenshot_region
                  │
                  ├─ JS                 evaluate / inject_css
                  │
                  ├─ devtools           get_console_logs / get_network_requests / clear_logs /
                  │                     get_response_body / memory_metrics / get_cookies /
                  │                     set_cookies / clear_cookies
                  │
                  ├─ stealth ops        check_detection / warm_session / rotate_fingerprint /
                  │                     set_verbosity
                  │
                  ├─ network ctrl       block_urls / set_extra_headers / set_viewport /
                  │                     dynamic_hook
                  │
                  ├─ interception       route_add / route_add_many / route_remove /
                  │  (full graph)       route_set_enabled / route_block_set / route_list /
                  │                     route_captures / har_record_start / har_record_stop /
                  │                     har_dump / har_clear / har_replay_load
                  │
                  ├─ proxy pool ⭐      proxy_pool_load / proxy_pool_add / proxy_pool_remove /
                  │  multi-provider     proxy_pool_clear / proxy_pool_list /
                  │                     proxy_pool_health_check / proxy_pool_export
                  │
                  ├─ handoff            handoff_start / handoff_wait / request_user_input
                  │  (cloudflared)
                  │
                  ├─ sessions           session_save / session_load / session_list /
                  │  (encrypted)        session_delete
                  │
                  ├─ files              upload_file / setup_downloads / wait_for_download
                  │
                  ├─ TLS                tls_fetch  (raw HTTP w/ Chrome JA3+JA4)
                  │
                  └─ batch ⭐ flagship  batch  (N tools in one round-trip; composes w/ dedup)

🐍 use as a python library

import asyncio
from umbra import stealth_browser

async def main():
    async with stealth_browser(timezone="America/New_York", block_trackers=True) as b:
        tab = await b.new_tab("https://news.ycombinator.com")
        await asyncio.sleep(2)
        await tab.save_screenshot("hn.png")

asyncio.run(main())
# ARIA driver — zero mouse coords
from umbra import stealth_browser, AriaDriver

async with stealth_browser() as b:
    tab = await b.new_tab("https://github.com/login")
    drv = AriaDriver(tab)
    await drv.snapshot()
    print(drv.render_tree())
    # [0] textbox "Login or email"
    # [1] textbox "Password"
    # [2] button "Sign in"
    await drv.type(0, "me@example.com")
    await drv.type(1, "...")
    await drv.click(2)
# Proxy pool — parallel browsers w/ distinct egress IPs
from umbra import StealthBrowser, StealthOptions
from umbra.proxypool import ProxyPool

pool = ProxyPool(rotation="round_robin")
pool.load_lines(open("proxies.txt").read())   # or load_json / load_csv / load_file

async def main():
    for i in range(3):
        b = StealthBrowser(StealthOptions(proxy_pool=pool))
        b._pool_browser_id = f"scraper-{i}"
        async with b:
            tab = await b.new_tab("https://api.ipify.org")
            print(await tab.evaluate("document.body.innerText"))

🔬 token efficiency

Counter-intuitively, umbra costs LESS context than minimal browser-MCPs (incl. playwright-mcp) on any real agent session — its 77-tool catalog adds ~13KB upfront, but per-call savings recover that within 3 calls and dominate after.

measured (79-call e2e session against real Chrome + httpbin, all 77 tools exercised, see tests/test_token_audit.py):

uncompressed (set_verbosity='full') compressed (default) saved
total tokens (cl100k_base) 136,285 33,357 75 %
total bytes (JSON) 420,648 71,282 83 %
median per call 14 tokens / 4 ms

Top per-tool wins: clone_element 97 %, dom_query 52 %, tls_fetch 40 %, proxy_pool_export 45 %. The handful of zero-save tools (screenshot*, aria_snapshot, inspect_element) either ship base64 binaries (incompressible) or are already pre-RLE'd in the driver before _compact sees them.

how the savings happen

Every MCP tool response goes through _compact():

  • drops None only — empty [] / "" / 0 / False KEPT (they're informative)
  • columnar layout for 4+ homogeneous-dict arrays: {"_columnar":true,"keys":[...],"rows":[[...]]} — 44% smaller on real dom_query/cookies/network
  • constant-column hoist — shared values factored to _constant: {col: val}
  • word-boundary string truncation w/ explicit ...[+Nc, raise max_str to see full] marker
  • list truncation w/ {_truncated, shown, total, more_via} marker — caller sees what was cut and how to lift the cap
  • _untrusted: true flag (8 bytes) instead of wrapping content in <external>...</external> tags
  • cross-call dedup ledger — identical repeat calls return {"_unchanged_since": "cN", "_hash": "..."} instead of full payload (force_refresh=True to bypass)
  • ARIA pattern grouping (RLE) — long lists w/ repeating (role,name) cycles collapse to a single [range] cycle×N (period P): ... line. Real HN comments page: ~70% smaller snapshot.
  • URL footnoting in extract_links — repeated hosts factored to _hosts: {h1: "https://..."} then referenced. ~50% smaller on link-heavy pages.

Toggle off via set_verbosity('full') when you need raw byte-exact output. Lossless: zero failures, zero inflations across all 79 audit calls.


🛡️ stealth coverage matrix

detection vector obscura fantoma sb-mcp umbra
canvas / audio / WebGL fp partial ✓ (per-session noise, deterministic w/in session)
navigator.webdriver
cdc_* / _phantom / _selenium n/a ✓ (nodriver) ✓ delete-only (no in operator tell)
event.isTrusted ✓ (no synth events) ✓ (CDP Input.dispatch* only)
mouse / scroll behavioral fp n/a ✓ (ARIA driver default)
keystroke timing fp n/a ✓ key-pair ✗ flat 50ms ✓ key-pair + log-normal jitter
Cloudflare turnstile (passive) partial ✓ (real Chrome)
TLS / JA3 / JA4 ✓ (real Chrome) ✓ + tls_fetch for raw HTTP
WebGL real GPU in headless ✗ no GL ✗ SwiftShader ✓ ANGLE Vulkan
WebRTC outgoing SDP host partial ✓ (mDNS-aware filter, real-Chrome behavior)
UA-CH version mismatch ✓ (dynamic Chrome version + setUserAgentOverride)
iframe + shadow DOM piercing
tracker/fp-script blocking ✓ (3520) ✓ (3520 + dynamic hooks)
session warming (cookie age) ✓ (4 profiles)
live human handoff ✓ (cloudflared tunnel)
proxy auth (CDP, any provider) ✓ + 5-strategy rotation pool
MCP tool surface ✓ (95 narrow) ✓ (77 broad)
prompt-injection signaling ✓ (_untrusted: true on all extraction)

🩹 CDP schema resilience

nodriver's CDP parser hardcodes Chrome protocol field names — when Chrome changes the schema between releases, the parser KeyErrors. Worse, the listener task dies on the unhandled raise → every subsequent CDP call on that tab hangs forever (no awaiter ever wakes up).

umbra ships three monkey-patches in umbra/nodriver_patch.py to make this class of bug impossible:

  1. Transaction.__call__ — every parser exception becomes future.set_exception(...) so the awaiter gets a real error, never a hang.
  2. Connection._listener — wraps the per-message dispatch so a single bad parse can't kill the listener task; future calls keep working.
  3. Cookie.from_json — tolerant of Chrome 146+ dropping sameParty (matches the pattern already used in CookieParam.from_json; upstream inconsistency).

Patches are idempotent (per-class flag + module-level short-circuit, safe to call N times) and partial-failure tolerant (each patch runs in its own try/except — one failing doesn't block the others). Applied automatically at umbra.browser import — zero config.


🏗️ architecture

                  ┌────────────────────────────────────────────┐
                  │  FastMCP server  (umbra.server, 77 tools)  │
                  │  + _compact() minification                 │
                  │  + _untrusted prompt-injection signaling   │
                  │  + cross-call dedup ledger                 │
                  └────────────────────────────────────────────┘
                                       │
       ┌────────────┬──────────────────┼──────────────┬──────────────┐
       ▼            ▼                  ▼              ▼              ▼
  ┌─────────┐  ┌──────────┐      ┌────────────┐  ┌──────────┐  ┌──────────┐
  │ Browser │  │  Drivers │      │  Stealth   │  │  Proxy   │  │  Misc    │
  │  multi  │  │  ARIA    │      │  payload   │  │  pool    │  │  session │
  │  inst.  │  │  CDP     │      │  3520 list │  │  CDP auth│  │  handoff │
  └────┬────┘  │ humanizer│      │  detection │  │  rotation│  │    tls   │
       │       │ intercept│      └─────┬──────┘  └────┬─────┘  └──────────┘
       │       └────┬─────┘            │              │
       ▼            ▼                  ▼              ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │nodriver (real Chrome via CDP) + Page.addScriptToEvaluateOnNewDocument│
  │  --headless=new + --use-angle=vulkan + dynamic UA-CH version pinning │
  └──────────────────────────────────────────────────────────────────────┘
                                       │
                                       ▼
                              ┌─────────────────┐
                              │   real Chrome   │
                              │  146.0.7680.x   │
                              └─────────────────┘

🧪 regression tests

pip install -e ".[test]"
pytest -m e2e -v -s                                      # full e2e
.venv/bin/python tests/test_token_audit.py               # token efficiency audit (79 calls)
UMBRA_PROXY_LIST=/path/to/proxies.txt \                  # opt-in: also exercise proxy pool
  .venv/bin/python tests/test_token_audit.py

Covers bot.sannysoft.com + creepjs + UA-CH consistency + automation-tell checks + (when enabled) end-to-end proxy pool spawn/auth/rotation. Catches drift if Chrome / nodriver update breaks something.


🛠️ roadmap

distribution

  • Submit to Smithery.ai registry — add smithery.yaml + tag a release. Auto-indexes for Claude Desktop / Cursor / Cline users.
  • Add .claude-plugin/plugin.json for Claude Code's plugin marketplace.
  • Submit to Anthropic's official marketplace via claude.ai/settings/plugins/submit.

features

  • Proxy pool rotation — shipped. ProxyPool w/ 5 rotation strategies, rolling health, geo + tag filters, sticky sessions, multi-format loaders (URL, host:port:user:pass, sticky-session gateway, JSON, CSV). CDP Fetch.authRequired handler so creds work on any provider despite Chrome's flag stripping. 7 MCP tools.
  • Full request interception graph — shipped as route_* + har_* (see recipes section).
  • Battle-test the ARIA tree on edge cases — fantoma-derived snapshot covers the 95% case (forms, lists, dialogs, nav) but real-world weirdness still exposes gaps: shadow-DOM-inside-iframe-inside-shadow-DOM, custom elements w/ delegated focus, <canvas>-rendered "trees" (Figma/Notion), virtual-scroll lists where ARIA indexes shift mid-snapshot, aria-owns cross-references, RTL/i18n role inflections. Need a regression corpus (gmail, github, notion, figma, linear, jira, gov forms) + property-based tests.
  • Network API ergonomics — current route_add(...) is declarative; Playwright's route(pattern, async (route, request) => {...}) is callback-based. Add route_handler(tab_id, pattern, js_handler_src) that lets the caller register a JS expression evaluated per paused request — returns {action: 'fulfill'|'continue'|...} per-call. Tradeoffs: sandbox the JS, network round-trip per request (slow), but unbeatable for "fulfill only if request body contains X" / "rewrite based on prior response" / dynamic decisions.
  • HAR tooling polish — current HAR record/replay is HAR-1.2 byte-exact + loose URL-only fallback. Add per-entry matchers (matchUrl(regex), matchPostData(json_path), matchHeaders(...) for query-drift / session-token tolerance), body morphing (updateContent(transform) to mutate a recorded body before serving), strict vs fallback modes, HAR sanitization (strip Authorization/Cookie/Set-Cookie/PII before commit). Unlocks committing HAR fixtures to test repos without leaking secrets.
  • Per-browser exit-node selection via Tailscale — userspace tailscaled per-browser w/ distinct exit nodes for self-hosted residential proxy farms (alternative to paid providers).

📜 license

MIT + Attribution Requirement. Free for any use (commercial, research, hobby) — but if you ship it in a product or publish research using it, please credit:

Powered by [umbra](https://github.com/GabriWar/umbra) by Gabriel Duarte Guerra.

(in your README, docs, about page, or paper acknowledgements — anywhere a human reading your project can see it).

Third-party attributions in LICENSE:

  • stealth/payload.js patterns from h4ckf0r0day/obscura @ 536072b (Apache-2.0)
  • stealth/tracker_domains.txt from obscura (Peter Lowe ad/tracker host file)
  • driver/aria.py + humanizer.py patterns from Huzy85/fantoma @ 86f20eb (MIT)
  • MCP tool surface convention from vibheksoni/stealth-browser-mcp @ def424d (MIT)

About

Stealth Chrome automation MCP server for AI agents. 64 tools, 0/0 creepjs detection, multi-browser, encrypted sessions, cloudflared handoff, markdown extraction.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors