shadow-web

mcp
Security Audit
Warn
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Pass
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Local-first Python SDK for AI agents: Shadow DOM flatten, Action Map, SchemaSnap, form fill, security scan, MCP (64–97% token reduction)

README.md

PyPI version
PyPI downloads
CI
Python
License: MIT
Stars
MCP Badge

Shadow Web

Cut 64–99% of tokens from web pages before your LLM sees them — then extract structured data.
Open-source Python SDK that flattens Shadow DOM, builds a typed Action Map with semantic groups, indexes rendered text on demand, heals broken selectors, and turns HTML tables/forms/lists into clean JSON — no cloud required.

from shadow_web.compressor import process_html

clean_html, actions, groups = process_html(raw_html)
# ✅ actions = [{"id":"1","type":"button","label":"Buy Now","group":"Checkout"}, ...]
# ✅ 164 → 46 tokens on a typical page

from shadow_web.schema_snap import parse_page

data = parse_page(clean_html)
# ✅ tables: [{columns, types, rows, total_rows}]
# ✅ forms:  [{action, method, fields: [{name, type, required, label}]}]
# ✅ lists:  [{type, items, total}]

Pain point

AI agents need to see web pages. But raw HTML is full of <script>, <style>, inline CSS, and interactive elements buried in Shadow DOM trees that Playwright can't reach. A typical Wikipedia page costs 99K tokens raw. Your LLM bill doesn't need that.

Shadow Web is what runs between the browser and the LLM: a compression layer that keeps only what matters — interactive elements, their labels, and a clean DOM skeleton. SchemaSnap then takes that clean HTML and turns it into structured data agents can actually use: table rows, form fields with validation, list items.


What you get

Feature Raw HTML Playwright locators Shadow Web
Token cost (Wikipedia) 99,343 16,462 (−83%)
Token cost (GitHub Trending) 167,875 37,833 (−77%)
Long-form via Content Index full page ~600t outline → fetch blocks
Shadow DOM readable ❌ partial ✅ flattened
Semantic groups ✅ Login / Cart / Nav
Self-healing selectors ✅ local + LLM fallback
Consent + lazy hydration manual ✅ readiness + scroll-until-content
Catalog / feed outline ranking ✅ cards / feeds before chrome
Tables → JSON columns+rows ✅ SchemaSnap
Forms → fields with validation ✅ SchemaSnap
Lists → typed items ✅ SchemaSnap
Works offline
PyPI package playwright shadow-web

Who this is for

You're building … Why Shadow Web
A browser-based AI agent Action Map + self-healing + SchemaSnap = fewer failures, structured data
An MCP tool for Cursor/Claude Built-in MCP server with 26 tools, one-command setup
A Playwright scraper that breaks on every deploy heal_local.py catches DOM drift without LLM cost
A Shadow DOM-heavy app (Web components, Lit, Angular) Read-only flatten — no React/Vue breakage
An agent that needs data from web pages SchemaSnap parses tables, forms, and lists into clean JSON
Long articles, catalogs, social feeds Rendered Text Index + ranked content_outline (cards / feeds)
Attack surface / security recon security_scan.py + CLI — forms, links, headers, cookies, page_class (not pentest)
Competitor monitoring & SEO content Shallow multi-site scans with token-bounded Action Map
SaaS onboarding automation AgentOps form fill — schema knows fields, LLM only picks values

Quick install

pip install shadow-web
playwright install chromium

Extras:

pip install "shadow-web[mcp]"          # Cursor/Claude MCP server
pip install "shadow-web[server]"        # FastAPI heal API
pip install "shadow-web[all]"           # everything

Run the optional API locally:

export DEEPSEEK_API_KEY="..."
export SHADOW_WEB_API_KEYS="local-secret"
shadow-web-server

Remote and production requests are rejected unless SHADOW_WEB_API_KEYS is configured.


Demo

Golden path (recommended — run locally)

Full agent loop with token counts at each step:

pip install -e ".[mcp]"
playwright install chromium
python examples/golden_path/demo.py

Output: raw HTML vs navigate(minimal) + schema_session_json + shadow_query — side-by-side token table.
Playbook: examples/golden_path/CASE.md

Attack surface security scan

Automated surface mapping (not penetration testing): forms, links, page_class, HTTP security headers (HSTS, CSP, XFO, nosniff, CORS), cookie flags (Secure, HttpOnly, SameSite), Markdown/JSON reports.

pip install -e ".[mcp]"
playwright install chromium

# Single URL
python scripts/security_surface_scan.py https://example.com

# Shallow same-domain crawl + reports (--no-headers / --no-cookies to skip checks)
python scripts/security_surface_scan.py https://yoursite.com \
  --crawl-depth 1 --max-pages 20 \
  --json report.json --markdown report.md

Rule engine (importable without browser):

from shadow_web.security_scan import analyze_surface, render_markdown_report

result = analyze_surface(
    "https://app.example.com/login",
    clean_html=html,
    action_map=actions,
    page_class="Static",
)
# findings: FORM_PASSWORD_GET, HEADER_MISSING_CSP, COOKIE_MISSING_HTTPONLY, ...

Example output: examples/security_scan/localpdf-full-report.md (20 pages, 0 critical/high on public marketing layer). Header-only sample: localpdf-headers-only.json.

Does not test: XSS, SQLi, auth bypass, or deep TLS/cipher analysis. Use only on authorized targets.

AgentOps form fill

See the full Form Fill section below. Quick start:

python scripts/form_fill_demo.py https://httpbin.org/forms/post \
  --profile examples/form_fill/profile.json --json plan.json

Competitor intelligence scan

Token-bounded weekly audit for programmatic SEO / compare pages:

python scripts/localpdf_competitor_scan.py --json reports/scan.json

Playbook: examples/localpdf/CASE.md

Smoke test (install + unit tests + one live site):

bash scripts/smoke_install.sh

Compress a page (3 lines)

from shadow_web.compressor import process_html, generate_grouped_xml_map

clean_html, actions, groups = process_html(open("page.html").read())
xml_map = generate_grouped_xml_map("https://example.com", "Example", groups)
print(xml_map)

Playwright + Shadow DOM flatten

from playwright.sync_api import sync_playwright
from shadow_web.wrapper import ShadowPage

with sync_playwright() as p:
    page = p.chromium.launch(headless=True).new_page()
    page.goto("https://example.com")
    shadow = ShadowPage(page)
    _, xml_map = shadow.refresh()
    print(shadow.capture_stats)  # shadow_hosts, iframes, a11y supplement

shadow_grep — send only what the LLM needs

result = shadow.query("intent:login", fmt="terse")
# @1 button Sign in [Login Form]
# @2 input[email] Email [Login Form]

Stream a delta after clicking

shadow.refresh()               # full baseline
shadow.click("3")              # navigate
_, delta_xml = shadow.refresh(diff=True)  # only what changed

SchemaSnap — extract tables, forms, and lists

from shadow_web.schema_snap import parse_page, parse_tables

# Parse everything from a page
data = parse_page(raw_html)
# {
#   "tables": [
#     {
#       "columns": ["Product", "Price", "Stock"],
#       "types": ["string", "currency", "integer"],
#       "rows": [["Widget A", "$19.99", "150"], ...],
#       "total_rows": 12,
#       "column_count": 3
#     }
#   ],
#   "forms": [
#     {
#       "action": "/checkout",
#       "method": "POST",
#       "fields": [
#         {"tag": "input", "type": "email", "name": "email",
#          "required": true, "label": "Email Address"},
#         {"tag": "select", "name": "country", "options": [
#           {"value": "US", "label": "United States"}, ...]}
#       ]
#     }
#   ],
#   "lists": [
#     {"type": "unordered", "items": ["Apples", "Bananas"], "total": 2}
#   ]
# }

# Or export table rows directly
from shadow_web.schema_snap import export_table_json, export_table_csv

records = export_table_json(clean_html, max_rows=50)
# [{"Name": "Alice", "Age": 30}, ...]

csv_text = export_table_csv(clean_html)
# "Name,Age\nAlice,30\n..."

SchemaSnap in MCP

From HTML string (no browser):

Tool Output
schema_table(html) columns + types + rows
schema_form(html) fields + validation
schema_list(html) ul/ol/standalone select
schema_page(html) all of the above
schema_json(html) [{column: value}, ...]
schema_csv(html) "col1,col2\n..."

From browser session (after navigate / snapshot):

Tool Output
schema_session() tables + forms + lists
schema_session_json() JSON records
schema_session_csv() CSV string
get_page_html(max_chars=50000) clean HTML (truncated by default)

All table tools accept max_rows=50 (default). Set max_rows=0 for full data.


How it works

Browser (live DOM)
    │
    ├─ capture_ready — cookie consent dismiss + wait + scroll-until-content
    │              ↓
    ├─ [default] DOM capture — flatten Shadow DOM + same-origin iframes (read-only)
    │              ↓
    ├─ [optional] a11y CDP supplement — catch closed Shadow DOM elements
    │              ↓
    ├─ [Chrome 145+] WebMCP bridge — page exposes document.modelContext.getTools()
    │
    └─→ compressor.py → Action Map (data-sid, type, label, group)
                          ↓
                    content_index.py → ranked outline (p0, p1…; cards/feeds) + fetch
                          ↓
                    query.py (shadow_grep) → filter Action Map before LLM
                          ↓
                    schema_snap.py → structured data (tables, forms, lists)
                          ↓
                    security_scan.py → attack surface rules (forms, links, headers, cookies)
                          ↓
                    heal_local.py → fuzzy selector recovery (no LLM)
                          ↓
                    FastAPI /v1/heal → LLM fallback + verification

No live DOM mutation. Shadow Web reads your page; it never writes back. React/Vue/Svelte listeners stay intact.


Architecture

shadow_web/
├── compressor.py      # DOM strip + Action Map + semantic groups
├── content_index.py   # Rendered Text Index — outline + on-demand fetch
├── capture_ready.py   # Consent dismiss + readiness wait + scroll-until-content
├── dom_capture.py     # Shadow DOM / iframe flatten (in-browser, read-only)
├── grouping.py        # Semantic groups (forms, nav, modals)
├── schema_snap.py     # Tables, forms, lists → JSON/CSV export
├── form_fill.py       # AgentOps form fill (auto_fill / ask / handoff)
├── security_scan.py   # Attack surface rules (forms, links, headers, cookies)
├── heal_local.py      # Local selector heal + ~/.shadow-web/heal_cache.json
├── query.py           # shadow_grep (type:, intent:, label~, AND)
├── webmcp.py          # WebMCP bridge (Chrome 145+)
├── diff.py            # Page diff (skeleton + delta XML)
├── a11y_capture.py    # CDP Accessibility dual capture + page_class
├── verified_heal.py   # Playwright selector verification
├── wrapper.py         # ShadowPage (sync Playwright)
├── browser_use.py     # AsyncShadowPage (browser-use / async Playwright)
├── mcp/server.py      # Cursor / Claude MCP tools (26)
└── server.py          # FastAPI (/health, /v1/compress, /v1/heal)

scripts/
├── security_surface_scan.py   # CLI: crawl + JSON/Markdown security reports
├── form_fill_demo.py          # AgentOps form fill demo (plan + execute + wizard)
├── localpdf_competitor_scan.py # Multi-site competitor snapshot
├── smoke_install.sh           # Install + pytest + one live navigate
└── cursor-setup.sh            # MCP one-command setup

deploy/
└── oci.sh                     # Oracle Cloud deployment script

Examples

Path What it demonstrates
examples/golden_path/ Full agent loop + token budget
examples/security_scan/ Attack surface scan reports (JSON + Markdown)
examples/localpdf/ Competitor intel playbook + sample scan
examples/form_fill/ AgentOps form fill playbook + profile sample
examples/browser_use/ browser-use integration

Benchmarks

Page Raw HTML (tokens) Grouped XML (tokens) Actions Reduction
Hacker News 8,637 6,704 227 −22% (1.3×)
Wikipedia (Web Scraping) 99,343 16,462 501 −83% (6.0×)
Wikipedia + Content Index 57,778 599¹ −99% (96×)
GitHub Trending 167,875 37,833 1,290 −77% (4.4×)

¹ content_outline(max_tokens=600) — 141 blocks, agent picks what to fetch.
Full benchmark: python benchmarks/run.py


MCP for Cursor / Claude

One-command setup:

bash scripts/cursor-setup.sh

Or manually:

{
  "mcpServers": {
    "shadow-web": {
      "command": "shadow-web-mcp"
    }
  }
}

All 26 tools

Category Tool What it does
Browse navigate Open URL → snapshot (detail: minimal / terse / xml / full)
snapshot Refresh page; diff=true for delta only
click, fill Interact by data-sid
AgentOps form_fill_plan Profile JSON → fill plan (auto_fill / ask / handoff)
form_fill_execute Run plan (+ answers); validation_error feedback
Filter (control plane) shadow_query grep-style filter on live session
query_page Alias for shadow_query (json output)
shadow_grep_html Filter raw/clean HTML without browser
Compress (offline) compress_html Strip + Action Map + groups
compress_html_to_xml Grouped XML from HTML
Data (SchemaSnap) schema_table Table columns + types + rows
schema_form Form fields + validation
schema_list Lists + standalone selects
schema_page All structured data at once
schema_json Table → JSON records
schema_csv Table → CSV string
schema_session Structured data from browser session
schema_session_json JSON records from session
schema_session_csv CSV from session
get_page_html Clean HTML (max_chars default 50000)
Content content_outline Ranked rendered-text index (cards / feeds + coverage)
content_blocks Fetch selected text blocks by compact IDs
Search web_search Brave Search (no API keys)
WebMCP webmcp_list_tools Chrome 145+ page tools
webmcp_execute_tool Execute WebMCP tool by name

Recommended MCP workflow

Browse + extract:

navigate(url, detail="minimal")     # ~200 tokens — action_count, page_class
schema_session_json(max_rows=50)    # data plane — table records
shadow_query("intent:login")        # control plane — what to click
click(sid) → snapshot(diff=true)    # delta only after action

Long-form content:

navigate(url, detail="minimal")
content_outline(max_tokens=600)              # p0, p1… + coverage/mode/signals/cards|feeds
content_blocks(ids="p3,p7", max_tokens=2000) # selected text only

The Rendered Text Index covers semantic articles and div/span-heavy apps
without site-specific selectors. Every readable text node belongs to one
bounded block; headings and paragraphs act as boundary signals rather than an
allowlist. Before the first capture, readiness dismisses cookie consent and
scrolls under-hydrated first paints until content appears. On catalog pages,
priced cards are ranked into the first outline page; on feed-like pages,
repeating mid-size items are ranked without requiring prices. The summary
reports coverage, extraction mode, retained price signals, cards, and
feeds so a large reduction cannot hide missing content.

Form fill (AgentOps):

form_fill_plan(profile='{"email":"...","name":"..."}', url="https://...")
form_fill_execute(plan='...', answers='{}')

See examples/golden_path/CASE.md for the full playbook.


browser-use Integration

Shadow Web provides out-of-the-box integration with browser-use (the popular agentic framework). It drops token usage by up to 90% and allows the agent to interact with elements inside Shadow DOM and iframes using a single line setup.

pip install "shadow-web[browser-use]"
from browser_use import Agent
from shadow_web import ShadowTools

# Default format is "terse" (compact). Use format="xml" in get_xml_action_map when needed.
tools = ShadowTools(
    heal_api_url="http://localhost:8000/v1/heal",  # Optional: LLM fallback self-healing API
)

agent = Agent(task="...", llm=llm, tools=tools)

get_xml_action_map also accepts query (e.g. intent:login) and format (terse | xml) per call.

See examples/browser_use/ for a complete working implementation.


Self-healing chain

click("3") → binding path → element not found?
    ↓
local heal (fuzzy label + stable attr match, 85% threshold) → no LLM, no cost
    ↓
LLM heal (DeepSeek / OpenAI via /v1/heal) → generates candidate selector
    ↓
selector verified in headless Chromium → cached to ~/.shadow-web/heal_cache.json

SchemaSnap — token-aware data extraction

SchemaSnap is the data plane complement to the Action Map control plane:

Layer Question Tools
Control What can I click? navigate, shadow_query, click, fill
Data What data is on the page? schema_session_json, schema_session, schema_csv

Default max_rows=50 per table. Set max_rows=0 for full export when needed.

Type inference: string, integer, number, currency, percentage, date, email, url.


AgentOps Form Fill

Give a URL + JSON profile. Shadow Web reads the form schema, fills safe fields, asks on ambiguity, and hands off on blockers — without sending 50k tokens of HTML to your LLM.

Classic browser agents: entire page → LLM guesses selectors → silent validation failures.
Shadow Web AgentOps split:

Data plane:   schema_form  → { name, type, required, minlength, options }
Control plane: Action Map → fill(sid), click(sid)
LLM job:      profile → values only (not “where to click”)

Execution modes

Mode When What happens Example
auto_fill Safe, typed profile fields fill(sid, value) immediately email, name, company, role, phone
ask Ambiguous or missing data Structured question returned in plan.questions textarea bio, <select> country, “How did you hear about us?”
handoff Human-only UI Stop with status: handoff — no blind retries CAPTCHA, OAuth, file upload, custom datepicker, page_class: Anti-bot

Design principle: never promise full autonomy. Pilots survive because blockers surface early instead of failing silently.

Status codes

status Meaning
completed Auto-fills ran (+ optional submit click)
questions_pending Operator must answer questions[] then re-run with answers
handoff Human step required (blockers[])
validation_error Client validation failed after fill — see errors[] with sid + field

Validation feedback loop

After every fill():

  1. snapshot(diff=true) — catch appeared error messages
  2. HTML5 checkValidity() on live DOM
  3. Preflight checks (e.g. email format before fill)
{
  "status": "validation_error",
  "errors": [{
    "sid": "2",
    "field": {"name": "email", "kind": "email"},
    "message": "Please include an '@' in the email address",
    "source": "constraint_validation"
  }]
}

Quick start (Python)

from shadow_web.form_fill import (
    validate_profile,
    plan_from_session,
    apply_question_answers,
    execute_form_fill_plan_async,
    execute_form_fill_plan_multi_step_async,
)

validation = validate_profile(profile)  # warns: unknown keys like "companny"
plan = plan_from_session(
    url=shadow.last_url,
    clean_html=shadow.clean_html,
    action_map=shadow.action_map,
    profile=profile,
    page_class=shadow.page_class,
)

result = await execute_form_fill_plan_async(shadow, plan, validate=True)

# Multi-step wizard (enterprise onboarding)
result = await execute_form_fill_plan_multi_step_async(
    shadow, profile, max_steps=3, answers={"form0_field4": "Senior engineer"}
)

CLI demo

pip install -e ".[mcp]"
playwright install chromium

# Plan only
python scripts/form_fill_demo.py https://httpbin.org/forms/post \
  --profile examples/form_fill/profile.json --json plan.json

# Execute safe fills
python scripts/form_fill_demo.py URL --profile examples/form_fill/profile.json --execute

# Multi-step wizard
python scripts/form_fill_demo.py URL --profile profile.json --execute --multi-step

MCP workflow

form_fill_plan(profile='{"email":"[email protected]","name":"Ada","company":"Acme"}', url="https://...")
form_fill_execute(plan='...', answers='{"form0_field4":"..."}')
# Wizard: form_fill_execute(profile='...', multi_step=true, max_steps=3)

Playbook: examples/form_fill/CASE.md


Known limitations

Limitation Workaround
Anti-bot / Cloudflare headless page_class: Anti-bot — stop, don't retry; use headed browser or manual step
Cookie / empty content shell page_class: SparseShell — readiness + scroll did not hydrate; don't index blindly
colspan / rowspan tables Column alignment may drift; verify row shape
JS-rendered grids (AG Grid, React Table) May not use <table> — use shadow_query + Action Map instead
Closed Shadow DOM navigate(..., capture_mode="dual") or "a11y"
Cross-origin iframes Not accessible — page_class: Iframe-heavy
Token bombs Never default to detail="full", get_page_html(max_chars=0), or max_rows=0 unless debugging
Security scan scope Surface mapping — forms, links, HTTP headers, cookie flags; no XSS/SQLi; SPAs may need capture_mode=dual
Security scan authorization Run only on systems you own or have explicit permission to test
Form fill custom widgets MUI Select, date pickers → handoff; use capture_mode=dual for Shadow DOM forms

When NOT to use Shadow Web

  • You need one document.querySelector — use Playwright directly.
  • You're building a static site scraper with no interaction.
  • The page is plain HTML with no Shadow DOM — overhead isn't worth it.

License

MIT. Free for anything.


Stars are the oxygen of open-source. If Shadow Web saved you tokens or debugging time, ★ the repo.

Reviews (0)

No results found