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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。