Tomato Downloader MCP
# Tomato Downloader MCP
面向 MCP 客户端的番茄小说下载器适配层。提供 **24 个工具**,支持搜索、书库查询、本地正文读取、章节时间、下载任务及可恢复的批量下载。支持 **stdio** 和原生 **Streamable HTTP**,无需 Supergateway。
本项目不是小说下载引擎,也不包含番茄账号登录、订阅管理或推送通知。它连接你已经部署的 [Tomato-Novel-Downloader](https://github.com/zhongbai2333/Tomato-Novel-Downloader),读取同一台机器上的下载数据。兼容性基线为下载器 **v2.4.16** 的 Web API;其他版本需要验证。
## 下载引擎来自哪里
下载引擎是上游作者 **zhongbai2333** 发布的独立项目,不是本仓库编译的程序:
- [上游源码仓库](https://github.com/zhongbai2333/Tomato-Novel-Downloader)
- [上游全部 Releases](https://github.com/zhongbai2333/Tomato-Novel-Downloader/releases)
- [本项目验证并固定使用的 v2.4.16 Release](https://github.com/zhongbai2333/Tomato-Novel-Downloader/releases/tag/v2.4.16)
- Linux x86_64:`TomatoNovelDownloader-Linux_musl_amd64-v2.4.16`
- Linux ARM64:`TomatoNovelDownloader-Linux_musl_arm64-v2.4.16`
Docker 构建直接从以上 GitHub Release 下载对应架构文件,并核验 Dockerfile 中固定的 SHA-256(与该 Release 的资产 digest 一致)。使用 musl 构建以减少对宿主机 glibc 版本的依赖。不是下载 Source code ZIP 后在本地编译,也不会自动跟随 latest 升级。
本仓库的 v0.2.x Release 附件是 **MCP 的 Node 包**,不是上游下载引擎。手动部署 Windows/macOS 时,从上游 Release 选择 Win64、WinArm64、macOS_amd64 或 macOS_arm64 资产,然后独立启动下载器;MCP 的手动配置见下文。
## 推荐:Docker Compose 一起部署
只需 Git、Docker 和支持 `--wait` 的 Docker Compose v2+,不需要在宿主机安装 Node 或 Rust。Windows/macOS 使用 Docker Desktop 的 **Linux containers** 模式。镜像支持 Linux amd64/arm64;当前端到端验证为 Linux amd64。
```sh
git clone https://github.com/luqing233/tomato-downloader-mcp.git
cd tomato-downloader-mcp
sh scripts/deploy-docker.sh
```
Windows PowerShell 将最后一行替换为:
```powershell
powershell -ExecutionPolicy Bypass -File scripts/deploy-docker.ps1
```
首次运行自动生成随机 Web UI 密码和 MCP Token,保存到被 Git 忽略的 `.env.docker`;重复执行保留原配置。脚本从源码构建两个镜像,并启动三个服务:
| 服务 | 作用 |
| ---------- | ----------------------------------- |
| downloader | 上游 v2.4.16 下载引擎和 Web UI |
| mcp | 原生 Streamable HTTP MCP,24 个工具 |
| worker | 离散批次后台执行与恢复 |
默认入口:
- 下载器 Web UI:[http://127.0.0.1:18423/](http://127.0.0.1:18423/),密码读取 `.env.docker` 中的 `TOMATO_WEB_PASSWORD`。
- MCP:`http://127.0.0.1:8001/mcp`,请求头为 `Authorization: Bearer <.env.docker 中的 TND_HTTP_TOKEN>`。
外部客户端配置可参考 `examples/mcp-http.json`。若端口已占用,在 `.env.docker` 中修改 `WEBUI_PORT/MCP_PORT`,再执行脚本。端口默认只发布到宿主机回环地址;服务器上的浏览器访问需要 SSH 隧道,其他容器访问需要配置共同网络或合适的代理。
三个容器共享名为 `tomato-data` 的 Compose 数据卷(实际名称带项目名前缀),保存配置、日志、下载正文和批次状态。新部署自动设置下载路径为 `/data/downloads`,已有配置不会被覆盖;默认章节并发为 2,可在 Web UI 调整。容器程序文件系统只读,数据卷和临时目录可写,避免上游自更新在容器内替换已校验的二进制;升级应修改固定版本及校验值后重新构建。
```sh
# 查看服务状态和日志
docker compose --env-file .env.docker ps
docker compose --env-file .env.docker logs --tail 100
# 停止并保留数据卷
docker compose --env-file .env.docker down
# 拉取本项目更新后重新构建;上游版本仍由 Dockerfile 固定
git pull
docker compose --env-file .env.docker up -d --build --wait
```
迁移或备份时同时保留数据卷和 `.env.docker`。`down -v` 会删除数据卷;普通停止和更新不要加 `-v`。首次构建需要访问 Docker Hub、GitHub Releases 和 npm registry;校验失败会终止构建。
## 手动部署:已有下载器时使用
要求:Node.js **22.9+**、已运行的下载器 Web UI,以及对下载目录的读写权限。MCP 和 worker 必须能访问同一个 Web API、下载目录和批次文件。
```sh
git clone https://github.com/luqing233/tomato-downloader-mcp.git
cd tomato-downloader-mcp
npm ci
```
复制 `.env.example` 为 `.env`,设置:
```dotenv
TOMATO_WEB_BASE=http://127.0.0.1:18423
TOMATO_WEB_PASSWORD=your-downloader-web-password
TND_DATA_DIR=/absolute/path/to/downloader/downloads
```
`TOMATO_WEB_PASSWORD` 是下载器 Web UI 的密码,不是番茄账号密码。Windows 路径建议使用 `E:/books/downloads`。
### stdio:桌面 MCP 客户端
将 `examples/mcp-stdio.json` 中的绝对路径和环境变量替换为你的配置。通用格式:
```json
{
"mcpServers": {
"tomato-downloader": {
"command": "node",
"args": ["/absolute/path/to/tomato-downloader-mcp/server.mjs"],
"env": {
"TOMATO_WEB_BASE": "http://127.0.0.1:18423",
"TOMATO_WEB_PASSWORD": "your-downloader-web-password",
"TND_DATA_DIR": "/absolute/path/to/downloader/downloads"
}
}
}
}
```
读取、搜索及直接创建单章/区间/整本任务不依赖 worker。使用 `tnd_start_discrete_downloads` 时,另开一个终端运行:
```sh
npm run worker
```
worker 独立执行,MCP 客户端退出不会中断批次。一个共享下载目录只需一个 worker;推荐使用 systemd 保持运行。
### Streamable HTTP:远程或 Docker 内的客户端
在 `.env` 中设置 `TND_HTTP_TOKEN`,然后运行:
```sh
npm run http
```
默认地址为 `http://127.0.0.1:8001/mcp`,所有路径(包括 `/healthz`)都要求 `Authorization: Bearer <TND_HTTP_TOKEN>`。`examples/mcp-http.json` 提供 AstrBot 风格配置;各客户端字段名称可能不同。
HTTP 模式为无状态请求传输,不保留会话子进程,不提供 SSE 订阅或通知。并发请求仍必须使用各自的 JSON-RPC ID。默认仅监听本机;跨机器访问推荐 SSH 隧道或带 TLS 的反向代理。Docker 中的 `127.0.0.1` 指向容器自身,需要使用可达的宿主机地址。
已有 Supergateway 部署可继续运行 `server.mjs`,也可以切换到 `http-server.mjs`;HTTP 模式兼容使用 `SUPERGATEWAY_API_KEY` 作为 Token 的旧环境文件。切换传输后客户端需要重连。
## 推荐调用流程(v0.3.0)
读取正文优先使用 `tnd_get_chapters`,默认只读本地。只有 `download_missing:true` 才会创建或复用下载批次;立即返回已有正文、缺失 ID 和 `batch_id`,不等待下载。取消请求返回 `canceling` 时应继续查询,不能当作已经停止。重试会返回新批次 ID。
完整字段、状态与错误处理见 [客户端接入说明](docs/CLIENT_INTEGRATION.md)。这 5 个新工具与原有工具兼容,MCP 与 worker 需同时升级。
## 工具
| 工具 | 作用 |
| ------------------------------ | ------------------------------------------------ |
| `tnd_get_chapters` | 统一获取:本地优先,可显式下载缺失章节并复用批次 |
| `tnd_check_chapters` | 纯本地检查正文有效性与缺失列表 |
| `tnd_cancel_batch` | 持久化取消请求,可选择是否取消当前任务 |
| `tnd_retry_batch` | 只重试终态批次中仍缺失的失败/取消目标 |
| `tnd_get_diagnostics` | 下载器、共享目录、权限及 worker 心跳诊断 |
| `tnd_get_status` | 下载器运行状态、版本和配置摘要 |
| `tnd_search_books` | 搜索书籍,不管理订阅 |
| `tnd_preview_book` | 书籍详情和章节总数 |
| `tnd_get_library` | 本地文件列表;只有 `start:true` 才启动扫描 |
| `tnd_get_updates` | 查询更新扫描;`start:true` 启动扫描 |
| `tnd_get_history` | 下载历史 |
| `tnd_get_jobs` | 下载任务;对有目标凭据的已完成任务核验本地正文 |
| `tnd_get_book_status` | 本地可用章节数、失败记录数和最新可用章节 |
| `tnd_list_downloaded_chapters` | 按目录顺序列出本地有正文的章节 |
| `tnd_read_latest_chapter` | 读取按目录排序的最后一个本地可用章节 |
| `tnd_read_downloaded_chapter` | 按 `item_id` 或本地 `index` 读取一章 |
| `tnd_read_downloaded_chapters` | 按本地范围或 `item_ids` 读取,最多 20 章 |
| `tnd_get_chapter_times` | 在线目录和 `firstPassTime`,不是最后编辑时间 |
| `tnd_start_download` | 连续目录区间;不传范围则整本 |
| `tnd_start_book_download` | 整本下载 |
| `tnd_start_chapter_download` | 按目录位置下载单章 |
| `tnd_start_discrete_downloads` | 按目录位置或 `item_ids` 创建离散批次,最多 50 章 |
| `tnd_get_batch_status` | 当前批次进度及逐组错误 |
| `tnd_wait_job` | 等待任务,遇到输入选择立即返回 |
### 章节标识:优先使用 item_id
- `item_id`:章节身份,推荐跨工具传递。
- 读取工具的 `index`:本地**有正文**列表按目录排序后的 1-based 槽位。
- 下载工具的 `index/indexes/range_*`:在线完整目录的 1-based 位置。
- `chapter_position`:在线目录位置;`chapter_order`:上游 `realChapterOrder`。
本地只下载第 183、207 章时,读取列表的 `index` 是 1、2,下载目录位置仍是 183、207。不要将读取 `index` 直接传给下载工具。
本地正文来自 `status.json` 加 `downloaded_chapters.jsonl`,不转换 EPUB。空正文和失败记录不会被当成可读章节。按 `item_id` 读取可在目录不可用时返回本地正文;最新章节、列表和按 index 读取缺少可信目录时返回 `DIRECTORY_UNAVAILABLE`,不会猜测顺序。
### 批量读取
```json
{
"book_id": "1234567890123456789",
"item_ids": ["1111111111111111111", "2222222222222222222"],
"max_chars_per_chapter": 2000,
"max_total_chars": 10000
}
```
范围读取使用 `start_index/end_index`,与 `item_ids` 二选一。单次最多 20 章;总字符预算默认 100000,0 表示不限制。当前任一目标缺失会使整个批量读取失败;总预算截断可能使后面的章节正文为空,调用方需检查每章的 `truncated`。
### 下载与连续章节合并
调用 `tnd_start_discrete_downloads`:
```json
{
"book_id": "1234567890123456789",
"indexes": [41, 42, 43, 47, 48, 60]
}
```
先按章节身份跳过已有正文,然后只合并连续的剩余位置:
```text
41–43 → 一个任务
47–48 → 一个任务
60 → 一个任务
```
不会扩展成 41–60,不会为了合并而下载中间未请求章节。每组提交前重新核对目录与目标 ID,发生变化则安全失败。`jobs` 数量表示区间组数,`current` 表示已处理目标章节数,不再等于完成组数。
状态为 `queued / running / waiting_input / canceling / done / done_with_errors / failed / canceled`。在 Web UI 完成书名或格式选择后,worker 自动继续原任务。成功需要本地目标正文核验通过,不能仅依赖下载器的 `done`。重试有上限,认证失败不重试;提交结果未知时停止,避免盲目重复提交。
直接下载工具会保存目标凭据,`tnd_wait_job/tnd_get_jobs` 在任务完成后核验正文并返回 `success_count/failed_count/missing_item_ids`。旧任务或非本 MCP 创建的任务没有凭据,返回 `verification:unavailable`,不能据此断言所有章节已成功。
## 错误与边界
失败使用 MCP `isError:true`,正文包含结构化 `code/message/details`。常见代码:
```text
INVALID_ARGUMENT INVALID_RANGE BOOK_NOT_FOUND
CHAPTER_NOT_FOUND CHAPTER_NOT_DOWNLOADED DIRECTORY_UNAVAILABLE
DIRECTORY_CHANGED BATCH_BUSY BATCH_NOT_FOUND AUTH_FAILED
DOWNLOADER_BUSY UPSTREAM_LIMITED UPSTREAM_ERROR
JOB_NOT_FOUND JOB_FAILED CHAPTER_DOWNLOAD_INCOMPLETE
SUBMISSION_UNKNOWN LOCAL_DATA_INVALID INTERNAL_ERROR
BATCH_ACTIVE BATCH_TARGETS_UNKNOWN CANCEL_FAILED JOB_TRACKING_FAILED
```
- 当前保留单活跃批次,完成后新批次会替换当前批次文件,不提供批次归档 API。
- 下载器与 MCP 是独立项目。本项目在 MCP 边界拒绝非法范围,并核验下载结果,**没有修改下载器二进制**;直接在下载器 Web UI 调用不经过这些保护。
- 上游目录与下载器读取目录之间没有共享快照或事务,校验无法消除目录瞬间变化的所有风险。
- worker 的跨进程锁带心跳,进程被强制杀死后旧锁最长约 120 秒才可回收。不要手工删除活跃锁。
- 下载器重启后任务 ID 可能复用;worker 核对书籍和时间,并根据本地正文恢复。提交任务与保存任务 ID 之间仍无后端幂等事务。
- 本地数据是可信输入;本项目不是多租户或不可信用户共享服务。不提供内容授权或解锁能力,请仅处理你有权使用的数据。
## Linux 后台运行
`examples/systemd/` 包含 HTTP MCP 和 worker 两个通用单元。假设项目安装在 `/opt/tomato-downloader-mcp`,Node 位于 `/usr/bin/node`,系统账号为 `tomato`:
1. 将 `.env` 配置放在 `/etc/tomato-downloader-mcp.env`,设置账号可读、其他用户不可读的权限。
2. 修改 unit 中的 Node 路径、账号和下载目录;确保服务账号能读写目录。
3. 安装单元并执行 `systemctl daemon-reload`。
4. 执行 `systemctl enable --now tomato-downloader-mcp tomato-downloader-batch`。
后端下载器需要独立安装并运行。示例不覆盖下载器安装,也不携带任何服务器凭据。
## 开发与验证
```sh
npm ci
npm run check
npm test
npm audit --omit=dev
npm pack --dry-run
```
测试使用隔离的临时目录、模拟目录/API,以及真实 SDK 的 stdio 和 HTTP 传输;不会访问个人书库或启动真实下载。CI 在 Linux、Windows 和 Node 22/24 上运行。
## 来源与许可
MCP 适配层使用 MIT License。下载引擎来自独立的 [Tomato-Novel-Downloader](https://github.com/zhongbai2333/Tomato-Novel-Downloader),本仓库不复制其 Rust 源码或 Release 二进制。项目需求最初源于对 [mcp-server-fanqie](https://github.com/fysh1010/mcp-server-fanqie) 的使用分析,本仓库不包含其账号、签名或登录实现。
TDQS
Scored across 24 tools
Descriptions do a lot of work to separate reading vs listing vs checking (many explicitly state '不返回正文'), but the four download-start tools (tnd_start_download, tnd_start_discrete_downloads, tnd_start_book_download, tnd_start_chapter_download) overlap heavily and an agent can easily pick the wrong one. Similarly tnd_read_downloaded_chapter(s) vs tnd_get_chapters (local-first batch read with optional download) is a subtle boundary. Read-side tools like tnd_get_book_status, tnd_preview_book, tnd_list_downloaded_chapters and tnd_check_chapters also cluster around the same object with only prose distinguishing them.
All tools follow a uniform tnd_ prefix plus snake_case verb_noun convention (get_, list_, read_, start_, cancel_, retry_). Minor deviation: the generic tnd_start_download sits awkwardly beside the more specific tnd_start_book_download / tnd_start_chapter_download / tnd_start_discrete_downloads, making the verb_object pattern slightly non-parallel. Otherwise very predictable.
24 tools is at the heavy end for a novel downloader; the batch/job lifecycle (start_, wait_, get_batch_status, cancel_batch, retry_batch) plus four separate download-entry points inflates the surface. Each tool has a stated role, but several could be collapsed (e.g. a single start_download with a scope parameter). Borderline over-scoped.
Core lifecycle is covered: search, preview, start download, poll/wait, batch cancel/retry, read local chapters, diagnostics. However, tnd_search_books notes it does '不管理订阅', implying a subscription feature with no corresponding tools, and there is no way to delete/clean library files, cancel a single job, or modify configuration (status only reads the config summary). Notable gaps for a downloader agent.