The single place that decides whether a fetched or imported paper is
already in the library. Every ingest path routes through
is_already_stored โ the DOI-only check used to be copy-pasted in the
ingest pipeline and the file importer and absent from the PDF branch,
which is how DOI-less papers came to duplicate on every run.
File import pipeline: BibTeX/BibLaTeX, CSL-JSON, or Zotero-native JSON
files โ store, with DOI de-duplication. Shared by the research import
CLI command and the MCP import_papers tool.
Push library tags into a running Zotero: the narrow write slice from the
roadmap. Papers are matched to Zotero items by normalized DOI; papers
without a DOI, without a Zotero counterpart, or whose Zotero item changed
since the read are skipped and counted, never guessed at. Dry-run is the
default โ the caller opts into writes with an explicit flag, and nothing
here is exposed over MCP (a CLI user pressing the button is the trust
boundary).