Skip to main content

zotero-agent

Full local control of your Zotero library — from your terminal or your AI agent.
No cloud, no account, no API key. Your library never leaves your machine.

PyPI CI Python 3.10+ AGPL-3.0 Docs

zot gives you full read-write control of your local Zotero library: search, bulk-edit metadata, tag, deduplicate, import by DOI/ISBN/arXiv, export, enrich missing fields, and summarize PDFs into notes — scriptable from the shell or drivable by an AI agent. It ships as a CLI, an MCP server, a Claude Code skill, and portable AGENTS.md instructions.

zotero-agent driving a real 2,865-item Zotero library from the shell

Why it's different. Zotero's local HTTP API is read-only, so the popular tools (zotero-mcp, pyzotero) can only write through the zotero.org web API — which needs an account, an API key, and sync. zotero-agent writes locally through a tiny token-protected bridge plugin. Local, offline, private — and it finally does the bulk metadata editing people have asked Zotero for since 2016. See the honest comparison.

flowchart BT
    U(["👤 you / AI agent"])
    subgraph SURF ["zotero-agent (one package)"]
        direction LR
        MCP["MCP server (zot mcp)"]
        SKILL["agent skill"]
        CLI["zot CLI"]
    end
    READ["Zotero local API<br/>GET /api/… — fast, read-only"]
    WRITE["bridge plugin<br/>POST /zotero-agent<br/>privileged JS · token-gated"]
    LIB[("your local<br/>Zotero library")]

    U --> SURF
    SURF -->|read| READ
    SURF -->|write| WRITE
    READ --> LIB
    WRITE --> LIB

Works with your agent

Anything that speaks MCP or can run a shell command can drive your library:

Tool How
Claude Code zot skill install (bundled skill), or claude mcp add zotero-agent -- zot mcp
Claude Desktop add zot mcp to claude_desktop_config.json
Codex CLI ~/.codex/config.toml MCP entry, or zot skill agents-md > AGENTS.md and it calls zot
Gemini CLI ~/.gemini/settings.json MCP entry
Cursor .cursor/mcp.json MCP entry
OpenCode / Windsurf / any MCP client generic stdio server: command: "zot", args: ["mcp"]
Local models / custom agents run the zot CLI directly (see AGENTS.md) — no MCP needed

Per-client setup: docs/ai-agents.md · website.

Install

# 1) the CLI (pick one)
uv tool install zotero-agent            # recommended
uv tool install "zotero-agent[mcp]"     # + the MCP server (zot mcp)
uv tool install "zotero-agent[mcp,toc]" # + PDF outlines (zot toc)
pipx install "zotero-agent[mcp]"
brew install alex-roc/tap/zotero-agent  # macOS/Linux; ships both extras

# 2) the bridge plugin in Zotero — download the XPI (this link is permanent):
#    https://github.com/alex-roc/zotero-agent/releases/latest/download/zotero-agent-bridge.xpi
#    Tools → Plugins → gear → "Install Plugin From File" → that .xpi
#    One-click, no restart. Zotero auto-updates it from then on.

# 3) wire it up
zot init      # generates a token, writes config, auto-detects your userID
zot ping      # local API up? bridge answering? plugin version? userID known?

# 4) optional: the Claude Code skill (bundled in the package)
zot skill install           # → ~/.claude/skills/zotero  (--project for one repo)

Updating: uv tool upgrade zotero-agent (or brew upgrade zotero-agent) for the CLI; the plugin updates itself (Zotero polls the release manifest), and zot ping shows both versions.

Requires Zotero 7+ (tested through 9.x) running with the local API enabled (the default), and Python 3.10+. Full guide: docs/install.md.

Use it from the shell

zot search "bolivia" --limit 10               # fast local read
zot missing abstract --collection SS5MVVB6    # items lacking a field
zot stats                                     # library analytics
zot add doi 10.1371/journal.pmed.0020124 --pdf   # import + attach an OA PDF
zot dedupe --by title --fuzzy                 # find near-duplicate titles
zot enrich --field doi --dry-run              # fill missing DOIs from Crossref
zot apply edits.jsonl                         # declarative batch edit (undoable)
zot undo last                                 # roll it back
zot tag normalize --dry-run                   # fold case/space tag variants
zot export "My Collection" --format bibtex --out refs.bib
zot bib ABCD1234 @smith2020 --style apa       # formatted bibliography (Zotero key or citekey)

Every command takes --json for scripting. Writes refuse to run non-interactively without --yes. Full reference: docs/commands.md.

Commands at a glance

Group Commands
Read / analyze search get cite pdf collections tags export missing author stats recent bib annotations related notes lint
Edit / organize add dedupe tag (add/rm/rename/purge/normalize) set move collection note attach pdf-fetch
PDF outlines toc (show/scan/set/auto/clear) — needs the [toc] extra
Scanned PDFs pdf-prep (split double pages, OCR, shrink) — needs [toc] + OCRmyPDF
Batch (undoable) apply undo enrich
Setup / escape ping init skill backup sync restart exec mcp completion

Batch edits are undoable

zot apply takes a JSONL script (one edit per line) and snapshots every touched item first, so you can reverse it:

{"key":"ABCD1234","set":{"date":"2021"},"addTags":["review"]}
{"key":"@smith2020","addToCollection":"To Read","removeTags":["old"]}
zot apply edits.jsonl --dry-run   # preview (runs NO JS — cannot write)
zot apply edits.jsonl             # apply, snapshotting first
zot undo last                     # restore exactly the prior state

This is how agents do LLM-assisted cleanup safely: the model decides the values and writes the JSONL; zot performs the writes. The CLI never calls an LLM itself.

Give a PDF a real table of contents

Zotero's reader has an Outline tab, but it can only display bookmarks a PDF already has — and most scanned books and reports have none. zot toc builds one and writes it into the file, so the sidebar works in Zotero and everywhere else.

uv tool install --force "zotero-agent[toc]"   # the PDF engine is an extra
zot toc show ABCD1234                 # what the file already has
zot toc scan ABCD1234                 # what it could have, and from which evidence
zot toc auto ABCD1234 --dry-run       # build one deterministically, preview it
zot toc set  ABCD1234 --from toc.txt  # write your own (title<TAB>page, indented)
zot undo last                         # restore the previous outline

Detection prefers the book's own contents page over guessing from fonts — those titles and that nesting are the publisher's. When the contents page prints page numbers rather than linking, zot toc maps them onto physical pages using /PageLabels, the folios printed on each page, and a title search to confirm each row. That matters more than it sounds: front matter is numbered i, ii, iii and the body restarts at 1, so a single offset is wrong for half the book.

Same loop from an agent: zot toc scan --json hands over the evidence, the model decides the hierarchy, zot toc set --from - writes it. Writes are guarded by --yes, previewable with --dry-run, and reversible with zot undo.

Make a scanned book usable

A book scanned on a flatbed arrives as one landscape page per pair of printed pages, with no text layer: you cannot search it, cite from it, or let an agent read it, and it is several times larger than it needs to be. zot pdf-prep does the whole pass — split, OCR, shrink — without the file leaving the item.

brew install ocrmypdf tesseract-lang unpaper jbig2enc   # apt: ocrmypdf tesseract-ocr-spa unpaper
zot pdf-prep @bowlesIntroduccionEconomia --dry-run   # what it sees, what it would do
zot pdf-prep @bowlesIntroduccionEconomia             # split + OCR, attached beside the original
zot pdf-prep --collection "Scans" --replace          # a whole shelf; trash each original
zot pdf-prep --collection "Scans" --prune            # or trash the originals later, once reviewed

A real example — Bowles, Introducción a la economía, 147 two-up pages at 200 dpi: 294 pages, 24.7 MB → 16.4 MB, searchable, in under three minutes.

The split is the part that usually needs a human with Briss, and the reason is that the binding is never exactly centred. pdf-prep measures the ink profile of sampled pages, takes the median of the ones with enough ink to be informative, and applies that one cut to the whole book — the same decision a human makes, for the same reason: on a near-blank page the "widest gap" is anywhere, so a per-page cut eventually slices a chapter opening in half. Each half keeps a sliver past the cut, so the ±2% a real binding wanders never clips a letter. Pages that must stay whole (covers, fold-outs) are listed with --single.

Nothing is thrown away by default: the processed PDF is attached beside the original and tagged pdf-prep, because OCR is a machine's judgement call and the scan is often the only copy. --replace trashes the original as it goes; --prune does it afterwards for items already processed, so a large library does not quietly end up holding two copies of every book. Both use Zotero's trash, and both leave an annotated original alone: Zotero anchors highlights to the attachment and to page coordinates, so they cannot follow a file whose pages have just been split apart (--trash-annotated overrides). Prepare a scan before you read it and the question never comes up.

Use it from an agent

Ask your agent things like "tag every abstract-less item #review and merge duplicate titles in collection X", "fill in missing DOIs", or "summarize this paper's PDF chapter by chapter and save it as a note." It drives zot and follows a safe workflow (backup → sync-off → dry-run → small batch), and batch edits stay undoable.

Security

The bridge runs arbitrary privileged JavaScript inside Zotero — deliberately, because it's the only complete local write path. It's gated by a required token, a browser-origin/CSRF guard, loopback-only binding, and an append-only audit log. This is a real capability: read docs/security.md before installing. Report issues privately via SECURITY.md.

Documentation

Contributing / development

git clone https://github.com/alex-roc/zotero-agent.git && cd zotero-agent
./install.sh                            # dev shim on PATH + skill (symlinked) + XPI + zot init
python3 -m unittest discover -s tests   # tests: fake Zotero server, no network
uvx ruff check src tests cli/zot scripts
uv build                                # wheel + sdist
bash plugin/build.sh                    # rebuild the bridge XPI

The core is stdlib-only; the optional surfaces ride behind extras ([mcp] for the MCP server, [toc] for the PDF outline commands). See CONTRIBUTING.md and CHANGELOG.md. License: AGPL-3.0-or-later.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zotero_agent-0.7.0.tar.gz (891.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zotero_agent-0.7.0-py3-none-any.whl (130.1 kB view details)

Uploaded Python 3

File details

Details for the file zotero_agent-0.7.0.tar.gz.

File metadata

  • Download URL: zotero_agent-0.7.0.tar.gz
  • Upload date:
  • Size: 891.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for zotero_agent-0.7.0.tar.gz
Algorithm Hash digest
SHA256 5fa2697df3deb7277ae957922eee24b72a1715a77a465c6bad22e78333774a5b
MD5 3a98d6a3b4cad32d5b3c4d07ea6fff42
BLAKE2b-256 d0a4e476ec261a9ac647c4da4e41529f82791d43504456b2f97340bf2e683ba2

See more details on using hashes here.

Provenance

The following attestation bundles were made for zotero_agent-0.7.0.tar.gz:

Publisher: release.yml on alex-roc/zotero-agent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zotero_agent-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: zotero_agent-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 130.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for zotero_agent-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0620ee9eec35371a47522499b0021fb0cf3559ff1c93086ff0e1903827e8df15
MD5 3dc0380686dd004cfea8472b7ba8118d
BLAKE2b-256 0f20ddd8e263ed16967e7b289c1e5edbb3efca264c068510f602829d80eb1e48

See more details on using hashes here.

Provenance

The following attestation bundles were made for zotero_agent-0.7.0-py3-none-any.whl:

Publisher: release.yml on alex-roc/zotero-agent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page