Windows servers that stalled on every tool call now work. Search spans every library you have instead of one at a time, and a batch import costs a bounded number of round trips instead of one set per item. Plus six PRs from @mronkko.
uv tool install --upgrade 'zotero-mcp-server[all]' # or: pip install -U zotero-mcp-serverUpgrading
One breaking change. zotero_semantic_search now scopes to the active library by default. It previously searched every indexed library whenever library_id was omitted, which made it the one search tool whose unqualified behaviour differed from the rest: the same question meant "this library" through two tools and "all libraries" through the third. Pass search_all_libraries=True for the old behaviour.
Fixed
On Windows, 0.10.0 was unusable (#485). Every tool call hung until the client gave up, around 120s. initialize and tools/list answered; tools/call never returned. It turned out to be three separate problems, and only the first was mine to find.
Two of them were the same mistake in two places: an import sitting above the config check that was documented as guarding it. So the server loaded ChromaDB and numpy on every startup no matter what the configuration said, in a worker thread, where on Windows that import wedges the whole process.
| before | after | |
|---|---|---|
| auto-update check, no update due | 5.2–13.8s, chromadb + numpy loaded | 0.001–0.003s, neither imported |
| reranker warmup, reranker disabled | 892ms, chromadb loaded | 3.6ms, not imported |
| first semantic search, fresh process | hung past 90s | 5.9s |
The third is stranger and the fix is honest about it. Even with both gates in place, importing chromadb inside the AnyIO worker thread that serves a tool call still wedges on Windows: py-spy shows one thread stuck in a native numpy DLL load, the main thread idle, and zero lock contention. The same import on the main thread takes about two seconds, every time. Nobody could reproduce it below the full server process, so serve now does that import on the main thread up front. It is a mitigation, not a root cause, and it is gated to Windows and to installs that actually configured semantic search, so it cannot undo the two fixes above.
Concurrent adds of the same identifier could create duplicates (#486). The API lock protects the single-threaded local API; it never promised to make check-then-create atomic, and narrowing it correctly removed that side effect wherever a CrossRef or page fetch now sits between the check and the write. Two parallel tool calls could both pass dedup and both create, with no version conflict for anything downstream to catch. Adds now serialize on the normalized identifier, so ISBN-10 and ISBN-13 of one book take the same lock, held across the check and the create only and never across third-party network work.
Every date shown by the SQLite search backend was wrong. Zotero stores date as "<ISO> <original text>" and the web API returns only the display half. The backend stripped that correctly for search conditions but not for the item it handed back, so you got 2025-05-29 2025-05-29 where you had typed an ISO date, and 2017-00-00 2017 where you had not. Found by running the backend against a real Zotero 10 library rather than a fixture: all 113 dated items disagreed with the API before the fix, all 113 agree after.
A semantic-search hit from another library could be found but not read (#163). Results are hydrated through a client bound to one library, so a hit from any other one 404'd and came back as Could not fetch full item data. The index knew about the paper and the tool could not show it. Foreign hits are now hydrated from zotero.sqlite in one batched query. No re-index needed.
Added
zotero_add_item takes a batch. doi/url/isbn accept a list or a comma/newline-separated string. One bad DOI in a batch of ten does not fail the other nine, and single-item output is byte-for-byte unchanged. It is not N single adds: importing 244 DOIs costs 5 CrossRef requests, 5 creation POSTs and at most 11 template fetches, against 244 of each before.
Global search across every library, on zotero_search_items, zotero_advanced_search and zotero_semantic_search via search_all_libraries=True (--all-libraries on zotero-cli). Each result is labelled with where it came from. Requires ZOTERO_SEARCH_BACKEND=sqlite and refuses without it rather than falling back, because the Zotero API can only search one library per request and a result that looks global but is not is the failure this exists to prevent.
The SQLite backend understands the boolean tag DSL. ["methods OR methodology", "-draft"] now compiles to SQL. Independently of the above, this removes a silent fallback that cost every single-library SQLite user the backend's speed on every tagged query.
Gemini Batch API, behind one provider-neutral --batch flag. Batch embedding runs at roughly half the realtime price. --openai-batch, --no-openai-batch, openai-batch-status and openai-batch-import all keep working as deprecated aliases, so anything scripted against 0.10.0 is unaffected.
Parallel embedding requests, paced against a provider token budget. Set semantic_search.embedding_config.max_parallel_requests and indexing overlaps document preparation, embedding and ChromaDB commits instead of running them in lockstep. Without it the sequential path is unchanged.
Thanks
@mronkko for six PRs this cycle, including the batching work, global search, both embedding splits, and a same-day implementation of the add-serialization fix.
@llrllr0123 and @w-clary for #485. The first report arrived with a py-spy dump and a root cause already traced; the follow-up isolated two further causes one at a time in a clean venv and came with a measured table. Both then verified the fixes on their own Windows machines, which I do not have. That issue would not be closed without them.
Full detail in CHANGELOG.md.