Cite Caddy
Cite Caddy is an MCP server that connects AI assistants to a Zotero reference library, providing comprehensive read/write access through dozens of tools. It supports full CRUD operations on items, collections, tags, notes, and attachments, with safety versioning and user warnings for destructive actions.
Read-only operations include searching items (with optional full-text search), retrieving item details, and listing collections, tags, trash, saved searches, groups, item types, fields, creator types, attachments, and notes. Users can download attachments and fetch extracted full-text content.
Safe write operations allow creating and updating items, collections, notes, saved searches, and attachments; managing tags (add, remove, set, rename); moving items to/from collections; and trashing/restoring items. All mutate operations check for version conflicts to prevent overwriting recent changes.
Destructive operations (to be used with caution) include permanent deletion of items, moving items to a different library (which breaks Word citations), and deleting collections, tags, or saved searches. Version checking applies, and warnings are given for citation-breaking actions.
The server can be run locally in single-user stdio mode or deployed remotely as a multi-tenant HTTP service with OAuth 2.1 authentication.
Provides full read/write access to a Zotero library, including search, add, update, delete, and move items; manage collections, tags, notes, attachments, saved searches, and trash; and retrieve Zotero item-type and field schema.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Cite CaddySearch my Zotero library for items tagged 'to-read'"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Cite Caddy
A reference-library bridge for AI assistants — a standalone, remote MCP server.
Independent, unofficial project. Not affiliated with, endorsed by, or sponsored by the Corporation for Digital Scholarship (Zotero) — see Zotero's trademark policy. Built on the Zotero Web API; today's backend is Zotero, but the name and tool surface are meant to support others later.
Cite Caddy gives full read/write access to a Zotero library — search, add, tag, update, delete, and move items; create, rename, and delete collections; upload/download attachments and read their extracted full text; read and write item notes; manage tags, trash, and saved searches library-wide; and look up Zotero's own item-type/field schema. 36 tools total — see Tools below for the full list.
Why this exists
Read-only tools that match findings against a Zotero library (e.g. by DOI/arXiv ID) can safely stop at reporting — they never need to write anything back. This project goes further on purpose: full CRUD against a Zotero library, including delete and move, so that tagging, adding, and cleaning up items can be automated too.
That's a deliberate scope choice, and it comes with a real risk: any write that changes an existing item's key (delete, move to another library, "clean library" reset) breaks Word documents that cite it via the Zotero Word plugin's live field codes — see "Key safety" below before touching delete/move.
Related MCP server: zotero-cli-cc
Key safety (read this before implementing delete/move)
Any Zotero item cited in a Word document via the Zotero Word plugin is referenced by that item's key, embedded in a live field code. Operations that preserve an item's key (create, update fields, add/remove tags, add notes) are safe. Operations that don't (delete, and library-to-library move, which Zotero implements as delete+recreate) will break those citations silently — the Word document won't error, it'll just show stale/broken field text next time someone updates fields or opens Zotero the next time.
Full CRUD was chosen deliberately for this project despite that risk. When implementing delete/move tools:
Make the destructive intent obvious in the tool name and docstring (an MCP client's model reads both before calling), not just in this README.
Consider requiring the caller to pass back the item's current Zotero
version(optimistic concurrency) so a delete/update can't silently clobber a change made concurrently from the Zotero desktop app or another client.A dry-run / confirmation step for delete is worth considering, but is an implementation decision for whoever builds that tool, not decided here.
Configuration
stdio mode (local, single-user — no $PORT): the library to connect to
comes from env vars.
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 mode (remote, multi-tenant — $PORT set): there's no single
configured library — each caller brings their own Zotero Library ID/Type/API
Key via the /login form (see "Deployment" below). Instead:
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)Optional in either mode:
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"
(that file must actually be served there). Left unset, both
fields are simply omitted.Deployment
Ships as its own Docker container (docker-compose.yml), meant to sit
behind a reverse proxy that terminates TLS and forwards to the container's
port on localhost. Remote/hosted by default, not a local stdio server — an
MCP client just points at the URL, nothing to install or run locally.
Access is gated by a real OAuth 2.1 authorization server built into the
app itself (app/oauth_provider.py), not HTTP Basic Auth in front of it.
This is a deliberate design choice: Claude Desktop/claude.ai's "Add custom
connector" UI is OAuth-first — it always tries the OAuth discovery +
authorization-code dance against a new server, so a plain 401 in front of
the server (as Basic Auth would produce) gets read as "this server needs
OAuth" and fails once it hits a nonexistent /authorize endpoint.
Implementing a real (if minimal) OAuth server is what makes "Add custom
connector" work.
Multi-tenant and self-service: /authorize doesn't delegate to a
third-party identity provider — it shows a first-party login form asking
for a Zotero Library ID, Library Type, and API Key. Submitting the form
validates the key directly against the Zotero API; a successful
validation both grants access and registers ("onboards") that library as
a tenant of this server, all in one step — there's no separate sign-up
and no admin approval. Any MCP client can dynamically register itself
(RFC 7591), but completing the login form with a working Zotero key is
what actually gates access. Each caller's tool calls are then routed to
their own Zotero library, not a shared one. See
app/oauth_provider.py's module docstring for the full flow. Registered
clients, issued tokens, and onboarded tenants' credentials (API keys
encrypted at rest with MCP_TOKEN_STORE_KEY) persist to MCP_DATA_DIR
(a Docker volume) so redeploys don't log connected clients out or forget
onboarded tenants.
.env on the host (not in this repo) holds MCP_TOKEN_STORE_KEY/
MCP_PUBLIC_URL, consumed via docker-compose.yml's env_file:.
ZOTERO_LIBRARY_ID/ZOTERO_LIBRARY_TYPE/ZOTERO_API_KEY are not needed
for the HTTP deployment — those only apply to stdio mode.
.github/workflows/deploy.yml automates redeploying to an already
set-up host: manual trigger only (workflow_dispatch, never on push),
runs the test suite first, then syncs the repo over SSH and rebuilds the
container. It needs its own GitHub Actions secrets for the deploy SSH
key and target host/port/user — see the workflow file for the full list.
Use a dedicated deploy key (not whatever key you use for direct/manual
access), so it can be revoked independently if it ever leaks.
Tools
36 tools total, grouped by risk (see "Key safety" above before using any
Destructive tool). Tools marked ✓ under Version require the
item's/collection's current Zotero version (from
search_items/get_item/list_collections) as an argument and refuse
the call if it's stale, rather than silently overwriting a concurrent
change.
Tool | Category | Version | Notes |
| Read-only |
| |
| Read-only | ||
| Read-only | ||
| Read-only | ||
| Read-only | ||
| Read-only | ||
| Read-only |
| |
| Read-only | ||
| Read-only | ||
| Read-only | Check before | |
| Read-only | Same, for | |
| Read-only | ||
| Read-only | ||
| Read-only | Content returned as | |
| Read-only | ||
| Safe write | ||
| Safe write | ||
| Safe write | ||
| Safe write | ✓ | |
| Safe write | ✓ | |
| Safe write | ✓ | |
| Safe write | ✓ | |
| Safe write | ✓ | |
| Safe write | Library-wide — acts on every item carrying the tag, not just one; no per-tag version. Can block on large libraries: #6. | |
| Safe write | ✓ | |
| Safe write | ✓ | |
| Safe write | ✓ | Reversible soft delete — undo with |
| Safe write | ✓ | |
| Safe write | Content sent as | |
| Safe write | ||
| Safe write | ✓ | |
| Destructive | ✓ | Breaks Word citations. |
| Destructive | ✓ | Recreates the item under a brand-new key in the target library, then deletes the original — breaks Word citations. |
| Destructive | ✓ | Cascades to sub-collections (matching Zotero's own "Delete Collection"); never deletes the items filed in them. |
| Destructive | Library-wide — acts on every item carrying the tag, not just one; no per-tag version. | |
| Destructive | Low-risk — a saved search is just a stored filter, never touches items or citations. |
Testing
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
pytestTests never call a live Zotero library, even if .env has real
credentials: app/zotero_service.py (all Zotero read/write logic) is
exercised against tests/fakes.py's in-memory FakeZotero, and
app/mcp_server.py's tool functions are tested directly against a
ZoteroService backed by that fake (see configure_service()).
Status
v1.5 — deployed and in active use, with full CRUD coverage of the Zotero Web API's item/collection/tag/trash/saved-search/schema surface (36 tools; see Tools). Add it as a remote MCP connector directly (e.g. Claude Desktop/claude.ai's "Add custom connector" with just the server's public URL) — the OAuth flow described above prompts for your own Zotero Library ID/Type/API Key in-browser, no manually-configured headers needed, and no admin sign-up step.
Listed in the official MCP Registry
as dk.herbertkokholm.citecaddy/cite-caddy — metadata lives in
server.json, published via mcp-publisher and DNS-verified
against citecaddy.herbertkokholm.dk. Not (yet) part of GitHub's separate,
manually-curated github.com/mcp directory, which
doesn't sync automatically from the open registry.
Known limitations
Tracked gaps against the MCP 2026-07-28 specification
("stateless core, enterprise authorization, extensions framework"). None
are currently exploitable or user-facing — each is either inert until an
upstream mcp SDK change, or already mitigated — but are documented here
so they're visibly known rather than silently absent.
OAuth authorization-response
issparam (RFC 9207) not sent. The spec hardens the OAuth flow against mix-up attacks by having the authorization server include anissparameter in the redirect back to the client (RFC 9207 §2.4), which spec-compliant clients then validate. This server's/loginflow builds its final redirect by hand incomplete_login()(app/oauth_provider.py) rather than through themcpSDK's built-in authorize handler, and currently omitsiss. Harmless today: the installedmcpSDK (mcp>=2.0.0,<3inpyproject.toml) never advertisesauthorization_response_iss_parameter_supportedin this server's OAuth metadata, so no compliant client requires it yet. Revisit if a future SDK version turns that advertisement on by default.Dynamic Client Registration (RFC 7591) instead of CIMD. The same spec update formally deprecates Dynamic Client Registration in favor of Client ID Metadata Documents (CIMD), though DCR remains functional for backward compatibility. This server's client auto-provisioning (
_FlexibleClientInformation/register_client/get_clientinapp/oauth_provider.py) is built on DCR — needed because some MCP clients (observed: Claude Desktop/claude.ai) skip registration and send/authorizean unregisteredclient_iddirectly (see that class's docstring). No action needed while the installed SDK keeps DCR working without warning; will need a CIMD-based replacement if/when that changes.rename_tagcan block on large libraries — tracked as #6; candidate for the spec's newtasksextension once the installed SDK exposes one.
Contributing
See CONTRIBUTING.md.
Security
See SECURITY.md for the threat model and how to report a vulnerability.
License
Maintenance
Related MCP Servers
- AlicenseCqualityDmaintenanceEnables interaction with Zotero libraries for searching, managing collections, items, tags, and attachments, plus optional semantic search across PDFs via local embeddings.Last updated382MIT
- Alicense-qualityAmaintenanceMCP server that exposes 45 tools for Zotero reference management, enabling AI agents to read/write items, search, extract PDF text, and manage workspaces via the Zotero CLI.Last updated194AGPL 3.0
- Flicense-qualityDmaintenanceEnables AI assistants to search and retrieve metadata, abstracts, and notes from a user's Zotero library through tools like search, get item, and list collections.Last updated
- Flicense-qualityAmaintenanceEnables MCP-capable agents to securely manage a local Zotero library through a plugin-hosted MCP endpoint, supporting read, write, search, and import/export operations with safety workflows like dry-run and approval.Last updated4
Related MCP Connectors
The everything Zotero MCP server — Web API v3 + local API, safe writes, citations, search.
User-owned memory for AI agents, Copilot, Claude, IDEs, CLIs, and chat apps over remote MCP.
Shared long-term memory vault for AI agents with 20 MCP tools.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/herbertkokholm/cite-caddy'
If you have feedback or need assistance with the MCP directory API, please join our Discord server