Added
- The open-access PDF cascade asks OpenAlex for repository copies. When
zotero_add_itemor a batch import attaches a PDF for a DOI, it now tries everypdf_urlOpenAlex lists after Unpaywall and before the publisher'scitation_pdf_url, open-access locations first. OpenAlex knows institutional repository copies that Unpaywall does not count as open access. For Maraun 2013 in J. Climate, DOI 10.1175/jcli-d-12-00821.1, Unpaywall has nothing and the AMS page offers a 2-page preview, while OpenAlex lists the full 7-page PDF in the GEOMAR repository. A URL already tried under one source is not downloaded again under the next. Thanks @phil-c137. - Markdown and YAML files can be attached (#703).
zotero-cli attachandadd fileaccept.md,.yamland.yml, stored astext/markdownandtext/yamlso Zotero's own full-text indexer picks them up..jsonstays a CSL-JSON import. Requested by @rizakardas. zotero-cli --json get fulltextsays when a read was cut short (#701).data.truncatedis always present, and a capped read addspages_readandpage_count, so a script no longer has to parse the TRUNCATED heading out ofdata.text.
Changed
- Semantic search is its own toolset, and hidden on installs without the
[semantic]extra (#572, #689).zotero_semantic_searchmoves from core into a newsemantictoolset that is on by default, so default installs see no change.ZOTERO_MCP_TOOLSETS=nonenow leaves it out too; usenone,semanticto keep it. A spec that usesnonewithout namingsemanticlogs one warning at startup saying so;none,-semanticleaves it out without the warning.semanticandsearch-adminare left out automatically when ChromaDB isn't installed, so a base install no longer advertises tools that can only reply with install instructions; naming them inZOTERO_MCP_TOOLSETSkeeps them. The connectorsearchkeeps its keyword fallback. Reported by @zhuziqi97; thanks @falcon1-1-code. - The literature-review and contradicting-evidence prompts fall back to
zotero_search_itemswhen semantic search is not usable (#693).zotero_literature_reviewandzotero_find_contradicting_evidencealways told the model to callzotero_semantic_search, which fails without the[semantic]extra and is absent when a toolset spec leaves it out. They now check at render time (ChromaDB found withfind_spec, not imported, and the tool's toolset enabled) and otherwise namezotero_search_itemswithqmode='everything', one key term per query. With semantic search usable the text is unchanged. Thanks @falcon1-1-code. zotero_advanced_searchrejects a field name it does not know (#694). A typo such astitelwas a valid-looking condition that matched nothing, so the tool answered "No items found matching the search criteria" and the mistake went unnoticed. It now returns an error naming the field, up to three close matches (Did you mean: title?) and the supported operations. Thanks @falcon1-1-code.zotero_advanced_searchaccepts a single condition sent as a bare object (#695).{"field": "title", "operation": "contains", "value": "x"}instead of a list of one was rejected with a raw pydantic "Input should be a valid list" error, and the same object as a JSON string answered "No search conditions provided". It is now read as a one-condition list. An empty object is still an error. Thanks @falcon1-1-code.
Fixed
- A startup semantic update no longer holds up the MCP handshake (#706). With
auto_update: trueandupdate_frequency: "startup"(or a duedailyrun), the server ran the update in a worker thread of its own process, where native calls that hold the GIL (ChromaDB, loading or downloading the embedding model) and, on Windows, DLL loads stall the event loop as well: Claude Desktop'sinitializewent unanswered until the update finished, timed out, and shut the server down. The update now runs as a childzotero-mcp update-dbprocess (with--fulltextin local mode), its output relayed to the server's log, and the server answers at once. The update lock still keeps it from racing a manualupdate-dbor a second server, and an update unfinished at shutdown is stopped and resumes on the next start. Reported by @marius-ne. - The Zotero Agent plugin finds the agent CLIs on Windows (#700). Detection ran
command -vthrough/bin/sh, so on Windows every backend failed withFile at path "/bin/sh" does not exist, or is not executable. There the plugin now searches PATH withwhere.exe(PATHEXT aware, takingcodex.cmdrather than npm's extension-less shim), reads the environment fromcmd.exe /c set, runs npm's.cmdshims by the script or.exethey wrap, makes its folders with Node instead ofmkdir, and stops agents withtaskkill. macOS and Linux are unchanged. Translating with pi is still unavailable on Windows. Reported by @patrickjcrawford. This is untested on a real Windows machine; reports welcome on #700. - In local mode the DOI duplicate check reads
zotero.sqlite(#704, #705). It ran aneverythingquick search per DOI, which against the local API is a full-text scan of the library and pinned Zotero at 100% CPU for minutes on a batch import. The DOI is now looked up in the database, about a millisecond per DOI, with the old search as the fallback when the database cannot answer. A candidate the API cannot fetch is skipped instead of failing the import. Thanks @mronkko. zotero-cli --json searchreports a failed search as an error (#691, #696). An error came back asok: true, count: 0, the same as no matches; it is nowok: falsewith exit code 1, also for a failed semantic search or a collection that does not exist. Thanks @RizgarOzan.urlconditions work in SQLite advanced search (#701). The field was missing from the SQL map, so with--all-libraries, which has no API fallback, everyurlcondition failed. Reported by @jstotero.zotero-cli edit --collectionshelp says it replaces (#701). It read "Add to collections", but the tool replaces the item's memberships, so following the help removed the item from its other collections. The help now says so and points atcollections manage --add-to.gemini-embedding-2returns one vector per text (#697, #698). The batch went out as one list, which that model reads as a single multi-part input and answers with one aggregated vector, so an index built with it was wrong. Each text is now its ownContent.gemini-embedding-001is unchanged. Reported by @Faustoneiros; thanks @lots-o.- Restoring after an update no longer replaces the semantic index (#699). The restore step deleted the index and copied the backup back, so a partial backup replaced a complete index, along with anything else in a shared
persist_directory. It now copies only when the index is missing. Thanks @lots-o. - YAML attachments are read as text (#703, #707).
.yaml,.ymlandapplication/yamlattachments were skipped by full-text extraction. Thanks @kskszq.