opencrawl
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@opencrawlscrape example.com and give me clean markdown"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
opencrawl
Local, keyless web scraping for agents and scripts. One Node process gives you an MCP server, a v2-style HTTP API, a CLI and a TypeScript library that turn web pages into clean markdown. No account, no credits, no data leaving your machine except the requests you make to the sites you scrape.
MCP server with
scrape,batch_scrape,map,crawl,check_crawl_status,search,parse,extract,feed,search_localandwatchtools for Claude Code and any other MCP client.Feeds: parse RSS 2.0, RSS 1.0, Atom and JSON Feed, or discover a site's feed from a page; the cheapest way to ask "what's new".
Documents: PDF, DOCX and XLSX, from disk or from a URL.
Polite and resilient: retries 429/503 with backoff and
Retry-After, honours robots.txtCrawl-delay, and can write every page to disk as markdown.Structured extraction without a model: JSON-LD, microdata, Open Graph, Twitter card and meta tags, values picked by CSS selectors (with attributes, lists and nested fields), and HTML tables as JSON rows or CSV. Exact values, no hallucination.
Local store (SQLite, no server): every scrape and crawl is saved.
maxAgeserves repeat requests from it,search_localfull-text searches everything you have ever scraped (offline), crawl jobs survive restarts, andwatchtells you what changed on a page since you last looked, with a unified diff.HTTP API (
/v2/scrape,/v2/map,/v2/crawl,/v2/search,/v2/parse) with{ success, data }envelopes, so clients that let you override their API base URL can point at it.CLI and library for direct use.
Plain HTTP first; headless Chromium only when a page is a JavaScript shell or when you ask for a screenshot or
waitFor. Uses your installed Google Chrome when Playwright's bundled Chromium is missing.
Install
git clone https://github.com/ktcod/opencrawl
cd opencrawl
npm install # Node 22.13 or newer (built-in SQLite)
npm run build
npm link # puts `opencrawl` on your PATH
opencrawl doctor # checks browser + search backendIf doctor reports no browser, either install Google Chrome or run
npx playwright install chromium.
Related MCP server: scrapyard
Use it from Claude Code (MCP)
claude mcp add --scope user opencrawl -- opencrawl mcpOr edit ~/.claude.json:
{
"mcpServers": {
"opencrawl": { "command": "opencrawl", "args": ["mcp"] }
}
}Tools are registered as scrape, batch_scrape, map, crawl, check_crawl_status,
search, parse, extract, feed, search_local and watch. crawl and batch_scrape
accept outDir to write pages to disk instead of returning them. Set OPENCRAWL_TOOL_PREFIX to prefix them if an existing
prompt expects other names.
Use the HTTP API
opencrawl serve # http://127.0.0.1:3002
curl -s http://127.0.0.1:3002/v2/scrape \
-H 'content-type: application/json' \
-d '{"url":"https://example.com","formats":["markdown","links"]}'Endpoint | Body | Response |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
|
| |
|
| |
|
|
|
| multipart |
|
|
|
|
|
|
|
|
| |
|
|
|
A Document is { markdown?, html?, rawHtml?, links?, images?, screenshot?, metadata }
where metadata carries title, description, language, sourceURL, url,
statusCode, contentType and the usual Open Graph fields.
The API refuses to bind a non-loopback address without OPENCRAWL_API_KEY; with a key,
every /v2 request needs Authorization: Bearer <token>. Browser requests (any Origin
header), unexpected Host headers and non-JSON bodies are refused, so a web page cannot
drive the scraper. /v2/parse only accepts a raw filePath from loopback callers; remote
callers upload the file as multipart form-data.
Use the CLI directly
opencrawl scrape https://example.com # markdown to stdout
opencrawl scrape https://example.com -f markdown,links --json
opencrawl scrape https://app.example.com --render always # force a browser render
opencrawl map https://docs.example.com --search "auth" --limit 20
opencrawl crawl https://docs.example.com --limit 50 --include-paths "^/docs" -o crawl.json
opencrawl search "query" --limit 5 --scrape
opencrawl parse ./report.pdf # also .docx and .xlsx
opencrawl scrape https://a.example https://b.example --out-dir ./pages # batch, one folder per page
opencrawl crawl https://docs.example.com --limit 50 --out-dir ./docs # <dir>/<host>/<path>/index.md
opencrawl feed https://example.com/blog --limit 5 # discovers and parses the feed
opencrawl feed https://example.com/feed.xml --scrape --json # items plus each page's markdown
opencrawl scrape https://example.com --max-age 3600000 # reuse a snapshot under an hour old
opencrawl search-local "pricing tiers" --site docs.example.com
opencrawl watch https://example.com/changelog # new | same | changed (+diff) | removed
opencrawl watch --list
opencrawl extract https://example.com/product # JSON-LD, microdata, OG, meta, tables
opencrawl extract https://example.com/list --select "names=.product .name*" --select "links=.product a@href*"
opencrawl extract https://example.com/stats --csv # every table as CSVFeeds, documents and files on disk
Feeds.
feedaccepts a feed URL or any page. For a page it reads<link rel="alternate">tags, then probes/feed,/rss.xml,/atom.xml,/index.xml,/feed.jsonand friends. Items are normalized across RSS 2.0, RSS 1.0 (RDF), Atom and JSON Feed:title,url,published,updated,author,summary(text),content(markdown),categories.sincefilters by date,scrape: trueattaches each item's page.DOCX and XLSX.
parseandscrapehandle them next to PDF. DOCX goes through the same HTML-to-markdown pipeline as web pages (headings, bold, lists survive). Each XLSX sheet becomes a markdown table whose first row is the header; ask for theextractformat to get the sheets as table objects too.Out-dir.
crawl --out-dir,scrape <urls...> --out-dir, andoutDiron the MCPcrawlandbatch_scrapetools write one folder per page at<dir>/<host>/<path>/withindex.md, plusindex.html,links.txt,extract.json,screenshot.pngandmetadata.jsonwhen those formats were requested. Query strings become a short hash so variants do not collide; nothing is written outside the directory.Retries and politeness. Plain HTTP fetches retry 429 and 503 up to
OPENCRAWL_RETRIES(2) times, honouringRetry-Afterand capping each wait atOPENCRAWL_MAX_RETRY_WAIT_MS(30 s). A crawl with no explicitdelayuses the start host's robots.txtCrawl-delay, which also serializes its requests.
Structured extraction
extract reads what the page already states in machine-readable form, so an agent gets
exact values before it reads any prose:
Embedded data (
structured, default all):jsonLd(everyapplication/ld+jsonblock, invalid ones reported undererrors),microdata(items as JSON-LD-style objects with@type, nested items and repeated properties as arrays),openGraph(og:*,article:*,product:*and friends, repeated keys as arrays),twitterandmeta(title, lang, canonical, description, author, and so on).Selectors (
selectors):{ "price": ".price" }returns the first match's text. A full spec is{ selector, attribute?, type?: "text" | "html" | "attribute", multiple?, fields? }.multiplereturns every match;fieldsevaluates sub-selectors inside each match so a list of cards becomes a list of objects;href/srcstyle attributes come back absolute. Invalid selectors land inerrors, not exceptions.Tables (
tables, default on): each<table>becomes{ caption, headers, rows, rowCount, columnCount }. Header rows (<thead>or all-<th>rows) become keys, duplicate headers get_2suffixes,colspan/rowspanare expanded, and{ csv: true }adds RFC 4180 CSV.selectorlimits which tables,minRowsandmaxRowsbound them.
The same options work as a scrape format, { type: "extract", ... }, so a crawl can
extract from every page it visits (the result lands in document.extract).
The local store
Every scrape, crawl page and watch snapshot is written to a SQLite file
(~/.opencrawl/opencrawl.db by default, OPENCRAWL_DB to move it). Screenshots are not
stored. What it gives you:
Cache.
maxAge(milliseconds) on a scrape or crawl returns the newest snapshot fetched within that window instead of hitting the network; the result carriesmetadata.cacheState: "hit"andcachedAt. The default window is 0 (always fresh); setOPENCRAWL_DEFAULT_MAX_AGE_MSto change it.storeInCache: falseskips the write.Local search.
search_local/opencrawl search-localruns SQLite FTS5 over titles and markdown of everything stored, one hit per URL, with a snippet. No network.Persistent crawls. Job status and pages are read from the store, so a crawl started by one process can be read by another and survives restarts (a crawl interrupted mid-run is marked
failed). Finished jobs are kept forOPENCRAWL_JOB_RETENTION_DAYS(7).Change tracking.
watchre-scrapes a URL and compares it with its last snapshot. The first call records a baseline (new); later calls answersame,changed(with a unified diff and added/removed line counts),removed(404/410 after a baseline) orerror. Nothing is scheduled inside opencrawl; call it from cron, launchd or an agent.
Delete the file to start over.
Use the library
import { scrape, map, crawl, search, parseFile } from "opencrawl";
const doc = await scrape("https://example.com", { formats: ["markdown", "links"] });
const { links } = await map("https://example.com", { search: "pricing" });
const job = await crawl("https://docs.example.com", { limit: 20, includePaths: ["^/docs"] });
const results = await search({ query: "self hosted scraper", limit: 5 });What it does and doesn't do
Feature | opencrawl |
Scrape formats markdown, html, rawHtml, links, images, screenshot, attributes | Yes |
LLM-backed formats (json, summary, query, changeTracking, branding) | No. Returned as a |
| Yes |
PDF scraping and parsing ( | Yes (text layer only, no OCR) |
Map with sitemap modes, keyword ranking, subdomains, query stripping, limit | Yes |
Crawl with limit, depth, include/exclude paths, subdomains, external links, entire-domain, delay, concurrency, sitemap modes, robots.txt | Yes. Default limit 100 pages |
Search sources web/news/images, time filters, domain filters, per-result scraping, github/pdf/research categories | Yes. News and images need SearXNG or Brave |
Parse PDF, DOCX, XLSX, HTML, text files | Yes |
Batch scrape, feeds (RSS/Atom/JSON), pages written to disk | Yes |
Retries on 429/503 with Retry-After, robots.txt Crawl-delay | Yes |
Cache ( | Yes, via the local SQLite store |
Structured extraction: JSON-LD, microdata, Open Graph, meta, CSS selectors, tables to JSON/CSV | Yes, no model involved |
Browser actions, interactive sessions, agents, scheduled monitors, webhooks | No |
Proxies, stealth, caching options | Accepted and ignored |
Search backends
Backend | Configure | Sources |
DuckDuckGo HTML (default) | nothing | web only; rate limited under heavy use |
SearXNG |
| web, news, images |
Brave Search API |
| web, news, images |
Force one with OPENCRAWL_SEARCH_BACKEND=duckduckgo|searxng|brave.
Configuration
Variable | Default | Meaning |
|
|
|
|
| Per-request timeout |
|
| Default crawl page cap |
|
| Parallel pages per crawl (max 20) |
|
| Default map URL cap |
|
| How far map follows links when the sitemap is thin |
| Chrome desktop UA | User agent for HTTP and browser requests |
|
| Token checked against robots.txt during crawls |
| empty | MCP tool name prefix |
|
| SQLite store path ( |
|
| Cache window used when a request sets no |
|
| Documents per page on |
|
| How long finished crawl jobs stay in the store |
|
| Retries on HTTP 429/503 per fetch |
|
| Longest wait between retries, even if Retry-After asks for more |
|
| Parallel pages for batch scrapes |
| unset | Bearer token required by the HTTP API. Required to bind a non-loopback |
| bind host | Extra |
| none | Browser origins allowed to call the HTTP API; any other |
|
| Largest page or sitemap body buffered (50 MB) |
|
| Sitemap files fetched per map or crawl |
|
| Browser pages open at once across all scrapes |
|
| HTTP API bind |
Development
npm test # vitest against a local fixture site; no network, no browser
npm run typecheck
OPENCRAWL_TEST_BROWSER=1 npm test # also runs the Chrome rendering testsDesign decisions and known gaps are in DESIGN.md.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Web scraping MCP server: any URL to Markdown or JSON, with JavaScript rendering and batch jobs.
Web scraping, extraction and search for AI agents. Hosted MCP with workspace API keys.
One MCP for the Web. Easily search, crawl, navigate, and extract websites without getting blocked.…
Scrape, crawl and search the web for AI agents via MCP.
Related MCP Servers
- AlicenseAqualityCmaintenanceMCP server for config-driven web scraping, offering tools to scrape, crawl, extract markdown, and check status.81MIT
- AlicenseAqualityAmaintenanceLocal MCP server for web search and page extraction, providing clean markdown from URLs, search results, site mapping, and research endpoints without API keys or accounts.5Apache 2.0
- AlicenseAqualityAmaintenanceA local-first MCP server that lets AI agents read any webpage as clean Markdown, crawl whole sites within configured limits, search without API keys, and solve supported captchas locally — all without cloud services or third-party keys.3020AGPL 3.0
- AlicenseAqualityCmaintenanceGives MCP-capable agents live web access: search the web, scrape pages into Markdown (including JavaScript-heavy and bot-protected sites), and extract named fields as JSON, with job polling, token-aware content offloading, and built-in research guidance. Ships as a self-hostable stdio or HTTP service with spend caps and per-request key support.7MIT