Five read-path bugs fixed, a machine-readable CLI, and a packaged agent skill that installs into any harness with one command. Plus 16 community PRs.
uv tool install --upgrade 'zotero-mcp-server[all]' # or: pip install -U zotero-mcp-serverFixed
zotero_get_pdf_outline was broken on every PDF for every new install (#455). The child process that reads the table of contents did import fitz; PyMuPDF ≥1.28 ends its legacy fitz shim with a deprecation notice written to stdout, and the parent parses that stdout as JSON. The floor is pymupdf>=1.24.2, so every fresh install resolved a version that does this. Fixed by importing pymupdf and sentinel-delimiting the payload — a sitecustomize hook or a C-level MuPDF write can pollute fd 1 too, and neither is catchable from inside the child.
zotero_advanced_search could take the server down with it for five minutes (#456). It had no server-side filtering at all: it paged the entire library 100 items at a time, evaluating conditions in Python, while holding the process-global API lock. Every other tool queued behind it. Now pushes itemType/tag to the API, exits early once limit is met when no sort is requested, and bounds the rest with a 20s budget that reports partial results honestly.
A collection over 100 items could not be enumerated (#453), and the tool advised raising a limit that did nothing past 100 with no offset to fall back on. Ceiling raised, offset added, and the footer now names the next offset to pass.
Notes were unreadable through all three tools that claim to read items (#447). get_item_metadata rendered # Untitled, get_notes on a note's own key said "No notes found", get_item_fulltext looked for an attachment a note doesn't have.
Type-specific title and date fields (#452). A case's title is caseName and its date dateDecided; a statute's are nameOfAct and dateEnacted. Every formatter read title/date directly and printed "Untitled" / "No date" over data Zotero holds correctly. As the reporter put it, that makes an item findable but unidentifiable.
Two tool parameters were unreachable from clients that stringify arguments (#459), thanks to @AmirF194. Sweeping every tool for the same shape found a second one, zotero_advanced_search.conditions, whose description promised "also accepts a JSON string" and whose body had the json.loads branch — both unreachable, because Pydantic rejected the string at the boundary.
Added
zotero-cli --json — one stable envelope per invocation, accepted on either side of the subcommand name. Read commands return real structure; errors arrive on stdout too, so one stream carries both outcomes. zotero-cli --json-schema prints the contract.
Nine new CLI commands — read, attach, delete, export, related, coverage, synthesize, path, batch, plus ann update/ann delete.
An agent skill, installable into any harness with one command:
zotero-mcp install-skillIt detects what's set up in your project and writes to each in that harness's own format: Claude Code skill directories, Cursor .mdc and Windsurf .md rules, and a managed block in AGENTS.md or GEMINI.md (Codex, Amp, OpenCode, Jules, Gemini CLI). Nothing is overwritten without --force, and in a shared instructions file only the text between the zotero-cli markers is ever touched.
Why it matters — measured, not estimated:
| Route | In context | Paid |
|---|---|---|
| MCP server, default profile (38 tools) | 13,448 | every request |
| Agent skill, frontmatter only | 98 | always |
| Agent skill, body loaded | 1,368 | when it fires |
~137× cheaper before either is used, ~10× once the skill fires. Re-measure with python scripts/measure_context_cost.py. This is the fixed context cost only — it says nothing about task success or round trips, and a cheaper surface that gets the answer wrong is not cheaper.
ZOTERO_SEARCH_BACKEND=sqlite — an opt-in direct-SQL metadata backend (#167). On a ~57,000-item library an advanced_search with a creator and date range goes from 27.4s (local API) / 467.6s (web API) to 0.08s. Falls back silently to the existing path for anything it doesn't cover.
Thanks
@mronkko, @AmirF194, @skhyun-ocean, @Yeyang28, @mihiarc, @raffaelemancuso, @schmidma, @feima3333, @dbuchber, and everyone who filed a reproducible bug report this cycle. Several of the fixes above landed because the report arrived with a root cause already traced.
Full detail in CHANGELOG.md.