webdocs-mcp
Crawl documentation sites, index them with hybrid search, and expose them as MCP tools so LLM agents can search and retrieve current docs.
README
<p align="center"> <img src="docs/assets/banner.png" alt="webdocs-mcp" width="100%" /> </p>
<p align="center"> <a href="https://github.com/saianthireddy/webdocs-mcp/actions/workflows/ci.yml"><img src="https://github.com/saianthireddy/webdocs-mcp/actions/workflows/ci.yml/badge.svg" alt="CI"></a> <img src="https://img.shields.io/badge/python-3.10%20%7C%203.12-blue" alt="Python"> <img src="https://img.shields.io/badge/license-MIT-green" alt="License"> </p>
🩺 webdocs-mcp
Crawl any documentation site, index it into DuckDB with hybrid (semantic + BM25) search, and expose the whole thing to LLM agents as an MCP server — so your coding assistant reasons over current docs instead of its training cutoff.
Runs completely offline by default: the built-in hashing embedder needs no API key, and every test runs without touching the network. Set one environment variable to switch to OpenAI embeddings in production.
Why
LLM agents hallucinate stale APIs. The fix is retrieval over the actual docs of the actual version you use. webdocs-mcp is the smallest honest stack that does this end to end: point it at a docs site, it crawls with hierarchy tracking, chunks and embeds every page, and serves search_docs / get_doc_page tools over the Model Context Protocol to Cursor, VS Code, or Claude Code.
Architecture
flowchart LR
U["You / CI"] -->|POST /fetch_url| API["FastAPI"]
API --> JOBS["Job runner<br/>(background threads)"]
JOBS --> CRAWL["Crawler<br/>BFS, same-domain,<br/>parent/child hierarchy"]
CRAWL --> CHUNK["Chunker<br/>paragraph-aware + overlap"]
CHUNK --> EMB["Embedder<br/>hashing (offline) / OpenAI"]
EMB --> DB[("DuckDB<br/>pages + chunks + embeddings")]
AGENT["LLM agent<br/>(Cursor / VS Code / Claude Code)"] -->|MCP JSON-RPC| MCP["/mcp endpoint"]
MCP --> SEARCH["Hybrid search<br/>cosine + BM25"]
SEARCH --> DB
BROWSER["Browser"] -->|/map| MAPS["Site maps<br/>pure HTML tree"]
MAPS --> DB
Design choices worth calling out:
- DuckDB over a vector-DB server — a doc index for one team is thousands of chunks, not billions. One portable file, zero ops.
- BM25 + embeddings, always both — embeddings catch paraphrases ("how do I get my money back" → refund policy); BM25 catches exact identifiers (
ERR_LOCK_TIMEOUT) that embeddings smear. Scores are min-max normalised and blended 60/40. - Injectable fetcher — the crawler takes a plain
(url) -> htmlcallable. Tests inject a dict-backed fake site; production uses httpx; a headless browser slots in without touching crawl logic. - MCP implemented directly — the streamable-HTTP core of MCP is JSON-RPC dispatch (
initialize,tools/list,tools/call). Owning those ~100 lines keeps the stack dependency-light and fully offline-testable. - Threads now, Redis when needed — jobs run on daemon threads behind a
JobManagerinterface shaped like a queue consumer. docker-compose already ships the Redis service for the scale-out path.
Quickstart
git clone https://github.com/saianthireddy/webdocs-mcp.git
cd webdocs-mcp
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
pytest # 23 tests, fully offline
ruff check src tests
PYTHONPATH=src uvicorn webdocs.api:app --port 9111
Then:
# start a crawl
curl -X POST localhost:9111/fetch_url -H 'Content-Type: application/json' \
-d '{"url": "https://docs.astral.sh/ruff", "max_pages": 30}'
# watch progress
curl localhost:9111/job_progress
# search what was indexed
curl 'localhost:9111/search_docs?query=how+do+I+ignore+a+rule&top_k=3'
Interactive OpenAPI docs at http://localhost:9111/docs, site maps at http://localhost:9111/map.
Docker
docker compose up --build
# app on :9111, index persisted in the webdocs-data volume
MCP integration
Add to Cursor / VS Code / Claude Code MCP configuration:
{
"webdocs": {
"type": "http",
"url": "http://localhost:9111/mcp"
}
}
Exposed tools:
| Tool | Purpose |
|---|---|
search_docs |
Hybrid search over every indexed chunk; returns text + source URLs |
list_doc_pages |
List indexed pages so the agent can pick one |
get_doc_page |
Full extracted text of one page |
API reference
| Endpoint | Description |
|---|---|
POST /fetch_url |
Start a crawl job ({url, max_pages?, max_depth?}); ?sync=true blocks until done |
GET /job_progress |
All jobs, or one via ?job_id= |
GET /search_docs |
?query=&top_k= hybrid search |
GET /list_doc_pages |
Every indexed page |
GET /get_doc_page |
?page_id= full page text |
GET /map |
Index of crawled sites (pure HTML, no JS) |
GET /map/site/{root_id} |
Hierarchical tree of one site |
GET /map/page/{id} |
Page view with breadcrumbs, siblings, children |
GET /map/page/{id}/raw |
Raw extracted text |
POST /mcp |
MCP JSON-RPC (initialize, tools/list, tools/call) |
GET /health |
Status + page/chunk counts |
Site maps
Crawled pages keep parent/child relationships, so /map renders a real navigable tree per site — breadcrumbs from root, sibling navigation, children listing — in pure HTML (no JavaScript). Pages fetched individually from the same domain group under one root.
Configuration
All optional (see .env.example):
| Variable | Default | Meaning |
|---|---|---|
WEBDOCS_DB_PATH |
data/webdocs.duckdb |
Index location (falls back to in-memory if unwritable) |
WEBDOCS_EMBEDDER |
hashing |
hashing (offline) or openai |
OPENAI_API_KEY |
— | Only for openai embedder |
WEBDOCS_MAX_PAGES / WEBDOCS_MAX_DEPTH |
50 / 3 | Crawl limits |
WEBDOCS_CHUNK_SIZE / WEBDOCS_CHUNK_OVERLAP |
1200 / 150 | Chunking |
Testing
pytest tests/ -v # 23 tests: crawler, chunker, embedder, DB, hybrid search, API, MCP
No test touches the network — the crawler tests run against an in-memory fake site injected through the fetcher interface, and each test gets a throwaway DuckDB file.
Roadmap
- [ ] Respect
robots.txtand add per-domain rate limiting - [ ] Incremental re-crawls (etag/last-modified) instead of full re-index
- [ ] Redis-backed worker pool using the existing compose service
- [ ] OCR/text extraction for linked PDFs
- [ ] DuckDB VSS extension (HNSW) once the index outgrows brute-force cosine
License
MIT — see LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.