内容
<p align="center">
<img src=".github/banner.png" alt="footnote-mcp" width="350">
</p>
<!-- mcp-name: io.github.kazkozdev/footnote-mcp -->
用于源头研究的MCP服务器。它搜索网络,获取和提取页面,从表格/文件/API中提取结构化数据,并且 - 这是其与众不同之处 - **验证声明是否实际上得到其来源的支持**,而不是仅仅信任一个片段。42种工具通过stdio MCP驱动,可以由任何MCP客户端(Claude Desktop,Cursor)或通过配套的[Scholiast](https://github.com/KazKozDev/scholiast)研究代理驱动。
设计优先级是*可信度优于便捷性*:搜索片段仅用于发现,每个获取的页面都缓存有出处,声明在计入之前会与源文本进行核对。同时,它也能优雅地降级 - 无需API密钥和配置也能正常工作(爬取搜索 + 自动无头浏览器备用 + 离线验证启发式);密钥和环境变量只会使其更好。
## 快速开始
从源码(Python ≥ 3.10):
```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -e . # 安装`footnote-mcp`控制台脚本 + 依赖项
python -m playwright install chromium # fetch fallback使用的无头浏览器
footnote-mcp # 启动服务器(通过stdio进行MCP通信)
```
`footnote-mcp`现在等待stdio上的MCP客户端。通过将以下内容添加到其MCP设置中,将客户端指向它(Claude Desktop:`claude_desktop_config.json`; Cursor:`~/.cursor/mcp.json`):
```json
{
"mcpServers": {
"footnote": { "command": "footnote-mcp" }
}
}
```
启动时不需要API密钥 - 搜索会降级到爬取Bing + DuckDuckGo。稍后在`"env"`下添加密钥(参见[搜索后端](#搜索后端))。传递`--headed`以查看浏览器层的工作。
可选的运行时变量在[.env.example](.env.example)中记录。将其复制到`.env`中用于本地shell,或将选定的变量粘贴到MCP客户端配置中:
```json
{
"mcpServers": {
"footnote": {
"command": "footnote-mcp",
"env": {
"TAVILY_API_KEY": "..."
}
}
}
}
```
在不安装的情况下直接从源代码树运行:
```bash
PYTHONPATH=src python -m footnote_mcp
```
## 验证声明 - 差异化因素
使用此工具而非普通搜索工具的原因是`evidence_entailment`等工具:它们会告诉一个声明一个来源*支持*,而不仅仅是它不支持。`benchmarks/run_benchmark.py`在标记的声明/来源对集合上衡量这一点(并演示`corroborate_claim`和`locate_claim_span`):
```bash
python benchmarks/run_benchmark.py # 离线启发式(确定性)
python benchmarks/run_benchmark.py --backend ollama # LLM裁判(需要ollama)
```
标记集上的离线启发式结果([benchmarks/REPORT.md](benchmarks/REPORT.md)):
| 集 | n | 准确率 | 无效声明捕获率 | 支持声明的精度 |
|-----|---|----------|------------------------------|--------------------------|
| 数据域(数字 + 事实) | 15 | 100% | 100% | 100% |
| 总体(包括语义) | 18 | 83% | 78% | 80% |
在其设计域 - 数字和事实数据声明 - 离线启发式从不祝福未经支持的声明,并且从不遗漏任何声明。其盲点是纯粹语义的否定/释义;对于这些,`evidence_entailment`与`backend="ollama"`(本地LLM裁判)弥补了这一缺陷。在自己的机器上运行`--backend ollama`行以评估该路径。
## 工具表面(42种工具)
<details>
<summary><b>发现和阅读</b>(9种工具)</summary>
| 工具 | 描述 |
|------|-------------|
| `web_search` | 密钥提供者(Tavily/Brave/Google)可用时,否则爬取Bing + DuckDuckGo。片段仅用于发现。 |
| `web_search_recent` | 搜索限制在时间窗口内(天/周/月/年)。 |
| `web_deep_search` | 搜索、获取、提取、重新排序并返回源上下文。 |
| `web_read` | 获取一个URL,提取文本,分类源质量,持久化缓存元数据。 |
| `scholarly_search` | 搜索arXiv(论文)或Wikipedia(百科全书)语料库。 |
| `web_archive_fetch` | 为死亡/更改的URL查找最接近的Wayback Machine快照。 |
| `web_fetch_authenticated` | 获取需要cookie或自定义标头的页面。 |
| `web_crawl` | 从起始URL开始的广度优先爬取,默认情况下在主机上(≤50页)。 |
| `generate_search_queries` | 生成操作查询(`site:`,`filetype:csv`,API/数据表变体)。 |
</details>
<details>
<summary><b>结构化数据</b>(9种工具)</summary>
| 工具 | 描述 |
|------|-------------|
| `web_extract_tables` | 将HTML表格解析为具有来源URL出处的行/列。 |
| `web_detect_downloads` | 检测链接的CSV/TSV/XLS/XLSX/PDF/JSON/XML文件。 |
| `web_parse_file` | 下载并解析CSV/TSV/XLS/XLSX/PDF/JSON。 |
| `web_fetch_json` | 获取直接API/JSON端点到解析的JSON。 |
| `check_date_completeness` | 验证所需的日期覆盖(天/周/月)。 |
| `resolve_units` | 检测货币,货币对,测量单位。 |
| `validate_unit_rows` | 拒绝具有不兼容单位或货币对的行。 |
| `reconcile_time_series` | 对齐系列在一个键上,计算增量,标记缺失键/异常值。 |
| `export_dataset` | 将合并的行写入`csv`/`xlsx`/`json`文件。 |
</details>
<details>
<summary><b>源质量和验证</b>(8种工具)</summary>
| 工具 | 描述 |
|------|-------------|
| `classify_source` | 分类官方/聚合器/博客/论坛/交互式/阻止/错误。 |
| `evidence_entailment` | 严格的声明与来源检查器:`heuristic`,`auto`,`ollama`,可选`local_nli`。 |
| `corroborate_claim` | 在摘录中三角测量声明(证实/冲突/单一来源/...)。 |
| `locate_claim_span` | 定位支持句子(s)与字符偏移和包含分数。 |
| `source_cache_get` / `source_cache_put` | 检查和写入持久性源缓存条目。 |
| `build_research_debug_report` | 查询,URL,源质量,验证间隙的紧凑报告。 |
| `startup_health_check` | 检查解析器,OCR,浏览器和缓存依赖项。 |
</details>
<details>
<summary><b>受控提取配方</b>(6种工具)</summary>
当通用解析器失败时,合成沙盒解析器:
| 工具 | 描述 |
|------|-------------|
| `tool_spec_propose` | 提出任务特定的提取配方规格。 |
| `tool_code_generate` | 生成启动`extract(source_text, input_payload)`配方。 |
| `tool_code_validate` | 验证配方代码与静态安全允许列表。 |
| `tool_code_run_sandboxed` | 在有限的子进程中运行验证的代码(仅JSON输出)。 |
| `tool_promote` | 保存验证的配方为可重用的内存(无需服务器编辑)。 |
| `recipe_registry` | 管理推广的配方:`list` / `get` / `run` / `delete`。 |
</details>
<details>
<summary><b>浏览器备用</b>(10种工具)</summary>
用于JS-heavy或交互式页面的受控Chromium会话:
| 工具 | 描述 |
|------|-------------|
| `web_navigate` · `web_snapshot` · `web_click` · `web_type` · `web_extract` · `web_scroll` | 通过稳定的元素引用驱动页面。 |
| `browser_set_date_range` · `browser_extract_tables` · `browser_extract_tables_for_date_range` | 设置日期范围,提交,提取可见表格。 |
| `web_screenshot` | 保存PNG和可选的OCR文本锁定在图像内。 |
</details>
## 搜索后端
`web_search`(以及在其上构建的所有内容)通过提供者层路由。 使用API密钥时,它使用该提供者;否则,它会爬取Bing + DuckDuckGo。 结果标准化为一种格式,无论后端如何。
| 提供者 | 环境变量 | 备注 |
|----------|----------|-------|
| Tavily | `TAVILY_API_KEY` | 面向LLM的搜索API。 |
| Brave | `BRAVE_API_KEY` | 独立的网络索引。 |
| Google | `GOOGLE_API_KEY` + `GOOGLE_CSE_ID` | 可编程搜索(自定义搜索JSON API)。 |
| Bing + DuckDuckGo | 无 | 默认备用;爬取,无密钥。 |
`auto`(默认)按顺序尝试每个密钥提供者,Tavily → Brave → Google,然后爬取。 使用`provider`参数强制使用一个(`tavily`/`brave`/`google`/`scrape`)。
**语义重新排序。** 在`web_search`中传递`semantic: true`以按含义而不是关键字重叠重新排序:它会过度获取,使用本地ollama模型嵌入查询和结果,并按余弦相似度排序(每个结果获得`semantic_score`)。尽力而为 - 如果ollama不可用,则返回原始顺序。 模型:`FOOTNOTE_EMBED_MODEL`(默认`bge-m3`)。
## 获取和反-bot阶梯
`web_read`通过升级阶梯获取(`scraper.py`):最便宜的方法首先运行,只有当结果看起来被阻止或为空时才会升级。 阻止/质量检测器决定何时升级;每个域的速率限制器,电路断路器和负缓存使其保持礼貌。 使用的层级和完整的尝试跟踪返回`fetch_tier` / `scrape_tiers`。
| 层级 | 方法 | 启用 |
|------|--------|-----------|
| 1 | HTTP(curl_cffi TLS伪装) | 始终 |
| 2 | 通过轮换代理的HTTP | 设置`FOOTNOTE_PROXIES` |
| 3 | 无头Chromium(运行JavaScript) | `FOOTNOTE_BROWSER_FALLBACK=1`(默认开启) |
| 4 | 通过代理的Chromium | 代理 + 浏览器 |
| 5 | 主机抓取API(Firecrawl / ScrapingBee) | 设置`FOOTNOTE_SCRAPE_API` |
无需配置即可使用普通HTTP路径以及JavaScript渲染页面的自动浏览器备用。
| 环境变量 | 默认值 | 用途 |
|---------|---------|---------|
| `FOOTNOTE_BROWSER_FALLBACK` | `1` | 将阻止的/JS页面升级到无头Chromium。 |
| `FOOTNOTE_PROXIES` | _(无)_ | 以逗号分隔的代理URL列表;每个域名粘性健康跟踪。 |
| `FOOTNOTE_SCRAPE_API` | _(无)_ | `firecrawl`或`scrapingbee`(需要匹配的API密钥)。 |
| `FOOTNOTE_DOMAIN_RPS` / `_BURST` | `3` / `5` | 每个域的速率限制(令牌桶)。 |
| `FOOTNOTE_BREAKER_THRESHOLD` / `_COOLDOWN` | `5` / `120` | 每个域的电路断路器。 |
| `FOOTNOTE_NEGCACHE_TTL` | `300` | 记住被阻止的URL的秒数。 |
| `FOOTNOTE_THIN_CONTENT_CHARS` | `200` | 在此提取长度下,脚本繁重的页面被视为JS壳。 |
## 运行时数据
```text
~/.footnote-mcp/source_cache/ # 持久页面缓存(带有出处)
~/.footnote-mcp/research_memory.json # 持久研究内存
```
使用`FOOTNOTE_SOURCE_CACHE=/path/to/cache footnote-mcp`覆盖缓存位置。
`check_date_completeness`支持日历`calendar`,`business_day`,`crypto_24_7`,`forex_weekday`,`us_business_day`和`ru_business_day`(传递显式`holidays`用于特定于源的那些;`us_`/`ru_`变体使用可选的`holidays`包)。
## 其他安装路径
**Docker** 将Chromium和tesseract捆绑在一起 - 无需其他安装:
```bash
docker build -t footnote-mcp .
docker run -i --rm footnote-mcp # 客户端启动它;参见下面的MCP配置
```
已发布的镜像可在GitHub容器注册表中找到:
```bash
docker run -i --rm ghcr.io/kazkozdev/footnote-mcp:1.1.0
docker run -i --rm ghcr.io/kazkozdev/footnote-mcp:latest
```
```json
{
"mcpServers": {
"footnote": {
"command": "docker",
"args": ["run", "-i", "--rm", "ghcr.io/kazkozdev/footnote-mcp:latest"]
}
}
}
```
**pipx / uvx** (独立安装入口点):
```bash
pipx install /path/to/footnote-mcp # 或:pipx install git+<仓库网址>
uvx --from /path/to/footnote-mcp footnote-mcp # 即席,无需安装
```
**OCR.** PDF/图像OCR使用`pytesseract` + 系统`tesseract`二进制文件(macOS上`brew install tesseract`)。 **本地NLI后端** 用于`evidence_entailment` `backend="local_nli"`:`pip install -r requirements-nli.txt`(模型通过`FOOTNOTE_NLI_MODEL`)。任一方式,`startup_health_check`报告实际可用的内容。运行时依赖范围在`pyproject.toml`中声明,并在`requirements.txt`中镜像。
## 测试
```bash
pip install -r requirements-dev.txt
python -m pytest -q # 离线单元测试 + 烟雾测试;无需网络或密钥
```
`tests/test_mcp_smoke.py`通过真正的MCP stdio启动服务器,并对本地HTTP夹具进行端到端测试工具;其余是解析器,获取梯度,搜索提供者和调度的离线单元测试。实时搜索测试是可选的:
```bash
RUN_LIVE_WEB_TESTS=1 python -m pytest -m live
```
CI运行相同的套件(`.github/workflows/tests.yml`)。
## 许可证
MIT - 参见[LICENSE](LICENSE)。