# Web Speed Web Speed solves the **Signal-to-Noise problem** for AI agents. While the modern web is optimized for human eyes (messy HTML, complex layouts, JS-heavy interfaces), Web Speed translates that chaos into a deterministic, token-efficient structural map designed for high-throughput agentic fleets. **No AI inside.** No `anthropic`, no `openai`, no LLM dependency of any kind. All interpretation lives in the calling agent. > We also recommend setting up a master instructional file for your LLM of choice (gemini.md, "Instructions for Claude", etc.). A sample doc can be found **[here](docs/instructions.md)** --- ## Why it exists | Problem | Web Speed solution | |---|---| | Raw HTML is 150,000+ chars of scripts, styles, and SVG noise | Strips everything non-structural → **up to 97% token reduction** | | LLMs hallucinate element IDs and miss interaction points in raw DOM | Returns a **frozen structural map** — what's there is there, nothing invented | | Custom scrapers break per-site | **Deterministic protocol** — the same JSON shape for every site on the web | | Agent has to re-discover pages one round-trip at a time | `site_map` crawls an entire domain in one call | --- ## Tools | Tool | Description | |---|---| | `interpret_page` | Full structured map: headings, navigation, content links, forms, tables, text, metadata | | `submit_form` | Submit a form (GET or POST), get back the resulting page's map | | `site_map` | Crawl from a root URL, return a combined map of all pages | | `inspect_element` | Deep structural data for nodes matching a CSS selector | | `page_type` | Instant page classification — `login`, `listing`, `article`, `form`, `navigation`, `other` | | `invalidate_cache` | Drop a cached map so the next call fetches fresh | --- ## Install ### Mac / Linux ```bash cd web-interpreter python3 -m venv venv source venv/bin/activate pip install -r requirements.txt ``` ### Windows ```powershell cd web-interpreter python -m venv venv venv\Scripts\activate pip install -r requirements.txt ``` --- ## Run For local development with the MCP inspector: ```bash mcp dev server.py ``` To run directly over stdio (how MCP clients launch it): ```bash python server.py ``` --- ## Register with Claude / Cowork Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac) or the equivalent on Windows: ```json { "mcpServers": { "web-speed": { "command": "/absolute/path/to/web-interpreter/venv/bin/python", "args": ["/absolute/path/to/web-interpreter/server.py"] } } } ``` Then quit and relaunch Claude Desktop / Cowork. The six tools will appear under the `web-speed` MCP server. --- ## Output schemas ### `interpret_page` ```json { "url": "https://example.com/", "fetched_at": "2025-01-01T12:00:00Z", "page_type": "other", "title": "Example Domain", "description": "", "headings": [ { "level": 1, "text": "Example Domain" } ], "navigation": [ { "label": "Home", "url": "https://example.com/", "location": "header" } ], "content_links": { "total": 47, "truncated": false, "items": [ { "label": "More information...", "url": "https://www.iana.org/domains/example" } ] }, "forms": [ { "id": "search", "action": "https://example.com/search", "method": "GET", "fields": [ { "name": "q", "type": "text", "label": "Search", "placeholder": "Search...", "required": false, "value": "" }, { "name": "_csrf", "type": "hidden", "label": "", "placeholder": "", "required": false, "value": "abc123" } ] } ], "tables": [ { "id": "results", "headers": ["Name", "Price", "Stock"], "rows": [["Widget A", "$9.99", "In stock"]] } ], "text_blocks": [ { "tag": "p", "text": "This domain is for use in illustrative examples." } ], "metadata": { "lang": "en", "canonical": "", "open_graph": { "title": "", "description": "", "image": "" } } } ``` **Key fields:** - **`navigation`** — links inside semantic nav/header/footer elements (site chrome, menus). Capped at 60. - **`content_links`** — links inside the page body (articles, search results, listings). Always includes `total` so you know the real count even when truncated at 60. - **`forms`** — every form with every field, CSRF tokens preserved verbatim in hidden-field `value`. - **`page_type`** — inferred from structure: password field → `login`, many items/links → `listing`, `
` with paragraphs → `article`, forms → `form`, mostly links → `navigation`. --- ### `page_type` Lightweight — returns just the classification. Instant when the page is cached. ```json { "url": "https://example.com/login", "fetched_at": "2025-01-01T12:00:00Z", "page_type": "login", "title": "Sign In" } ``` --- ### `submit_form` Same output shape as `interpret_page`, for the page the server lands on after submission. ```json { "url": "https://example.com/login", "method": "POST", "fields": { "email": "user@example.com", "password": "hunter2", "_csrf": "abc123" } } ``` CSRF tokens go in `fields` verbatim — pull them from the hidden fields in the `forms` array of the prior `interpret_page` call. --- ### `inspect_element` Deep structural data for nodes matching a CSS selector. Capped at 25 elements. ```json { "url": "https://example.com/shop", "selector": ".product-card", "matched": 48, "truncated": true, "elements": [ { "tag": "div", "id": "product-42", "classes": ["product-card", "featured"], "text": "Widget Pro $49.99 Add to cart", "attributes": { "id": "product-42" }, "links": [{ "label": "Add to cart", "url": "https://example.com/cart/add/42" }], "fields": [], "children": [ { "tag": "h3", "text": "Widget Pro" }, { "tag": "span", "text": "$49.99" }, { "tag": "a", "text": "Add to cart", "href": "https://example.com/cart/add/42" } ] } ] } ``` Example selectors: `#login-form`, `.product-card`, `table.results tbody tr`, `nav a`, `[data-testid="price"]` --- ### `site_map` ```json { "root_url": "https://example.com", "crawled_at": "2025-01-01T12:00:00Z", "total_pages": 8, "pages": [ { "url": "https://example.com", "title": "Home", "page_type": "navigation", "depth": 0, "links_to": ["https://example.com/about", "https://example.com/contact"] } ], "all_forms": [ { "found_on": "https://example.com/contact", "id": "contact", "action": "https://example.com/contact/submit", "method": "POST", "fields": [ { "name": "email", "type": "email", "label": "Your email", "placeholder": "", "required": true, "value": "" }, { "name": "message", "type": "textarea", "label": "Message", "placeholder": "", "required": true, "value": "" } ] } ], "all_navigation": [ { "label": "About", "url": "https://example.com/about" }, { "label": "Contact", "url": "https://example.com/contact" } ] } ``` --- ### `invalidate_cache` ```json { "url": "https://example.com", "invalidated": true } ``` --- ### Errors Tools never raise. On failure: ```json { "error": true, "code": "FETCH_FAILED | PARSE_FAILED | TIMEOUT | NOT_HTML", "message": "human-readable explanation", "url": "https://example.com/broken" } ``` --- ## How an agent should use the output **Navigate a site:** Read `navigation` for site chrome (menus, header, footer) and `content_links` for page body links. `content_links.total` tells you how many exist even if the list is truncated. Pick the link matching your goal and call `interpret_page`. **Submit a form:** Read `forms`. Each field has `name` (what to send), `type` (what data it expects), `label`/`placeholder` (what it's for), `required`, and `value`. Hidden fields (`type: "hidden"`) carry CSRF tokens — pass their `value` back verbatim. Build a flat `name → value` dict and call `submit_form`. **Classify before committing:** Call `page_type` first when you need to branch logic (e.g., is this a login page or a dashboard?) without paying for a full `interpret_page`. **Drill into a component:** Saw a table in the map but want the individual rows? A product listing but want each card's link and price? Call `inspect_element` with a CSS selector to get structured detail on those specific nodes without re-loading the whole page. **Pre-plan a multi-step workflow:** Call `site_map` before you start. You get every page's title, type, depth, and outgoing links, plus every form across the site — you can plan the whole workflow (find the login form, find the data entry page, find the submit endpoint) without a single round-trip. **`page_type` is a signal, not a guarantee:** The classification is heuristic. JS-rendered SPAs that serve empty HTML shells will often be `other` — the password field isn't in the HTML until JavaScript runs. Treat `page_type` as a fast filter, then verify against the actual `forms` and `headings`. --- ## Shared registry sync By default, every fresh page map your OSS server builds is asynchronously contributed to the Web Speed shared registry at `api.getwebspeed.io`. This is the crowdsourced flywheel — every agent that fetches a URL adds it to the global cache, so the next agent anywhere gets an instant response. **This is opt-out, not opt-in.** The default is on because the more contributors, the faster everyone's agents run. ### What is shared Structural page data only: - Page type, title, description - Headings, navigation links, content links - Form field names, types, and labels (no values) - Tables, text blocks - Open Graph metadata **Never shared:** cookies, session tokens, form values, JS-rendered maps (which may contain session-specific login state). ### Disabling sync Set the environment variable before starting the server: ```bash WEB_SPEED_REGISTRY_SYNC=false python server.py ``` Or in your MCP client config: ```json { "mcpServers": { "web-speed": { "command": "/path/to/venv/bin/python", "args": ["/path/to/server.py"], "env": { "WEB_SPEED_REGISTRY_SYNC": "false" } } } } ``` ### Pointing at a self-hosted registry If you're running your own hosted instance, point the sync there: ```bash WEB_SPEED_REGISTRY_URL=https://your-instance.example.com python server.py ``` ### Sync behaviour - Fire-and-forget: the contribution is sent in the background. Your agent's request completes at full speed regardless of whether the ping succeeds. - Only on cache MISS: maps already in your local 24h disk cache are not re-sent. - Failures are silent: network errors, timeouts, and server rejections are logged at DEBUG level only and never surface to the agent. --- ## Architecture ``` URL ──▶ fetcher.py (httpx: 10s timeout, 5 redirects, Chrome UA ▼ OR Playwright headless Chromium for js=true) cleaner.py (BeautifulSoup/lxml: strip noise, split nav vs content ▼ links, filter layout tables, deduplicate text blocks, structured map infer page_type, detect auth_gated) ▼ cache.py (24h TTL, MD5 keyed JSON files in ./cache/) ▼ registry_sync.py (fire-and-forget POST to api.getwebspeed.io/v1/contribute) ▼ server.py (FastMCP: 8 tools over stdio) ``` No AI. No interpretation. The agent is the brain. --- ## Known limitations - **JS-rendered SPAs**: Pages that load content via JavaScript (React, Vue, Angular) return only the pre-render HTML shell. Password fields, search results, and navigation that are injected by JS will be missing. Use `inspect_element` on what's visible and pair with a browser-automation tool for SPA-heavy targets. - **`page_type` heuristics**: Classification is structural and fast, but not infallible. A marketing page with many internal links might be `listing`; a page with an email field and no password won't be `login`. - **Cache is local disk**: The `./cache/` directory is local. In a multi-process or distributed deployment, cache entries won't be shared across instances. For shared caching, replace `cache.py` with a Redis or Memcached backend. - **Rate limits not enforced**: Web Speed does not throttle outbound requests. For high-volume agent fleets, put a rate-limiting proxy (e.g., Cloudflare, nginx) in front of the server.