zotero-local-mcp
Two MCP servers for a local Zotero 10 library, plus a weekly alert script:
zotero-local-mcp(librarian): search and edit the library, faceted tagging with a controlled vocabulary, citekeys, imports by DOI/PMID/ISBN, metadata audit and repair, duplicates, retraction check, open-access PDFs, notes, collections, undo.zotero-scholar-mcp(researcher): PubMed, Europe PMC and OpenAlex search with "already in library" flags, one merged search over several phrasings and all three sources, open-access full text by sections, citation graph, manuscript citation check, bibliographies as CSL JSON (for Quarto) or RIS (for Rayyan, Covidence and EndNote), also of works outside the library, import queue. It cannot write to Zotero.zotero-alerts: runs saved searches from an Obsidian note and writes new works to an Inbox note. No AI model.zotero-review: preview or apply several tag review notes from the terminal (zotero-review apply 13-31: note numbers, or paths in the vault). Asks before it writes; notes already applied are left out.zotero-relink: after a vault rename, change theobsidian://links in the Zotero child notes.zotero-relink --map PLAN.tsvshows the notes that would change.--applywrites them (Zotero asks for permission). PLAN.tsv has a header rowold,new,kind,note; the paths are relative to the vault root (the folder with.obsidian/).--vault NAMEsets the vault name in the links (defaultpowerslave).zotero-bakeoff: model test for tagging.samplefixes 25 items and writes a reference note for your own tags;run --model Xruns each model throughopencode run(toolsbakeoff_items,bakeoff_submit, no library writes);scorewrites precision, recall, edits needed, tokens and cost per model.
The library is reached only through Zotero's local API on 127.0.0.1:23119: no zotero.org key, changes appear in Zotero at once and sync as normal edits (WebDAV file sync is not affected). Outside metadata comes from Crossref, PubMed (NCBI E-utilities), Europe PMC, OpenAlex, Unpaywall and Open Library.
Safety
- Every write tool defaults to
dry_run=trueand returns a preview. - Added tags must be in the vocabulary file. Tags that start with
_are system tags. Imports never add keywords or MeSH headings as tags. - Each item write sends the item version; concurrent edits in Zotero are kept.
- Every applied write goes to a journal (
~/.local/share/zotero-local-mcp/journal/).undoreverts it, skipping items edited since. Imports and attached PDFs are undone by moving them to the trash. - No DELETE requests. Items go to the trash.
- The researcher's library client refuses every write except creating a new child note (attach_note: a short summary plus an obsidian:// link, journaled, undone by the librarian). Works it proposes go to an import queue note that a person ticks; the librarian imports the ticked lines. Queue labels are sanitised so they cannot create or tick lines.
- Duplicate detection by title needs the same year and first author, and no conflicting DOI/PMID. find_duplicates also finds titles cut off during import, shows the evidence for each item, and says when the item types differ (Zotero's Duplicate Items view shows only items of the same type).
- Files and notes without a parent item: standalone_items lists them (find_items covers regular items only and says so when asked for attachments or notes). set_parent_items puts each under an existing item, a new item from a DOI, PMID or ISBN, or a new item from fields (a magazine article, for example); an item already in the library is used instead of a copy. The new item takes the file's collections. One journal entry; undo makes the files top-level again in their collections and moves the new items to the trash. find_reference searches Crossref, Google Books, Internet Archive, Open Library and Wikidata; web_search uses Brave Search when a key is set.
- Tag limits per facet come from the vocabulary (
max_per_facet: topic=4, type=2); tag_items skips a change that would go over.replacemakes the given tags the complete set for those facets. write_tag_review writes a review note (a table of proposed tags, checked against the vocabulary, never overwriting a note); apply_tag_review applies the edited note exactly, removes the review marker, lists items whose tags changed after the note was written, and refuses a note that was already applied unlessagain=true.
Tools
| Librarian: read | Librarian: write (dry run by default) | Researcher |
|---|---|---|
| status, library_overview | tag_items, rename_tags, remove_tags | search_pubmed, search_openalex, search_europepmc |
| search_multi, read_oa_fulltext, find_contact | ||
| find_items, get_item, get_fulltext | remove_automatic_tags, set_citekeys | get_work, citation_graph |
| list_tags, get_vocabulary | update_fields, file_items, create_collection | library_lookup |
| list_collections, history | create_note, trash_items, undo | check_manuscript |
| audit_metadata, find_duplicates | import_identifiers, import_queue | export_bibliography |
| check_retractions, missing_pdfs | repair_metadata, attach_oa_pdfs | queue_imports, attach_note |
| tag_audit, standalone_items | write_tag_review, apply_tag_review | |
| find_reference, web_search | set_parent_items | |
| get_figure, list_libraries |
Group libraries are read-only. list_libraries lists the user's library ("user") and the groups in this Zotero ("group:"). The read tools (find_items, get_item, get_fulltext, get_figure, standalone_items, library_overview, list_tags, list_collections, find_duplicates, check_retractions) take library="group:<id>"; their zotero:// links then point into the group. The write tools have no library argument, and the client refuses a write while a group is being read.
get_figure cuts a figure or table out of an item's PDF and saves it as a PNG in Literature/figures/ of the vault, for a note. The caption is the anchor ("Figure 2.", "Table 1:"): a figure is the graphics next to its caption, a table runs between its rules. When neither can be found, it takes the whole page and says so. It reads the PDF on this computer with pypdfium2 and changes nothing in the library.
Requirements
- Zotero 10 or later, running, with Settings > Advanced > "Allow other applications on this computer to communicate with Zotero".
- uv (
brew install uv).
Install
- Quit Zotero. Copy
~/Zoteroto a backup location. Start Zotero. - Copy
examples/zotero.envto~/.config/opencode/zotero.env. Fill inZOTERO_CONTACT_EMAIL(required for Unpaywall). - Run
uv sync && uv run pytest -q. All tests must pass. - Run
ZOTERO_MCP_ENV=~/.config/opencode/zotero.env uv run zotero-local-mcp --check. The result must show"zotero": "reachable". - Add the servers and the two agents to OpenCode: see
examples/opencode-global.jsonc. - Optional: weekly alerts with launchd, see
examples/launchd/.
Layout styles
The server reads the style from the folder names on disk. If the ZOTERO_VAULT folder holds inbox, literature, syntheses, research or zotero (exact lowercase names), the style is lowercase. Otherwise the style is TitleCase, and nothing changes.
In the lowercase style the folders are inbox/, literature/, syntheses/, research/ and zotero/. The notes have slug names: inbox/zotero-import-queue.md, inbox/literature-alerts-YYYY-MM-DD.md, inbox/zotero-tag-review-NN.md. The settings notes are zotero/zotero-tags.md, zotero/zotero-agent.md and zotero/literature-alerts.md. ZOTERO_VOCAB and ZOTERO_ALERTS still win.
Configuration
Settings come from environment variables or from the file named by ZOTERO_MCP_ENV (default ~/.config/zotero-local-mcp/env), one KEY=VALUE per line.
| Variable | Use |
|---|---|
ZOTERO_VOCAB |
Vocabulary Markdown file. Tag writes are blocked without it. In the lowercase layout the default is <vault>/zotero/zotero-tags.md. |
ZOTERO_VAULT |
The Sub-Sub folder in your vault: import queue (Inbox/Zotero import queue.md) and alert notes. See Layout styles. |
ZOTERO_ALERTS |
Saved searches (default <vault>/Systems/Literature alerts.md; in the lowercase layout <vault>/zotero/literature-alerts.md). |
ZOTERO_CONTACT_EMAIL |
Required by Unpaywall; sent to Crossref, OpenAlex and NCBI for polite use. |
NCBI_API_KEY, OPENALEX_API_KEY |
Optional higher rate limits. |
BRAVE_API_KEY |
Optional: web_search through the Brave Search API. Without it web_search says it is not set up. |
GOOGLE_BOOKS_API_KEY |
Optional: without it Google Books shares one daily quota among all users, which runs out. |
ZOTERO_API_URL |
Default http://127.0.0.1:23119/api. |
ZOTERO_AGENT_MARKER |
Review marker tag, default _agent. |
ZOTERO_MCP_STATE |
Journal, write key, caches. Default ~/.local/share/zotero-local-mcp. |
Vocabulary file
---
required_facets: topic, status
single_facets: status
---
## topic
- `topic/spirometry` Spirometry. aliases: Spirometry, FEV1
Alerts file
---
days: 7
max_per_query: 20
include_terms: asthma, exhaled nitric oxide, FeNO
exclude_terms: mice, rat, in vitro
---
## pubmed
- `FeNO[tiab] AND asthma[tiab]` FeNO in asthma
## openalex
- `health data literacy` Health data literacy
include_terms and exclude_terms are optional, comma-separated. When either is present, each new work gets a keyword pre-screen on its title and abstract (whole words or phrases, case- and accent-insensitive): +2 per include term in the title, +1 per include term only in the abstract. The decision is exclude when an exclude term matches, pass when the score is above 0, else no_terms_matched. It is written after the identifiers, for example ... doi:10.1000/x pmid:123 (prescreen: pass, score 3), so ticked lines import as before. Excluded works are not dropped: they move to a last section, "Probably not relevant (excluded terms)".
Write authorization
The first write opens a Zotero dialog. Choose "Always Allow"; the key is saved in the state folder (permissions 600). With "Allow", each batch shows the dialog again. To revoke: Zotero > Settings > Advanced > Clear Write Authorizations.
Citekeys
jacinto2026, jacinto2026a. Stored in the native citationKey field when the item has one, otherwise as Citation Key: in Extra. Better BibTeX reads both.
Not yet checked against a real Zotero 10
The tests use simulated services. Check on first use: trash and undo, the citekey location, one import, one PDF attachment (local file upload).
Literature search
search_multi: two to six phrasings of one question on PubMed, Europe PMC and OpenAlex in one call. The same work found by several searches is merged (DOI, PMID, PMCID, then title and year) and listed once, withfound_by(source and query) andin_library. Works found most often come first. No abstracts, so the list stays short; read the chosen works withget_workorread_oa_fulltext.rank_by:found(default: number of searches that found the work, then best rank, then citations) orcited(number of searches plus citations divided by the highest citation count in the results).citedfavours older work.include_terms,exclude_terms: optional keyword pre-screen on title and abstract, with the same rules as the alerts (see "Alerts file"). Each row then hasscoreandprescreen; excluded works stay in the list but come last.screeninggives the counts: records found by all searches, unique works, and works per decision. It is a word match, not a relevance judgement.
read_oa_fulltext: open-access full text from Europe PMC. First call: the sections (title, IMRaD class, length), the abstract and the captions. Second call: only the sections needed (for examplemethods,results), with a character limit.
Development
uv sync
uv run pytest -q
tests/fake_zotero.py imitates the Zotero 10 local API; tests/fake_external.py gives canned Crossref, PubMed, Europe PMC, OpenAlex, Unpaywall and Open Library responses.
License
MIT (see LICENSE). The Europe PMC full-text reading adapts code from Feynman (MIT, Companion, Inc.); see THIRD_PARTY_NOTICES.
Metadata
Release files for zotero-local-mcp 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| zotero_local_mcp-0.8.0.tar.gz | 190.4 kB | Without provenance | Details for zotero_local_mcp-0.8.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| zotero_local_mcp-0.8.0-py3-none-any.whl | Python 3 | none | any | Details for zotero_local_mcp-0.8.0-py3-none-any.whl |
Total release size: 299.5 kB
Release files / zotero_local_mcp-0.8.0.tar.gz
| Download URL | zotero_local_mcp-0.8.0.tar.gz |
|---|---|
| Size | 190.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c4e376a08a31ab312266b7e373c38c0d38bcc355ffde0912c2a45f0735fda2b6
|
|
BLAKE2b-256 checksum How to use checksums |
da7e84e82975c03926a1fe30f603e7c5f8a674fe823efb71112f114e0f37c4c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / zotero_local_mcp-0.8.0-py3-none-any.whl
| Download URL | zotero_local_mcp-0.8.0-py3-none-any.whl |
|---|---|
| Size | 109.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
87ab3376fa092dfee5e45b0b12848dc2243f92b9651ba3ce409e55bec682aed3
|
|
BLAKE2b-256 checksum How to use checksums |
4b3405f93a0a6fda15d9e65cbbdd9610718dec285732b74b24470294a8a1f4e7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for zotero-local-mcp 0.8.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.