Architecture
Crate Structure
crw is a Rust workspace with 11 crates, each with a focused responsibility:
crw-mcp-proto shared MCP JSON-RPC types (leaf)
crw-core types, config, errors → crw-mcp-proto
crw-diff change-tracking diff engine → crw-core
crw-extract HTML cleaning, format conversion → crw-core
crw-renderer HTTP + CDP fetcher → crw-core, crw-extract
crw-search search client and result transforms → crw-core
crw-crawl BFS crawler, single-page scraper → crw-core, crw-diff, crw-extract, crw-renderer
crw-server Axum HTTP API (library + own binary) → crw-core, crw-crawl, crw-diff, crw-extract, crw-renderer, crw-search
crw-browse interactive browser-automation MCP server → crw-core, crw-renderer
crw-mcp stdio MCP proxy → crw-core, crw-renderer, crw-server
crw-cli command-line binary → crw-browse, crw-core, crw-crawl, crw-extract, crw-renderer, crw-search, crw-server
crw-server has no explicit [[bin]] section in its Cargo.toml, but it does
have a src/main.rs, so Cargo auto-discovers a crw-server binary target
alongside the library. That binary is the production container entrypoint
(Dockerfile CMD ["crw-server"]). The crw binary is a separate build, from
crw-cli, which embeds the crw-server library for crw serve. The workspace
produces four binaries: crw (from crw-cli), crw-server (from
crw-server), crw-mcp (from crw-mcp), and crw-browse (from crw-browse,
a separate standalone MCP server for interactive browser automation, see
crw-browse).
Request Flow
Scrape request
Client → POST /v1/scrape
→ Auth middleware (Bearer token check)
→ scrape handler
→ FallbackRenderer.fetch(url)
→ HTTP request (reqwest)
→ SPA detection heuristics
→ CDP rendering if needed (LightPanda/Playwright/Chrome)
→ HTML cleaning (lol_html)
→ Remove script/style/iframe/svg
→ Remove nav/footer/header if onlyMainContent
→ Apply includeTags/excludeTags CSS selectors
→ Readability extraction
→ Try: article → main → [role=main] → .post-content → body
→ Format conversion
→ Markdown (fast_html2md with fallback chain)
→ HTML / RawHTML / PlainText / Links
→ JSON (LLM extraction via Anthropic/OpenAI)
→ Metadata extraction
→ title, description, og:*, canonical, lang
→ JSON response
Crawl request
Client → POST /v1/crawl
→ Create crawl job (UUID)
→ Spawn async BFS task
→ Fetch robots.txt
→ BFS loop with VecDeque:
→ Dequeue URL
→ Check robots.txt allowance
→ Acquire semaphore (max_concurrency)
→ Rate limit (requests_per_second)
→ Scrape page (same pipeline as single scrape)
→ Extract links from page
→ Filter: same origin, not visited, under max_depth
→ Enqueue new URLs
→ Update crawl state via watch::Sender
→ Until: maxPages reached, maxDepth exhausted, or queue empty
→ Return job ID immediately
Client → GET /v1/crawl/{id}
→ Read crawl state via watch::Receiver
→ Return progress + results
Middleware Stack
The Axum server applies middleware in this order:
- CORS —
CorsLayer::permissive() - Trace — HTTP request logging via
tracing - Body limit — Max 1 MB request body
- Timeout — Configurable request timeout
- Auth — Bearer token validation (if
auth.api_keysis set)
Feature Flags
| Flag | Crate | Effect |
|---|---|---|
cdp |
crw-renderer |
Enables CDP rendering via tokio-tungstenite |
cdp |
crw-server |
Passes through to crw-renderer/cdp |
test-utils |
crw-server |
Exposes internal functions for testing |
Key Dependencies
| Dependency | Purpose |
|---|---|
axum 0.8 |
HTTP API framework |
tokio |
Async runtime |
reqwest |
HTTP client (rustls) |
tokio-tungstenite |
CDP WebSocket (with cdp feature) |
lol_html |
Streaming HTML rewriting |
scraper |
CSS selector-based HTML parsing |
fast_html2md |
HTML → Markdown conversion |
jsonschema |
LLM output validation |
config |
Layered TOML configuration |
tracing |
Structured logging |