内容
<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
爬取任何文档网站,将其索引到DuckDB中,并使用混合搜索(语义搜索 + BM25)将其公开给LLM代理,作为一个 **MCP服务器** —— 让您的编码助手根据 *当前* 文档进行推理,而不是其训练截止日期。
默认情况下完全 **离线运行** :内置的哈希嵌入器不需要API密钥,每个测试都在不访问网络的情况下运行。设置一个环境变量即可在生产环境中切换到OpenAI嵌入。
## 为什么
LLM代理会产生过时的API幻觉。解决方法是对 *实际* 文档的 *实际* 版本进行检索。webdocs-mcp是最小化的诚实堆栈,可以实现端到端的此功能:指向一个文档网站,它会爬取并跟踪层次结构,分割和嵌入每个页面,并通过模型上下文协议向Cursor、VS Code或Claude Code提供`search_docs` / `get_doc_page`工具。
## 架构
```mermaid
flowchart LR
U["您 / CI"] -->|POST /fetch_url| API["FastAPI"]
API --> JOBS["作业运行器<br/>(后台线程)"]
JOBS --> CRAWL["爬虫<br/>BFS,同域,<br/>父/子层次结构"]
CRAWL --> CHUNK["分段器<br/>段落感知 + 重叠"]
CHUNK --> EMB["嵌入器<br/>哈希(离线)/ OpenAI"]
EMB --> DB[("DuckDB<br/>页面 + 分段 + 嵌入")]
AGENT["LLM代理<br/>(Cursor / VS Code / Claude Code)"] -->|MCP JSON-RPC| MCP["/mcp 端点"]
MCP --> SEARCH["混合搜索<br/>余弦 + BM25"]
SEARCH --> DB
BROWSER["浏览器"] -->|/map| MAPS["站点地图<br/>纯HTML树"]
MAPS --> DB
```
值得注意的设计选择:
- **使用DuckDB而不是向量数据库服务器** —— 一个团队的文档索引是几千个分段,而不是数十亿。 一个可移植的文件,零运维。
- **BM25 + 嵌入,始终两者** —— 嵌入捕获同义表达(“如何退款” → 退款政策);BM25捕获确切标识(`ERR_LOCK_TIMEOUT`),而嵌入会扩散。分数归一化为 min-max 并按 60/40 混合。
- **可注入的获取器** —— 爬虫采用一个简单的 `(url) -> html` 调用。测试注入一个字典支持的假网站;生产环境使用httpx;一个无头的浏览器在不影响爬虫逻辑的情况下接入。
- **直接实现MCP** —— MCP的可流式HTTP核心是JSON-RPC调度(`initialize`、`tools/list`、`tools/call`)。拥有这大约100行代码保持堆栈依赖轻量,并且完全可以离线测试。
- **现在使用线程,将来使用Redis** —— 作业在后台线程上运行,守护线程在 `JobManager` 接口后面,该接口类似于队列消费者。docker-compose已经提供了Redis服务用于扩展路径。
## 快速开始
```bash
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 个测试,完全离线
ruff check src tests
PYTHONPATH=src uvicorn webdocs.api:app --port 9111
```
然后:
```bash
# 启动爬取
curl -X POST localhost:9111/fetch_url -H 'Content-Type: application/json' \
-d '{"url": "https://docs.astral.sh/ruff", "max_pages": 30}'
# 查看进度
curl localhost:9111/job_progress
# 搜索索引内容
curl 'localhost:9111/search_docs?query=如何+忽略+规则&top_k=3'
```
交互式OpenAPI文档位于 `http://localhost:9111/docs`,站点地图位于 `http://localhost:9111/map`。
## Docker
```bash
docker compose up --build
# 应用程序在 :9111 上,索引保存在 webdocs-data 卷中
```
## MCP集成
将以下内容添加到Cursor / VS Code / Claude Code的MCP配置中:
```json
{
"webdocs": {
"type": "http",
"url": "http://localhost:9111/mcp"
}
}
```
公开的工具:
| 工具 | 用途 |
|---|---|
| `search_docs` | 对每个索引分段进行混合搜索;返回文本 + 源URL |
| `list_doc_pages` | 列出索引页面,以便代理可以选择一个 |
| `get_doc_page` | 一个页面的完整提取文本 |
## API参考
| 端点 | 描述 |
|---|---|
| `POST /fetch_url` | 启动爬取作业(`{url, max_pages?, max_depth?}`);`?sync=true` 阻止直到完成 |
| `GET /job_progress` | 所有作业,或通过 `?job_id=` 获取一个作业 |
| `GET /search_docs` | `?query=&top_k=` 混合搜索 |
| `GET /list_doc_pages` | 所有索引页面 |
| `GET /get_doc_page` | `?page_id=` 完整页面文本 |
| `GET /map` | 爬取站点的索引(纯HTML,无JS) |
| `GET /map/site/{root_id}` | 一个站点的层次树 |
| `GET /map/page/{id}` | 页面视图,带有breadcrumb,siblings,children |
| `GET /map/page/{id}/raw` | 原始提取文本 |
| `POST /mcp` | MCP JSON-RPC(initialize,tools/list,tools/call) |
| `GET /health` | 状态 + 页面/分段计数 |
## 站点地图
爬取的页面保持父/子关系,因此 `/map` 渲染一个真实的可导航树,按站点 —— breadcrumb从根,siblings导航,children列表 —— 在纯HTML(无JavaScript)中。单独从同一域名获取的页面分组在一个根目录下。
## 配置
所有配置项均为可选(参见 `.env.example`):
| 变量 | 默认值 | 含义 |
|---|---|---|
| `WEBDOCS_DB_PATH` | `data/webdocs.duckdb` | 索引位置(如果不可写,则回落到内存中) |
| `WEBDOCS_EMBEDDER` | `hashing` | `hashing`(离线)或 `openai` |
| `OPENAI_API_KEY` | — | 仅用于 `openai` 嵌入器 |
| `WEBDOCS_MAX_PAGES` / `WEBDOCS_MAX_DEPTH` | 50 / 3 | 爬取限制 |
| `WEBDOCS_CHUNK_SIZE` / `WEBDOCS_CHUNK_OVERLAP` | 1200 / 150 | 分段 |
## 测试
```bash
pytest tests/ -v # 23 个测试:爬虫,分段器,嵌入器,DB,混合搜索,API,MCP
```
没有测试会访问网络 —— 爬虫测试在内存中运行在一个假网站上,通过获取器接口注入,每个测试都会得到一个临时DuckDB文件。
## 路线图
- [ ] 尊重 `robots.txt` 并添加每个域的速率限制
- [ ] 增量重新爬取(etag/last-modified)而不是完全重新索引
- [ ] 使用现有的compose服务基于Redis的worker池
- [ ] 链接PDF的OCR/文本提取
- [ ] DuckDB VSS扩展(HNSW),一旦索引超出强力余弦搜索的范围
## 许可证
MIT —— 请参阅 [LICENSE](LICENSE)。