Tendril # Tendril **Web ingestion that reaches what other crawlers can't.** Scrape Β· Map β€” running on a residential Mac, in **real WebKit**, with **deterministic, testable** extraction.
[![live](https://img.shields.io/website?url=https%3A%2F%2Fwww.ten-dril.com%2Fhealthz&style=flat-square&label=www.ten-dril.com&up_message=live&up_color=4ade80&down_message=down&down_color=ff6b6b)](https://www.ten-dril.com) ![tests](https://img.shields.io/badge/tests-86_passing-4ade80?style=flat-square&logo=vitest&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white) ![Node](https://img.shields.io/badge/Node-%E2%89%A522-5FA04E?style=flat-square&logo=nodedotjs&logoColor=white) ![Fastify](https://img.shields.io/badge/Fastify-5-000000?style=flat-square&logo=fastify&logoColor=white) ![pnpm](https://img.shields.io/badge/pnpm-workspaces-F69220?style=flat-square&logo=pnpm&logoColor=white) ![engine](https://img.shields.io/badge/engine-WebKit-2dd4bf?style=flat-square&logo=safari&logoColor=white) ![extraction](https://img.shields.io/badge/extraction-deterministic-f5c451?style=flat-square) ![LLM in pipeline](https://img.shields.io/badge/LLMs_in_extraction-0-07100c?style=flat-square&labelColor=4ade80) ![host](https://img.shields.io/badge/host-m3u96a_Β·_macOS_ARM-111?style=flat-square&logo=apple&logoColor=white) ![ingress](https://img.shields.io/badge/ingress-ngrok-1F1E37?style=flat-square&logo=ngrok&logoColor=white) ![agent ready](https://img.shields.io/badge/agent-MCP_%2B_Skill-4ade80?style=flat-square) **[🌐 Live](https://www.ten-dril.com) Β· [β–Ά Playground](https://www.ten-dril.com/#playground) Β· [πŸ€– For agents](https://www.ten-dril.com/#agents) Β· [πŸ“– Docs](https://www.ten-dril.com/#docs) Β· [πŸ“¦ Source](https://git.spboucher.ai/tendril)**
--- ## ✦ Why Tendril Firecrawl runs in the cloud, on datacenter IPs, in headless Chromium. Tendril runs on a **residential machine**, in **WebKit** β€” Safari's actual engine β€” behind a real IP. | | | |---|---| | πŸ•ΈοΈ **Browser fidelity** | WebKit on macOS ARM behind a residential IP is *not* a headless-Chromium signature. Sites that block others often don't block Tendril. | | πŸ“ **Deterministic extraction** | No LLM in the extraction path. The same page always yields the same Markdown β€” so you can write **golden-file tests** against it. | | 🧭 **Map without rendering** | Merge sitemaps, robots directives, homepage links and `/llms.txt` into one deduplicated URL list in seconds. | | πŸ”’ **Fair by default** | Identifiable bot UA + contact URL, `robots.txt` honoured by default, per-host rate limiting, SSRF-hardened. | --- ## ⚑ Quickstart ```bash # Scrape a page β†’ clean Markdown + structured data + links curl -s -X POST https://www.ten-dril.com/v1/scrape \ -H 'content-type: application/json' \ -d '{"url":"https://www.iana.org","formats":["markdown","structured","links"]}' # Map a site β†’ every URL, no rendering curl -s -X POST https://www.ten-dril.com/v1/map \ -H 'content-type: application/json' \ -d '{"url":"https://www.iana.org","limit":50}' ``` Every response is `{ "success": boolean, "data"?: …, "error"?: { "code", "message" } }` with an `X-Request-Id` header. --- ## πŸ€– Give it to your AI agent Hand Tendril to any agent β€” two drop-in integrations, downloadable from the site: | | Download | Install | |---|---|---| | **MCP server** β€” single file, no `npm install` | [`tendril-mcp.mjs`](https://www.ten-dril.com/downloads/tendril-mcp.mjs) Β· [`.zip`](https://www.ten-dril.com/downloads/tendril-mcp.zip) | `claude mcp add tendril -- node /path/to/tendril-mcp.mjs` | | **Agent Skill** β€” `SKILL.md` | [`tendril-skill.zip`](https://www.ten-dril.com/downloads/tendril-skill.zip) | `unzip tendril-skill.zip -d ~/.claude/skills/` | The MCP server exposes `tendril_scrape`, `tendril_map`, `tendril_status`. Both point at `https://www.ten-dril.com` and need no API key today. --- ## πŸ›οΈ Architecture β€” three-tier escalation router Always start at the cheapest tier; escalate only on evidence of failure. ``` request ──▢ Router ──shouldEscalate()──┐ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β–Ό β–Ό β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ TIER 0 β”‚ β”‚ TIER 1 β”‚ β”‚ TIER 2 β”‚ β”‚ β”‚ undici β”‚β–Άβ”‚ WKWebView β”‚β–Άβ”‚ Safari β”‚ β”‚ ← Tiers 1/2 = next phase β”‚ HTTP β”‚ β”‚ pool β”‚ β”‚driver β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό Extraction engine (deterministic) β–Ό markdown Β· structured Β· links ``` --- ## πŸ“¦ Endpoints | Method | Path | Purpose | |---|---|---| | `POST` | `/v1/scrape` | Fetch one page β†’ Markdown, structured data, links, metadata | | `POST` | `/v1/map` | Discover a site's URLs (sitemaps + robots + homepage + `/llms.txt`) | | `GET` | `/v1/status` | Per-tier availability + pipeline version | | `GET` | `/healthz` | Shallow health for uptime monitors | --- ## 🧩 What's built | Area | Spec | Status | |---|---|---| | Error taxonomy (single source) | Β§15 | βœ… | | `Result` + typed errors, strict TS | Β§21 | βœ… | | URL normalization | Β§10.1 | βœ… | | Escalation decision `shouldEscalate` (pure) | Β§3.4 | βœ… | | SSRF validation (resolve-then-validate + IP pinning) | Β§16.5 | βœ… | | Tier 0 HTTP fetch (Safari header order, manual redirects, gzip/br) | Β§3 | βœ… | | Deterministic extraction (JSON-LD/OG, Readability + density, Turndown) | Β§8 | βœ… | | robots.txt (RFC 9309) + `/v1/map` | Β§10.1, Β§14.3 | βœ… | | `/v1/scrape`, `/v1/status`, playground + docs UI | Β§14 | βœ… | | MCP server + agent Skill | β€” | βœ… | | Deployment: m3u96a + PM2 + ngrok | Β§16 | βœ… | | **Tier 1 WKWebView (Swift)**, Tier 2 Safari, proxies, crawl, cache | Β§4–13 | πŸ”œ | > Under `tier: "auto"`, a page that needs escalation returns `ERR_TARGET_BLOCKED` until Tiers 1/2 ship β€” force `tier: "http"` for raw Tier 0 output. --- ## πŸ› οΈ Tech stack ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) ![undici](https://img.shields.io/badge/undici-HTTP-000?style=flat-square) ![linkedom](https://img.shields.io/badge/linkedom-DOM-2dd4bf?style=flat-square) ![Turndown](https://img.shields.io/badge/Turndown-Markdown-f5c451?style=flat-square) ![Readability](https://img.shields.io/badge/Readability-boilerplate-4ade80?style=flat-square) ![Zod](https://img.shields.io/badge/Zod-schemas-3E67B1?style=flat-square&logo=zod&logoColor=white) ![Vitest](https://img.shields.io/badge/Vitest-86_tests-6E9F18?style=flat-square&logo=vitest&logoColor=white) ![MCP](https://img.shields.io/badge/MCP-server-4ade80?style=flat-square) --- ## πŸ’» Develop ```bash pnpm install pnpm test # 86 hermetic unit/contract tests, no network pnpm build # tsc -b, strict, emits dist/ pnpm dev:api # Fastify on 127.0.0.1:3000 ``` ## πŸš€ Deploy Production host `m3u96a`; ingress via ngrok reserved domain `www.ten-dril.com`. API binds `127.0.0.1` only β€” ngrok is the sole ingress. ```bash TENDRIL_NODE=M3U96a TENDRIL_PORT=8092 ./scripts/deploy.sh # rsync + install + build + PM2 + smoke ./scripts/health.sh https://www.ten-dril.com # smoke an existing deployment ``` --- ## πŸ—‚οΈ Layout ``` packages/shared Result, error taxonomy, types, logger, URL normalization packages/router shouldEscalate (Β§3.4) packages/egress SSRF validation + IP classification (Β§16.5) packages/fetcher-http Tier 0 undici client (Β§3) packages/extract deterministic pipeline (Β§8): structured Β· boilerplate Β· markdown packages/frontier robots.txt (Β§10.1), sitemaps, /map discovery (Β§14.3) apps/api Fastify public surface + playground UI (Β§14) integrations/mcp Model Context Protocol server (single-file bundle) integrations/skill Agent Skill (SKILL.md) deploy/ Β· scripts/ PM2 ecosystem, ngrok config, deploy.sh, health.sh (Β§16) test/fixtures/html saved pages for offline extraction tests (Β§20) ``` ---
Built by **Simon-Pierre Boucher** Β· [contact@spboucher.ai](mailto:contact@spboucher.ai) Stealth means matching a real browser's fingerprint β€” not hiding who we are. Tendril honours robots.txt and identifies itself.