Security
- HTTP transports reject requests for a foreign host (DNS rebinding). Served over
streamable-httporsse, the server accepted anyHostandOrigin, so a web page could rebind its hostname to 127.0.0.1 and use every tool, writes and deletes included, from the user's browser.servenow turns on FastMCP's Host/Origin check inautomode: on a loopback bind, requests must name localhost/127.0.0.1 and come from no origin or a local one (421/403 otherwise). FastMCP's own option does not reach its SSE app, so the check is attached tosseas middleware. Clients that connect to localhost, including through a VPN ormcp-remote, are unaffected. A tunnel or reverse proxy that forwards its own hostname (ngrok's default) now needs that hostname inFASTMCP_HTTP_ALLOWED_HOSTS='["host.example"]', or the proxy must rewriteHost;FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=falseturns the check off. The server prints this on start-up, and docs/troubleshooting.md covers it. A FastMCP too old to support the check logs a warning.
Fixed
- Linked-file attachments resolve against the right Zotero profile (#389). With more than one Zotero profile, the local reader took
baseAttachmentPathfrom whichever profile's prefs.js it found first, soattachments:paths could point at another profile's folder and full text was silently left out of the index. The profile whose data directory holds the database is now preferred. Thanks @feiiiiii5. - BibTeX imports keep every author when the author list wraps across lines (#631).
zotero_add_by_bibtexandzotero-cli add bibtexsplit names only on a literaland, soSmith, John andfollowed by a line break andDoe, Jane(how exported.bibfiles commonly wrap long author fields) became a single creator with first nameSmith, John and Doe,and last nameJane. Names are now separated byandbetween any whitespace. - Adding a book chapter or conference paper by DOI keeps the book or proceedings title (#638). CrossRef's
container-titlewas written only topublicationTitle, whichbookSectionandconferencePaperitems do not have, so the container was dropped silently. It now goes tobookTitle/proceedingsTitle. zotero_batch_updateapplies Extra edits to the items it selected (#640). With both tag and Extra actions and atag/queryselector, the Extra half re-ran the search after the tag half had already edited tags, sotag='to-read', remove_tags=['to-read'], set_keys={...}removed the tag and then reported "No items found" for the Extra edits, which were never written. The selection is now resolved once and shared.- CSL JSON import keeps name particles and suffixes (#639). Names exported with
non-dropping-particle,dropping-particleorsuffix(Better BibTeX, citation.js) lost them, so "van der Maaten" was stored as "Maaten". They are now folded into the last and first names. - Note search in local mode stays in the active library (#632).
zotero_get_notes(query=...)readitemNotesanditemAnnotationsfrom SQLite with no library filter, so with group libraries synced, a personal-library search also returned the groups' notes and annotations (and a search afterzotero_switch_libraryreturned personal ones), even though the tool is documented as active-library only. The local queries are now scoped like the other local searches; a group that isn't in the local database falls back to the API. - PDF downloads have a size and time limit (#644).
zotero_attach_filewithurl, the open-access PDF step ofzotero_add_itemand the arXiv PDF step streamed the response to disk with no ceiling while holding the global API lock, so a huge or endless response filled the disk and a slow one kept every other write tool waiting. Downloads now stop at 500 MB or after 300 s, and aContent-Lengthover the limit is refused before reading. - Index bookkeeping no longer wipes
config.json(#646). The savers for the sync watermark, index schema version and backfill count, and--db-path, read the config with any parse error treated as{}and wrote the result back in place. One typo from hand-editing the file, or a read that caught another process mid-write, followed by anyupdate-dberased the web API key, the local write key and the embedding settings. They now refuse to overwrite a config they cannot parse (and log why), and every write goes through a temp file and an atomic rename, so readers never see half a file. A config that is a symlink is written through, so the file it points to stays current. A config that is not valid UTF-8 is treated like invalid JSON. - Stopped servers no longer leave copies of the library behind (#645). The SQLite backend reads a private copy of
zotero.sqliteplus its WAL (#536) and deleted it only at a normal exit, so every stop by SIGTERM (launchd, systemd, Docker) or SIGKILL left a full copy of the library in the temp directory, one per restart. Copies are now named after their process, and each process removes those of processes that are gone before making its first one; unnamed copies from older versions go once they are a week old. The snapshot tests no longer leave copies in the real temp directory either. On Windows, where a process id cannot be probed safely, only copies older than a week are removed. - A failed batch-manifest save no longer loses the run (#646). The manifest of an OpenAI/Gemini Batch API run, its only local record, was rewritten in place on every status refresh and import, so a crash or a full disk mid-write left truncated JSON. That run then disappeared from status and import, and an older run was treated as the newest. Manifests are now written to a temp file and renamed. The full-text cache index now uses the same writer, which also removes a fixed temp file name that concurrent writers shared.
zotero-cli library switchno longer claims the switch carries over (#606). Eachzotero-clicommand is a new process, so a switch only lasted for that one command, while the message said "All tools now operate on this library" and the next command silently read the default library. The CLI now says the switch lasts for this command only and prints theZOTERO_LIBRARY_ID/ZOTERO_LIBRARY_TYPEvalues to use instead;library resetsays the same.- Merging duplicates no longer trashes the annotations on a duplicate's PDF (#661). When a duplicate's attachment is the same file as one the keeper already has (same md5),
zotero_merge_duplicatesleft it on the duplicate and trashed it, along with every highlight and note the user had made on that copy, and still reported "Merge complete". Such an attachment is now moved to the keeper whenever it has annotations or a child note, so the keeper may end up with two copies of the file but nothing is lost. Zotero's local API lists annotations only when asked for them by type, so the check asks for them explicitly. - Embeddings work with Voyage AI through the
openaiprovider (#667). Voyage's/v1/embeddingsrejectsencoding_format="float", which the provider always sent (#348), so abase_urlpointing atapi.voyageai.comfailed with a 400. For that host the provider now asks forbase64and decodes the vectors itself, since the OpenAI SDK decodes base64 only when it picked the format. OpenAI and every otherbase_urlstill sendfloat. - Web-mode feed tools report an error (#601).
zotero_list_feedsandzotero_get_feed_itemsanswered "only accessible in local mode" as plain text, sozotero-cli --json list-feedsandget feed-itemsprintedok: trueand exited 0. The refusal now starts withError:.