Cite Caddy

Full read/write access to your Zotero library —
for your own Claude, over MCP.

A standalone, remote MCP server that turns Zotero into something your AI client can act on directly: search, create, tag, update — even delete and move, with real safeguards. Covers the full library surface — items, collections, tags, trash, saved searches, and schema lookups — in 36 tools. Self-service and multi-tenant: sign in with your own Zotero Library ID and API key, no admin approval, no shared credentials. Nothing to install locally, gated by a first-party OAuth 2.1 flow built into the server itself.

Connect via MCP Key safety model View on GitHub

Unofficial, community-maintained project. Not affiliated with, endorsed by, or sponsored by the Corporation for Digital Scholarship (Zotero) — see Zotero's trademark policy.

How it works

Sign in with your own Zotero key — that's the whole sign-up

This server is multi-tenant and self-service, so /authorize doesn't delegate to a third-party identity provider — it's a first-party login form asking for your own Zotero Library ID, Library Type, and API Key. Validating that key against the live Zotero API is what gates access and onboards your library, in one step.

1. Add custom connectorThe MCP client points at the server's public URL and self-registers via dynamic client registration (RFC 7591) — no manual app setup.
2. Bring your own keyThe authorization screen asks for your Zotero Library ID/Type/API Key — no admin sign-up, no shared credentials.
3. Tool calls → your libraryAuthenticated calls are routed to your own Zotero library, keyed off your access token — never a shared one.
Read-only
search_items(query, full_text)
Search by title, creator, or tag — or opt into full-text search across attached files and notes.
Safe write
add_tags(key, version, tags)
Key-preserving — never breaks a Word citation to this item.
Destructive
delete_item_permanently(key, version)
Refuses the call if version is stale — no silent overwrite.

MCP access

Point your own Claude at the server

Add it as a remote MCP connector directly — the OAuth flow prompts for your own Zotero Library ID/Type/API Key in-browser, nothing to configure by hand, no admin sign-up step.

Also listed in the official MCP Registry as dk.herbertkokholm.citecaddy/cite-caddy.

Claude Desktop
  1. Settings → Connectors → Add → "Add custom connector"
  2. Paste the server's public URL with /mcp appended
https://your-domain.example/mcp
Claude Code
claude mcp add --transport http cite-caddy \
  https://your-domain.example/mcp
Available tools — 36 total
search_items / get_item / list_collections / list_tags / list_trashRead-only lookups against the connected library
list_saved_searches / list_groupsDiscover saved searches, and groups whose id can target move_item_to_different_library
list_item_types / list_item_fields / list_item_type_fields / list_item_creator_typesZotero's own schema — check before a create/update call, instead of guessing
list_attachments / get_fulltext / download_attachment / list_notesRead-only lookups for attachments and notes
create_item / update_itemAdd new items or edit fields — always key-preserving
add_tags / remove_tags / set_tags / rename_tagManage tags per-item or library-wide, without touching item keys
add_to_collection / remove_from_collectionReorganize safely, in-library only
create_collection / update_collectionCreate a collection, or rename/move (reparent) an existing one
create_saved_searchAdd a new saved search
trash_item / restore_from_trashReversible soft delete, and its undo
upload_attachment / create_note / update_noteAdd a file attachment or note, or edit a note in place
delete_item_permanently / move_item_to_different_libraryDestructive — requires the item's current version
delete_collection / delete_tag / delete_saved_searchPermanently remove a collection, a tag library-wide, or a saved search

Features

Built to be trusted with write access

Full CRUD, on purposeNot just search — create, tag, update, delete, and move, so tagging and cleanup can be automated too.
Optimistic concurrencyMutating calls on existing items require the current Zotero version; a stale one is refused, never silently overwritten.
First-party OAuth 2.1A real authorization server built into the app, not Basic Auth in front of it — so "Add custom connector" just works.
Dynamic client registrationAny MCP client can self-register (RFC 7591); completing the login form is what actually gates access.
Remote by defaultShips as its own Docker container behind a reverse proxy — nothing to install or run locally.
Self-service, multi-tenantBring your own Zotero Library ID/Type/API Key — the first successful login is sign-up, no admin approval step.
Per-tenant isolationEach caller's tool calls are routed to their own onboarded library, keyed off their access token — never a shared one.
Encrypted at restOnboarded tenants' Zotero API keys are encrypted (Fernet) in the token store, not kept in plaintext.
Tested against a fakeAll read/write logic runs against an in-memory FakeZotero in tests — the suite never touches a live library.

Key safety

Why delete and move need care

Full CRUD was a deliberate scope choice, and it comes with a real risk this project is built around rather than around ignoring.

Keys are load-bearingZotero's Word plugin cites items by key via live field codes. Anything that changes an item's key breaks that citation silently.
Delete and move are the riskMoving between libraries is delete+recreate under the hood — both change the key, both are flagged destructive in the tool name and docstring.
Version-gated writesDelete/move require the item's current version. A mismatch means it changed since — the call is refused, not forced through.
Safe ops preserve keysCreate, field updates, tags, notes, and collection membership never touch an item's key — safe to automate freely.
Deliberate, not accidentalThe risk was accepted with eyes open, not overlooked — see the README before extending delete/move further.
OAuth over Basic AuthChosen because Claude's connector UI always tries OAuth discovery first; a bare 401 gets read as "needs OAuth" and fails at a nonexistent endpoint.