You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c1ff100
Browse filesBrowse the repository at this point in the historyBrowse files
- Tool count 64 → 65 (everywhere: intro, in-numbers, install verify,
tool tree heading, comparison matrix, architecture box).
- batch ⭐ added to tool tree as flagship category + new highlight
section with a 5-call HN example and stop_on_error note.
- 🔬 token-efficiency expanded with three v0.5 mechanisms that were
shipped but undocumented:
• cross-call dedup ledger ({_unchanged_since, _hash}) w/ before-
after example
• ARIA pattern grouping (lossless RLE for repeating snapshots,
~70% smaller on HN comments)
• URL footnoting (host dedup in extract_links via _hosts table,
~50% smaller on link-heavy pages)
- 🩹 New CDP schema resilience section explaining the three nodriver
monkey-patches and their idempotence + partial-failure guarantees.
- in-numbers row added: "CDP schema drift — resilient".
- TODO (features) gains two playwright-parity items:
• full request interception graph (Fetch.requestPaused + match DSL)
• battle-test ARIA tree on real-world edges (regression corpus)
Copy file name to clipboardExpand all lines: README.md
+81-9Lines changed: 81 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,7 +5,7 @@
5
5
# umbra
6
6
7
7
> **The de-facto MCP server for stealth browser automation.**
8
-
> Real Chrome, 0% creepjs detection, 31/31 sannysoft, 64 broad tools, multi-browser orchestration, encrypted sessions, prompt-injection signaling, and live human handoff over a Cloudflare tunnel — for AI agents that need to browse the web like a human, not a bot.
8
+
> Real Chrome, 0% creepjs detection, 31/31 sannysoft, 65 broad tools, multi-browser orchestration, encrypted sessions, prompt-injection signaling, and live human handoff over a Cloudflare tunnel — for AI agents that need to browse the web like a human, not a bot.
9
9
10
10
> *umbra — the darkest part of a shadow, where light is fully blocked.*
11
11
@@ -15,7 +15,7 @@ Built by merging the best parts of [obscura](https://github.com/h4ckf0r0day/obsc
15
15
16
16
## 🪙 token efficiency
17
17
18
-
Counter-intuitively, umbra costs LESS context than minimal browser-MCPs (incl. playwright-mcp) on any real agent session — its 64-tool catalog adds ~13KB upfront, but per-call savings recover that within 3 calls and dominate after that.
18
+
Counter-intuitively, umbra costs LESS context than minimal browser-MCPs (incl. playwright-mcp) on any real agent session — its 65-tool catalog adds ~13KB upfront, but per-call savings recover that within 3 calls and dominate after that.
|**headless**| real GPU via `--headless=new + ANGLE Vulkan` (no SwiftShader tell) |
46
46
|**TLS / JA3**| real Chrome stack + optional `curl_cffi` for raw HTTP |
47
47
|**WebRTC**| mDNS-aware SDP filter (real-Chrome behavior, no LAN IP leak) |
48
48
|**handoff**| live remote-view via `cloudflared` Quick Tunnel (works VPS → home laptop) |
49
+
|**CDP schema drift**| resilient — survives Chrome field churn (e.g. dropped `sameParty`) without hangs |
49
50
50
51
---
51
52
@@ -103,7 +104,7 @@ claude mcp add-json umbra '{
103
104
}'
104
105
```
105
106
106
-
Then restart Claude Code → `/mcp` should show `umbra` with 64 tools.
107
+
Then restart Claude Code → `/mcp` should show `umbra` with 65 tools.
107
108
108
109
For Cursor / Claude Desktop / other MCP clients, edit their `mcp_servers` config with the same shape.
109
110
@@ -124,6 +125,10 @@ pytest -m e2e -v -s # full regression suite (boots real Chrome, ~60s)
124
125
125
126
-[ ]**Proxy pool rotation** — currently `StealthOptions(proxy="...")` accepts one proxy per session. For high-volume scraping or geo-distributed scraping, add a `proxy_pool=[...]` option that round-robins (or rotates per-tab / per-N-requests / on-403). Pair w/ residential providers (smartproxy, iproyale, brightdata) for IP reputation. ~80 LoC + a per-tab proxy override via CDP `Network.setExtraHTTPHeaders` + `--proxy-server` per browser instance.
126
127
128
+
-[ ]**Full request interception graph** — current `block_urls` + `dynamic_hook` cover block/header-injection patterns, but playwright-mcp wins on deep interception: per-request `route()` w/ `fulfill / continue / abort`, body rewrite, response stubbing, HAR replay, conditional-on-headers matching. Build via CDP `Fetch.enable + Fetch.requestPaused` (already wired for hooks) + a richer match DSL — `route(pattern, handler)` returning `{action, status, body, headers, delay_ms}`. Unlocks offline replay + auth-token swap + chaos testing.
129
+
130
+
-[ ]**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 so we don't regress as nodriver/Chrome update. Playwright's accessibility tree has a decade of these baked in — ours is ~6 months.
131
+
127
132
---
128
133
129
134
## 🤖 use as an MCP server (the main use case)
@@ -137,11 +142,11 @@ claude mcp add-json umbra '{
137
142
}'
138
143
```
139
144
140
-
Now your agent has 64 MCP tools for stealth Chrome automation. Cursor, Claude Desktop, Claude Code — anything MCP.
145
+
Now your agent has 65 MCP tools for stealth Chrome automation. Cursor, Claude Desktop, Claude Code — anything MCP.
141
146
142
147
---
143
148
144
-
## 🧰 the 64 tools
149
+
## 🧰 the 65 tools
145
150
146
151
```
147
152
┌─ browser spawn / close / list_browsers / close_browser /
@@ -179,13 +184,33 @@ Now your agent has 64 MCP tools for stealth Chrome automation. Cursor, Claude De
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.
211
+
212
+
`stop_on_error=True` short-circuits the batch on first failure (default: keep going + report fail_count).
213
+
189
214
### `handoff_start` — when you hit a captcha, hand the wheel back
| prompt-injection signaling | ✗ | ✗ | ✗ | ✓ (`_untrusted: true` on all extraction) |
273
298
274
299
---
@@ -319,13 +344,60 @@ every MCP tool response goes through `_compact()`:
319
344
320
345
**24% average wire-byte savings** on real-world pages (test data on HN/wikipedia). flip with `set_verbosity('full')` when you need raw.
321
346
347
+
### cross-call dedup ledger
348
+
349
+
Identical repeat calls return `{"_unchanged_since": "cN", "_hash": "..."}` instead of the full payload — the data is unchanged from call cN, so the agent reuses what it already has in context. Pass `force_refresh=True` to bypass.
350
+
351
+
```python
352
+
extract_text('t0') # → call c5: full {text:"...",length:8421,...}
~50% smaller on link-heavy pages. Reconstruct via `_hosts[h] + path`.
379
+
380
+
---
381
+
382
+
## 🩹 CDP schema resilience
383
+
384
+
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).
385
+
386
+
umbra ships three monkey-patches in `umbra/nodriver_patch.py` to make this class of bug impossible:
387
+
388
+
1.**`Transaction.__call__`** — every parser exception becomes `future.set_exception(...)` so the awaiter gets a real error, never a hang.
389
+
2.**`Connection._listener`** — wraps the per-message dispatch so a single bad parse can't kill the listener task; future calls keep working.
390
+
3.**`Cookie.from_json`** — tolerant of Chrome 146+ dropping `sameParty` (matches the pattern already used in `CookieParam.from_json`; upstream inconsistency).
391
+
392
+
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.
0 commit comments