关于 Cite Caddy
Cite Caddy 是供 AI 助手读写 Zotero 文献库的远程 MCP 桥接服务。其 39 个工具涵盖文献条目、分类、附件、笔记、标签和保存的搜索,并为版本冲突及重试操作提供保护。
主要功能
- 01搜索、创建、更新、删除和移动条目
- 02管理分类、标签和保存的搜索
- 03上传、下载附件并读取提取文本
- 04读写条目笔记
- 05部分写入操作支持版本检查和幂等键
适用场景
- 01研究时添加并标记参考文献
- 02读取文献库附件的提取文本
- 03整理分类和保存的搜索
- 04响应不明确时重试 Zotero 操作
MCP 相关
通过远程 HTTP 服务或本地 stdio 模式向 MCP 客户端提供 39 个 Zotero 文献库工具。HTTP 访问使用 OAuth 2.1,并将每位调用者的操作路由到其已接入的文献库。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
https://citecaddy.herbertkokholm.dk/mcpclaude mcp add --transport http dk-herbertkokholm-citecaddy-cite-caddy https://citecaddy.herbertkokholm.dk/mcp{"mcpServers":{"dk-herbertkokholm-citecaddy-cite-caddy":{"type":"http","url":"https://citecaddy.herbertkokholm.dk/mcp"}}}在 Cursor 中打开{"servers":{"dk-herbertkokholm-citecaddy-cite-caddy":{"type":"http","url":"https://citecaddy.herbertkokholm.dk/mcp"}}}在 VS Code 中打开{"mcpServers":{"dk-herbertkokholm-citecaddy-cite-caddy":{"type":"http","url":"https://citecaddy.herbertkokholm.dk/mcp"}}}{"mcpServers":{"dk-herbertkokholm-citecaddy-cite-caddy":{"serverUrl":"https://citecaddy.herbertkokholm.dk/mcp"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
详细介绍
Cite Caddy
面向 AI 助手的参考文献库桥接工具——一个独立的远程 MCP 服务。
独立的非官方项目,与数字学术联盟(Zotero)无关联、未获其认可,也未受其赞助—— 请参阅 Zotero 的商标政策。 基于 Zotero Web API 构建;目前的后端是 Zotero,但项目名称和 工具接口旨在日后支持其他服务。
Cite Caddy 可对 Zotero 文献库进行完整的读写操作——搜索、添加、 标记、更新、删除和移动条目;创建、重命名和删除 分类;上传/下载附件并读取提取出的全文;读取和写入条目笔记;在整个文献库范围内管理标签、回收站和已保存的搜索, 并查询 Zotero 自身的条目类型/字段架构。共 39 个工具——完整列表请见下文的工具。
为什么要开发这个项目
只读工具可以将发现结果与 Zotero 文献库进行匹配(例如通过 DOI/arXiv ID),并安全地止步于报告结果——它们无需写回 任何内容。本项目有意更进一步:对 Zotero 文献库提供完整的 CRUD 操作,包括删除和移动, 以便也能自动完成标记、添加和清理条目。
这是有意做出的范围选择,但确实存在风险:任何会更改现有条目 key 的写操作(删除、移动到另一个文献库、 “清理文献库”重置)都会破坏通过 Zotero Word 插件的动态域代码引用该条目的 Word 文档——执行删除/移动前,请先阅读下文的“Key 安全性”。
Key 安全性(实现删除/移动前请先阅读)
在 Word 文档中通过 Zotero Word 插件引用的任何 Zotero 条目,都是通过该条目的 key 来引用的;这个 key 嵌入在动态域代码中。保留条目 key 的操作(创建、更新字段、添加/移除标签、添加笔记)是安全的。不保留 key 的操作(删除,以及 Zotero 通过删除并重新创建实现的跨文献库移动)会悄无声息地破坏这些引用——Word 文档不会报错,只会在下次有人更新域代码或再次打开 Zotero 时显示过时或损坏的域文本。
尽管存在这种风险,本项目仍有意选择提供完整的 CRUD 功能。实现删除/移动工具时:
- 在工具名称和文档字符串中明确标示破坏性操作(MCP 客户端的模型在调用前会同时读取二者),不要只在本 README 中说明。
- 可以考虑要求调用方传入条目当前的 Zotero
version(乐观并发控制),以避免删除/更新操作悄无声息地覆盖通过 Zotero 桌面应用或其他客户端并发进行的更改。 - 可以考虑为删除操作增加试运行/确认步骤,但这是工具构建者的实现决定,本项目不作规定。
幂等性
delete_item_permanently、delete_collection、delete_tag、
delete_saved_search、move_item_to_different_library 和
update_publication_status 均接受可选的 idempotency_key。在因响应丢失或不明确(例如网络超时)而重试调用时,传入相同的不透明字符串,即可重放原始结果(无论成功还是出错),而不会再次对 Zotero 执行操作。
如果使用同一个键调用时参数不同,则会报错,而不是静默返回旧结果,因此可以为每个逻辑请求生成一个键,并在重试该请求时放心地重复使用。
这对 move_item_to_different_library 尤为重要:它会在目标库中重新创建条目,然后将其从源库中删除。如果创建成功,但删除随后失败,直接重试就会重新执行整个操作——由于源条目的版本没有变化,因此会在目标库中再创建一个重复条目。使用 idempotency_key 后,重试会重放缓存的失败结果(以及“手动清理”的指引),而不会再次操作 Zotero。
缓存位于每个服务进程的内存中(HTTP 模式下按已接入的租户隔离),TTL 为 24h——它能保留此时间窗口内的重试结果,但在重新部署或重启后不会保留。
配置
stdio 模式(本地、单用户——未设置 $PORT):要连接的库由环境变量指定。
ZOTERO_LIBRARY_ID numeric library ID (user or group)
ZOTERO_LIBRARY_TYPE "user" or "group" (default: user)
ZOTERO_API_KEY from Zotero -> Settings -> Security -> Applications
(needs write permission, not just read)
HTTP 模式(远程、多租户——已设置 $PORT):没有单一的预配置库——每位调用者通过 /login 表单提供自己的 Zotero Library ID/Type/API Key(见下文“部署”)。具体如下:
MCP_TOKEN_STORE_KEY Fernet key encrypting onboarded tenants' API keys at
rest; generate once at deploy time with:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
MCP_PUBLIC_URL public HTTPS URL this server is reachable at
MCP_DATA_DIR where OAuth clients/tokens/tenants persist (default: ./.data)
两种模式下均可选:
MCP_WEBSITE_URL public site reported as serverInfo.website_url; also used to
build serverInfo.icons[0].src as MCP_WEBSITE_URL + "icons/icon.svg"
and the /login page's privacy policy link as
MCP_WEBSITE_URL + "privacy.html" (both files must actually be
served there). Left unset, all three are simply omitted.
MCP_SECURITY_CONTACT security contact (email or URI) served as /.well-known/security.txt
(RFC 9116), e.g. for MCPBundles' ownership verification. Left
unset, that path 404s.
部署
以独立的 Docker 容器形式提供(docker-compose.yml),设计为部署在反向代理之后,由反向代理终止 TLS 并将请求转发到 localhost 上容器的端口。默认采用远程/托管方式,而非本地 stdio 服务——MCP 客户端只需指向该 URL,无需在本地安装或运行任何内容。
访问权限由应用本身内置的真实 OAuth 2.1 授权服务器控制(app/oauth_provider.py),而非通过前置 HTTP Basic Auth 控制。这是经过深思熟虑的设计选择:Claude Desktop/claude.ai 的“Add custom connector”界面优先使用 OAuth——它总会尝试与新服务端进行 OAuth 发现和授权码流程,因此,服务端前置的普通 401(Basic Auth 会产生这种响应)会被解读为“此服务端需要 OAuth”,并在请求到不存在的 /authorize 端点时失败。正是通过实现真正的(虽然精简的)OAuth 服务端,才让“Add custom connector”能够正常工作。
支持多租户和自助服务:/authorize 不会将请求转交给第三方身份提供商——它会显示一个第一方登录表单,要求填写 Zotero Library ID、Library Type 和 API Key。提交表单后,系统会直接通过 Zotero API 验证密钥;验证成功后,既会授予访问权限,也会将该库注册(“引导入驻”)为此服务端的租户,一步完成——无需单独注册,也无需管理员审批。任何 MCP 客户端都可以动态注册自身(RFC 7591),但只有使用有效 Zotero 密钥完成登录表单,才能真正获得访问权限。之后,每个调用者的工具调用都会路由到其自己的 Zotero 库,而不是共享库。完整流程请参阅 app/oauth_provider.py 的模块文档字符串。已注册的客户端、已签发的令牌以及已入驻租户的凭据(API 密钥使用 MCP_TOKEN_STORE_KEY 加密后静态存储)都会持久化到 MCP_DATA_DIR(Docker 卷),因此重新部署不会使已连接的客户端退出,也不会遗忘已入驻的租户。
主机上的 .env(不在此仓库中)保存 MCP_TOKEN_STORE_KEY/MCP_PUBLIC_URL,并通过 docker-compose.yml 的 env_file: 加载。HTTP 部署不需要 ZOTERO_LIBRARY_ID/ZOTERO_LIBRARY_TYPE/ZOTERO_API_KEY——这些配置仅适用于 stdio 模式。
.github/workflows/deploy.yml 可自动将服务重新部署到已配置的主机:仅手动触发(workflow_dispatch,绝不会在推送时触发),先运行测试套件,然后通过 SSH 同步仓库并重建容器。该工作流需要单独配置 GitHub Actions 机密,用于提供部署 SSH 密钥和目标主机/端口/用户名——完整列表请参阅工作流文件。请使用专用部署密钥(不要使用直接/手动访问时所用的密钥),这样即使密钥泄露,也可以单独撤销。
监控
4 个无需身份验证的 GET 端点,仅限 HTTP 模式(都需要 $PORT,
与 /login 相同):
-
/healthz— 返回纯文本200 OK,用于负载均衡器/运行状态检查。 -
/status— 聚合的进程级活动 JSON 快照:{ "version": "2.1.0", "uptime_seconds": 41213, "tenants": 7, "tool_calls": {"search_items": 512, "add_tags": 41}, "tool_errors": {"add_tags": 2} }tenants是TokenStore.tenant_count(),即已接入的 Zotero 文库数量。tool_calls/tool_errors是所有工具名称的调用数/错误数,统计范围为所有租户,由tools/call中间件(app/mcp_server.py中的_track_tool_call)记录。此响应中不会显示任何按租户或文库细分的数据——这使该端点可以安全地保持无需身份验证,不像那 39 个工具。计数器位于app/metrics.py中,仅保存在内存里:每次重启/重新部署时都会重置为零。这不是指标/分析系统,只是一个轻量的“服务是否正常运行、大致有多忙”的信号。 -
/status.html— 与/status数据相同,但渲染为简洁页面,包含名称和图标、一张显示版本/运行时长/租户数的表格,以及一张显示各工具调用数/错误数的表格,供用户在浏览器中查看,而非供脚本读取。只有设置了MCP_WEBSITE_URL才会显示图标,与/login相同。 -
/.well-known/security.txt— RFC 9116 安全联系信息,仅在设置了MCP_SECURITY_CONTACT时提供(否则返回 404)。Expires按请求计算为一年后,因此不会过期。MCPBundles 会读取此文件,在将服务发布到其目录时验证所有权。 -
/.well-known/mcp/server-card.json— 连接前的发现文档,包含服务身份、身份验证要求,以及带有架构的完整工具列表。每次请求时都会根据实际工具注册表实时生成,因此内容不会与注册表脱节。这不是已批准的标准:它是对 SEP-2127(“MCP Server Cards — HTTP Server Discovery”,取代已撤回的 SEP-1649)的务实近似方案。截至本文撰写时,该提案仍处于开放、未合并状态;甚至其 well-known 路径在不同草案中也有所变化(部分版本使用.well-known/ai-catalog.json)。这并非对符合规范的一般性声明;如果/当 SEP-2127(或后续提案)以不同约定正式获批时,此处可能需要调整。
工具
共 39 个工具,按风险分组(使用任何破坏性工具前,请参阅上文“关键安全事项”)。在 Version 下标记 ✓ 的工具要求将条目/集合当前的 Zotero version(来自 search_items/get_item/list_collections)作为参数;如果版本已过期,就会拒绝调用,而不是静默覆盖并发更改。
| 工具 | 类别 | 版本 | 说明 |
|---|---|---|---|
search_items | 只读 | query 默认为 Zotero 的快速搜索(标题/创作者/年份);full_text=True 也会匹配附件和笔记的已索引内容(qmode="everything"),且要求 query 非空。每个结果的 creators 都是由 {creatorType, firstName, lastName}(或单字段/机构创作者对应的 {creatorType, name})条目组成的列表;格式与 create_item/update_item 接受的格式相同,并保留角色(作者、编辑、译者等)。 | |
get_item | 只读 | creators 格式如上。 | |
list_collections | 只读 | ||
list_tags | 只读 | ||
list_trash | 只读 | creators 格式如上。 | |
list_saved_searches | 只读 | ||
list_groups | 只读 | id 也可作为 move_item_to_different_library 的 target_library_id(配合 target_library_type="group")使用。 | |
list_item_types | 只读 | ||
list_item_fields | 只读 | ||
list_item_type_fields | 只读 | 在 create_item/update_item 之前检查特定 item_type 接受哪些字段,而不是猜测。 | |
list_item_creator_types | 只读 | 同上,检查 creators 条目的 creatorType。 | |
list_creator_fields | 只读 | 可用于 creators 条目本身的名称格式字段(firstName、lastName、name 等);这与 list_item_creator_types(角色)不同。 | |
list_attachments | 只读 | ||
get_fulltext | 只读 | ||
download_attachment | 只读 | 内容以 content_base64 返回;服务无法访问调用方的本地文件系统。 | |
list_notes | 只读 | ||
export_bibliography | 只读 | 为一组条目键导出格式化的 HTML 参考文献/引文条目(采用指定的 CSL style),或可移植的导出数据(csljson、bibtex)。未知键会被静默忽略。 | |
create_item | 安全写入 | ||
create_collection | 安全写入 | ||
create_saved_search | 安全写入 | ||
update_collection | 安全写入 | ✓ | |
update_item | 安全写入 | ✓ | |
update_publication_status | 安全写入 | ✓ | 预印本 → 已发表:就地修改字段,并且这是唯一会就地修改 item_type 的操作。接受 idempotency_key——参见上文“幂等性”。 |
add_tags | 安全写入 | ✓ | |
remove_tags | 安全写入 | ✓ | |
set_tags | 安全写入 | ✓ | |
rename_tag | 安全写入 | 作用于整个文库——会修改所有带有该标签的条目,而非仅一个条目;没有按标签划分的版本。大型文库上可能会阻塞:#6。 | |
add_to_collection | 安全写入 | ✓ | |
remove_from_collection | 安全写入 | ✓ | |
trash_item | 安全写入 | ✓ | 可撤销的软删除——使用 restore_from_trash 撤销;除非之后被永久删除或清空回收站,否则不会破坏 Word 引文。 |
restore_from_trash | 安全写入 | ✓ | |
upload_attachment | 安全写入 | 内容以 content_base64 发送;服务无法访问调用方的本地文件系统。 | |
create_note | 安全写入 | ||
update_note | 安全写入 | ✓ | |
delete_item_permanently | 破坏性操作 | ✓ | 会破坏 Word 引文。接受 idempotency_key——参见上文“幂等性”。 |
move_item_to_different_library | 破坏性操作 | ✓ | 在目标文库中以全新键重新创建条目,然后删除原条目——会破坏 Word 引文。接受 idempotency_key,强烈建议在此使用——参见上文“幂等性”。 |
delete_collection | 破坏性操作 | ✓ | 级联删除子集合(与 Zotero 自身的“Delete Collection”一致);绝不会删除其中归档的条目。接受 idempotency_key。 |
delete_tag | 破坏性操作 | 作用于整个文库——会处理所有带有该标签的条目,而非仅一个条目;没有按标签划分的版本。接受 idempotency_key。 | |
delete_saved_search | 破坏性操作 | 风险较低——保存的搜索只是存储的筛选条件,不会触及条目或引文。接受 idempotency_key。 |
测试
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
pytest
测试绝不会调用实时 Zotero 文献库,即使 .env 中有真实凭据也是如此:app/zotero_service.py(所有 Zotero 读写逻辑)会针对 tests/fakes.py 中的内存版 FakeZotero 进行测试;app/mcp_server.py 的工具函数则会直接针对由该假对象支持的 ZoteroService 进行测试(参见 configure_service())。
状态
v2.3 — 已部署并投入使用,完整覆盖 Zotero Web API 的条目、分类、标签、废纸篓、已保存搜索和架构操作(39 个工具;参见工具)。可直接将其添加为远程 MCP 连接器(例如,在 Claude Desktop/claude.ai 中使用“Add custom connector”,只需提供服务的公开 URL)——上文所述的 OAuth 流程会在浏览器中提示你输入自己的 Zotero 文献库 ID/类型/API 密钥,无需手动配置请求头,也无需管理员注册。
已列入官方 MCP Registry,名称为 dk.herbertkokholm.citecaddy/cite-caddy——元数据位于 server.json,通过 mcp-publisher 发布,并由 DNS 对照 citecaddy.herbertkokholm.dk 完成验证。目前尚未列入 GitHub 单独维护、由人工整理的 github.com/mcp 目录,该目录不会自动与开放注册表同步。
已知限制
针对 MCP 2026-07-28 规范(“无状态核心、企业授权、扩展框架”)跟踪的差距。目前这些问题均无法被利用,也不会影响用户——每一项要么在上游 mcp SDK 变更前不起作用,要么已有缓解措施——但我们在此记录,以便明确说明,而非默默遗漏。
-
已发送 OAuth
iss参数(RFC 9207),但未声明支持。complete_login()中构建的最终授权重定向(app/oauth_provider.py)包含iss,可增强流程抵御混淆攻击的能力(RFC 9207 §2)。/.well-known/oauth-authorization-server中对应的authorization_response_iss_parameter_supported标志未设置:该文档由mcpSDK 生成,而截至mcp2.3.0,该 SDK 尚无相关设置。没有该标志时,客户端无需验证iss,但会验证它的客户端仍可获得这项保护。 -
使用动态客户端注册(RFC 7591),而非 CIMD。 同一规范更新正式弃用动态客户端注册,转而采用客户端 ID 元数据文档(CIMD),不过 DCR 仍可为向后兼容而正常使用。本服务的客户端自动配置(
app/oauth_provider.py中的_FlexibleClientInformation/register_client/get_client)基于 DCR 构建——这是必要的,因为某些 MCP 客户端(已观察到:Claude Desktop/claude.ai)会跳过注册,直接向/authorize发送未注册的client_id(参见该类的文档字符串)。只要已安装的 SDK 继续支持 DCR 且不发出警告,就无需采取措施;如果这一情况发生变化,则需要以 CIMD 为基础进行替换。 -
rename_tag在大型文献库上可能会阻塞 — 作为 #6 跟踪;可考虑采用规范中的tasks扩展。mcp2.3.0 有通用扩展框架,但尚未实现 Tasks。
贡献
请参阅 CONTRIBUTING.md。
安全
有关威胁模型及如何报告漏洞,请参阅 SECURITY.md。
隐私
有关你在 /login 登录时服务会存储哪些信息(Zotero 文献库 ID/类型/API 密钥)、这些信息如何受到保护,以及如何要求删除这些信息,请参阅隐私政策。
许可证
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。