github 54yyyu/zotero-mcp v0.7.0

4 hours ago

Added

  • update-db --allow-mass-deletion — the deletion pass now refuses to remove 25+ documents amounting to 25%+ of the syncing library's indexed documents in one run, a fingerprint far more likely to be a truncated item_versions() response or a sync-scoping regression than a real purge. Intentional purges (including emptying a library outright) opt in with this flag for a single run. It is deliberately a CLI flag rather than an environment variable, so no persisted configuration can disable the guard and the MCP server's unattended background sync can never mass-delete. A run whose deletion pass was skipped (guard, failed or empty item_versions()) does not promote the sync watermark, so the rerun actually re-enters deletion detection instead of hitting the unchanged-version early exit. The same opt-in now also gates --force-rebuild when the collection holds documents not attributed to the active library (a reset destroys the whole collection but repopulates only the active library) and the --force-rebuild --limit N combination.
  • Structured JSON annotation export. zotero_get_annotations(format="json") returns normalized annotation records with paper and attachment keys, page metadata, text, comments, color, tags, timestamps, and source. zotero_synthesize_annotations(format="json") returns the same records grouped by paper alongside structured notes and summary counts. The standalone CLI exposes this as zotero-cli annotations list --format json (#215). Records are normalized across all three annotation sources: page is always the page label, page_index always 0-based (Better BibTeX and direct PDF extraction report 1-based pages internally), and color_category is derived for every source rather than only the Better BibTeX one.

Changed

  • fastmcp is pinned to <4, and httpx is now a declared dependency. FastMCP 4 (currently 4.0.0b1) targets the stateless 2026-07-28 MCP revision and is a breaking upgrade for this server: it removes ctx.sample()/ctx.list_roots()/ctx.elicit(), raises the pydantic floor to 2.12, and replaces httpx with httpx2 throughout. That last change is the reason httpx moves into the dependency list — client.py imports it directly for the HTTP/1.1-pinned local Zotero transport, and until now relied on it arriving transitively through FastMCP. The CI test job installed fastmcp unpinned, so it would have picked up the 4.0 stable release on its next scheduled run and failed without a commit to this repo; it is pinned to match. The mcp dependency is dropped: nothing in src/ imports it, and FastMCP already pins the SDK version it needs. The FastMCP 4 migration is tracked separately and will not ship until 4.0 leaves beta.
  • zotero_synthesize_annotations groups its digest by item key instead of by title, so two distinct papers that share a title are no longer merged into one section. Markdown headings are qualified with the item key when a title collides.

Fixed

  • zotero_advanced_search sorting works, and says so when it cannot. Sorting by date ordered results by month name: Zotero's data["date"] is a display string ("October 1, 2016"), and it was compared lexically, so October preceded March preceded January regardless of year. Date sorts now use the API's normalized meta.parsedDate, falling back to the first four-digit year in the display string for sparse records that carry no parsedDate. Separately, a sort field absent from every result — misspelled, or simply not present in that backend's item shape — sorted every key to the empty string, leaving results in library order while reporting nothing; the tool now says the sort was not applied and names the sortable fields instead of returning a silently unsorted list (#418).
  • zotero_advanced_search honors collection conditions. The condition was advertised in the tool description but could never match: an item's membership is stored in data["collections"] (a list of collection keys), and extraction read a data["collection"] that does not exist, so every collection condition compared the empty string against the requested key. Any query containing one returned "No items found", including queries whose other conditions matched hundreds of items. Membership is now read from the right field, an item filed in several collections matches on any of them, and an item in no collection correctly satisfies isNot. collections is accepted as a spelling of the same field. Scoping is direct membership, matching Zotero's own "Collection is X" with subcollections not included. This was never mode-specific — advanced_search filters client-side on every backend, so the condition was equally broken against the web API (#418).
  • zotero_update_item writes fields that a type stores under a renamed key. Several Zotero item types keep a base field under a type-specific key — a statute's title is nameOfAct, a case's is caseName, a statute's date is dateEnacted — but the update path gated each field on whether the key was already present on the fetched item, using instance presence as a proxy for type validity. Setting title on a statute therefore did nothing and was reported as a skipped field, and the same held for every other base-field rename. Each generic parameter is now resolved to the type's actual field and validated against that type's declared field set, so a field that is valid but simply absent from the record is written rather than false-skipped (place on a journal article and accessDate on a book were both being refused this way). The mapping is a trimmed slice of Zotero's global /schema document vendored with the package, refreshed weekly in the background and on demand with zotero-mcp schema-refresh — the vendored copy is the floor, so resolution is correct offline and for local-only installs, and only item types Zotero adds after a release need the refresh. The citationKey special case from #321 is retired: it is an ordinary schema field and now validates like one (#402). The background refresh backs off for a day after a failure rather than retrying on every startup, and ZOTERO_MCP_SCHEMA_REFRESH=0 declines the network call outright — the vendored table is always a correct floor, so the only cost is not picking up item types Zotero adds after a release (#402).
  • Syncing one library no longer deletes every other library's documents from the semantic index (#404). The incremental deletion pass diffed every stored id against item_versions() of the active library, so the first incremental sync after multi-library indexing became possible treated all other libraries' documents as "no longer present in Zotero" and deleted them. Confirmed in the wild: a single update-db run removed 833 documents, 738 of them live group-library items — essentially the entire group portion of a multi-library index. Deletion candidates are now fetched DB-side with a where={"group_id": …} filter for the syncing library only, so another library's documents — and documents with no attribution at all — are structurally out of the pass's reach. Two failure modes that used to read as "the library is empty" now skip deletion instead of wiping: item_versions() raising (it previously degraded to an empty dict, turning a transient API failure into a full wipe of everything in scope) and item_versions() succeeding with an empty body against a non-empty index. The library identity a run tags, watermarks and deletes under is pinned once per run from the run's own Zotero client, so a concurrent zotero_switch_library can no longer re-point half an update at another library.
  • The multi-library group_id backfill actually runs — it called methods that did not exist. _backfill_group_ids() has invoked ChromaClient.iter_metadatas() and update_metadatas() since #396, but only the test suite's fakes defined them; the real class never did, so every production backfill died with AttributeError, no pre-existing document was ever tagged, and zotero_semantic_search(library_id=…) silently matched nothing on migrated indexes. The real methods exist now (id-snapshot paging and batch-split updates, per the #368/#369 precedents), regression tests run against the real ChromaClient, and a conformance test auto-discovers every Chroma-shaped fake in the suite and pins it to the real API so a fake-only method can never go green in CI again. The backfill's attribution also stopped guessing: a document is tagged only on positive evidence (the local database's key→group map, trashed items included so each library's own scoped pass cleans its trash, or membership in the active library's item_versions()), and documents with no evidence stay untagged — excluded from library-filtered search and, deliberately, from deletion. The failure log no longer recommends --force-rebuild as the repair: a rebuild re-embeds the whole collection and permanently drops any documents the current ingest paths cannot recreate. Users who already followed that advice have a freshly built, fully tagged index and need no action.
  • OpenAI Batch API imports no longer fail on large libraries. ChromaClient.upsert_documents already split upserts into chunks of client.get_max_batch_size() to stay under ChromaDB's batch limit, but upsert_embeddings — the method the batch-import path uses to write precomputed vectors — upserted the whole payload in a single call, so any import larger than ChromaDB's max_batch_size (~5461) failed outright (e.g. "Batch size of 6312 is greater than max batch size of 5461"). upsert_embeddings now chunks the same way (#410).

Upgrading: the semantic index migrates to schema version 3 on your first zotero-mcp update-db after upgrading — a metadata-only backfill, no re-embedding. If you run a multi-library index, upgrade before your next sync: 0.6.4's deletion pass can delete every other library's documents.

Install: uv tool install zotero-mcp-server or pip install zotero-mcp-server

Don't miss a new zotero-mcp release

NewReleases is sending notifications on new releases.