Zotero-MCP
# Zotero-MCP
An MCP server that gives Claude access to your Zotero library **and** lets it
insert *live* Zotero citations into Word documents — real citation fields that
Zotero owns, not text that merely looks like a citation.
That distinction is the whole point of this project, so it is worth being
precise about it.
---
## The problem this solves
Ask an assistant to "cite Vaswani et al. 2017" in a Word document and you
normally get a string of characters: `(Vaswani et al., 2017)`. It looks right.
It is also dead. Zotero does not know it exists. Change your citation style
from APA to IEEE and it does not update. Add a source and the bibliography does
not renumber. Delete a reference and nothing tells you the citation is orphaned.
What you actually want is what Zotero's own Word plugin produces: a **field**
that stays linked to the library item. This server writes those.
## Why it works this way: the Word integration problem
Zotero's Word plugin talks to Zotero over a private bridge — a COM/DLL channel
on Windows, AppleScript on macOS. It is not a public API, it is not documented
for third parties, and it is not designed to be driven by anything other than
Zotero itself. Calling the plugin's macro (`ZoteroInsertCitation`) opens the
**interactive citation picker** and waits for a human to click. There is no
supported, headless way to ask the plugin to insert a citation.
So driving the plugin was off the table. Three options remained:
| Approach | Verdict |
| --- | --- |
| **Automate the Word plugin** via COM / macros | Not viable. The insert path is interactive by design; anything built on it breaks between Word and Zotero versions. |
| **Generate formatted text** with a CSL processor | Easy, but produces exactly the dead citations described above. Rejected. |
| **Write Zotero's field codes directly into the `.docx`** | ✅ What this project does. |
The third approach works because a Zotero citation in a `.docx` is not magic.
It is an ordinary Word field whose instruction text looks like this:
```
ADDIN ZOTERO_ITEM CSL_CITATION {"citationID":"a1b2c3","properties":{…},
"citationItems":[{"id":"…","uris":["http://zotero.org/users/123/items/ABCD1234"],
"itemData":{…CSL-JSON…}}],"schema":"…csl-citation.json"}
```
Plus an `ADDIN ZOTERO_BIBL … CSL_BIBLIOGRAPHY` field for the bibliography, and
a set of hidden `ADDIN ZOTERO_PREF_1…N` fields recording which CSL style the
document uses.
Reproduce those payloads exactly — the item URIs, the CSL-JSON, the schema URL,
the preference blob's chunking — and Zotero cannot tell the difference. It
adopts the citations as its own. **Refresh**, **Add/Edit Bibliography** and
switching styles all behave normally.
No Word automation. No COM. No open Word instance. Just the file.
### What this costs you (read this part)
Being honest about the trade-offs, because they are real:
- **The document must be closed in Word.** Word holds a lock and would
overwrite our edits when it saves. The server detects the `~$file.docx` lock
and refuses rather than losing your work. A `.docx.bak` backup is written
beside the file on every change.
- **`.docx` only.** Legacy `.doc` and OpenDocument `.odt` are not supported.
LibreOffice uses a different field representation (ReferenceMarks); support
could be added but is not there yet.
- **The first Refresh in Word is what makes it exact.** We pre-render the
visible citation text so the document reads correctly straight away, but
multi-source citations, numeric styles and bibliography ordering follow rules
only a full CSL processor gets perfectly right. One click of
*Zotero → Refresh* hands that job to Zotero and normalises everything.
- **Footnote styles are a known limitation.** Citations are inserted into the
body text. For note-based styles (Chicago notes-bibliography and similar),
Zotero will not migrate an in-text field into a real Word footnote. In-text
styles — APA, MLA, Harvard, IEEE, Vancouver, Nature, AMA — work properly.
The server warns you when it detects a note style.
- **Google Docs is not supported.** It stores citations as links, a different
mechanism entirely.
---
## Architecture
```
Claude ──MCP/stdio──▶ zotero-mcp
├── reads ──▶ Zotero 7 local API (localhost:23119) [fast, no quota]
│ └─ falls back to ──▶ api.zotero.org
├── writes ──▶ Zotero Web API v3 [needs API key]
└── Word ──▶ .docx OOXML, direct field injection [no Word needed]
```
Reads prefer the **local API**: it is instant, unmetered, and reflects changes
you have not synced yet. It is read-only, so every write goes to the **Web
API**. If Zotero is not running, reads transparently fall back to the web.
---
## Quick start
Requires Python 3.10+ and Zotero 7.
### 1. Install
**Windows**
```powershell
git clone https://github.com/RogerAylagas/Zotero-MCP.git
cd Zotero-MCP
.\scripts\setup.ps1
```
**macOS / Linux**
```bash
git clone https://github.com/RogerAylagas/Zotero-MCP.git
cd Zotero-MCP
./scripts/setup.sh
```
The script finds a suitable Python, builds the virtualenv, installs the
package, creates `.env`, and runs a health check.
| Flag | Effect |
| --- | --- |
| `-Register` / `--register` | Write the server into `claude_desktop_config.json` (see route B below). |
| `-ConfigPath` / `--config-path` | Target a different config file. |
| `-Force` / `--force` | Rebuild the virtualenv from scratch. |
| `-SkipChecks` / `--skip-checks` | Skip the health check. |
### 2. Add your Zotero credentials
Fill in `.env` — details in [Configure](#configure) below — then run the script
again, or `python scripts/doctor.py`, until every line is green.
### 3. Connect it to Claude
These are the values Claude needs, whichever route you take:
| Field | Value |
| --- | --- |
| Command | `<checkout>/.venv/Scripts/python.exe` (`<checkout>/.venv/bin/python` on macOS/Linux) |
| Arguments | `-m zotero_mcp` |
| Environment | `ZOTERO_MCP_ENV_FILE` = `<checkout>/.env` |
The setup script prints them with your real paths already filled in.
**Route A — the app's settings (recent builds).** Recent Claude Desktop builds
manage MCP servers and extensions through their own settings UI and an
extensions marketplace, not through a config file. Look for *Extensions* or
*Connectors* in Settings and add a local MCP server there using the values
above. This is the supported route: nothing external competes for the file.
**Route B — `claude_desktop_config.json` (older builds).** Older builds read
the server list from a config file. `setup.ps1 -Register` writes it for you; see
[Register with Claude manually](#register-with-claude-manually) for the JSON.
> **Quit Claude before using route B.** Claude keeps that file in memory and
> rewrites it periodically, so an edit made while it is running is silently
> discarded minutes later — it looks like it worked and then quietly undoes
> itself. The script detects a running Claude and refuses rather than
> pretending to succeed.
>
> If the entry keeps disappearing even with Claude closed, your build does not
> read that file at all. Use route A.
Afterwards, restart Claude and ask it to run `zotero_check_setup`.
### A note on "starting" the server
There is no server to start. An MCP stdio server is not a daemon: Claude spawns
it on demand and talks to it over stdin/stdout, then shuts it down. So the
setup script's job is installing it and handing Claude the launch command —
after that the server starts itself whenever Claude needs it. If you launch
`python -m zotero_mcp` by hand it will just sit there waiting for JSON-RPC on
stdin, which is correct but not useful.
To check the server's health at any time:
```powershell
.venv\Scripts\python.exe scripts\doctor.py # Windows
```
```bash
.venv/bin/python scripts/doctor.py # macOS / Linux
```
It tests each layer separately — configuration, local API, Web API, server
startup — so a failure points at one specific thing.
### Configure
```bash
cp .env.example .env
```
Then fill in `.env`:
1. **API key** — create one at <https://www.zotero.org/settings/keys/new>.
Tick *Allow library access* and *Allow write access*. Copy it into
`ZOTERO_API_KEY`.
2. **Library ID** — the numeric *Your userID for use in API calls* shown at
<https://www.zotero.org/settings/keys>. Into `ZOTERO_LIBRARY_ID`.
3. **Local API** (recommended) — in Zotero: *Settings → Advanced →* tick
**"Allow other applications on this computer to communicate with Zotero"**.
> Your API key is a credential. Keep it in `.env` (which is gitignored) and
> never paste it into a chat.
### Register with Claude manually
`setup.ps1 -Register` does this for you. To do it by hand, add to your MCP
client configuration:
```json
{
"mcpServers": {
"zotero": {
"command": "C:\\path\\to\\Zotero-MCP\\.venv\\Scripts\\python.exe",
"args": ["-m", "zotero_mcp"],
"env": {
"ZOTERO_MCP_ENV_FILE": "C:\\path\\to\\Zotero-MCP\\.env"
}
}
}
}
```
Pointing at the `.env` file rather than copying `ZOTERO_API_KEY` into the config
keeps the credential in exactly one place — a gitignored file — instead of
duplicating it into a config file that is easy to share by accident. Individual
`ZOTERO_*` variables still work here if you prefer them; anything already in the
environment wins over the `.env` file.
Claude's config lives at:
| Client | Path |
| --- | --- |
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Claude Desktop (Linux) | `~/.config/Claude/claude_desktop_config.json` |
Restart Claude completely afterwards, then ask it to run `zotero_check_setup` —
it reports exactly what is configured, what is reachable, and what is missing.
### Installing without the script
```bash
python -m venv .venv
.venv/bin/pip install -e . # .venv\Scripts\pip.exe on Windows
cp .env.example .env
```
---
## Tools
### Library — reading
| Tool | Purpose |
| --- | --- |
| `zotero_check_setup` | Diagnose configuration and connectivity. Start here. |
| `zotero_search` | Search by text, type, tag or collection. Returns item keys. |
| `zotero_get_item` | One reference in full, with its notes and attachments. |
| `zotero_list_collections` | Browse collections and sub-collections. |
| `zotero_list_tags` | Tags in the library. |
| `zotero_list_saved_searches` | Saved searches defined in Zotero. |
| `zotero_get_fulltext` | Indexed full text of a PDF attachment. |
| `zotero_list_libraries` | Personal library plus group libraries. |
| `zotero_list_item_types` / `zotero_item_type_template` | Valid types and their fields. |
### Library — writing
| Tool | Purpose |
| --- | --- |
| `zotero_import_identifier` | Add a source from a DOI, arXiv id or ISBN. The fast path. |
| `zotero_create_item` | Create a reference field by field. |
| `zotero_update_item` | Edit fields, with version checking against concurrent edits. |
| `zotero_delete_item` | Move to Zotero's trash (recoverable). |
| `zotero_add_note` | Child or standalone note. |
| `zotero_add_tags` | Add tags without clobbering existing ones. |
| `zotero_attach_link` | Attach a URL to a reference. |
| `zotero_create_collection` / `zotero_set_item_collections` | Organise the library. |
### Citations
| Tool | Purpose |
| --- | --- |
| `zotero_format_citation` | Render citation/bibliography as **plain text** — for email, markdown, slides. Not for Word. |
| `zotero_list_styles` | Browse CSL styles. |
### Word — live citation fields
| Tool | Purpose |
| --- | --- |
| `word_document_outline` | Paragraph-by-paragraph map with indices. Read before inserting. |
| `word_insert_citation` | Insert a live `ZOTERO_ITEM` field. |
| `word_insert_bibliography` | Build the `ZOTERO_BIBL` field from the document's citations. |
| `word_set_citation_style` | Set the document's CSL style. |
| `word_list_citations` | Inspect the Zotero citations already in a document. |
| `word_remove_citation` | Remove a citation field. |
---
## Typical session
> **You:** Add the Vaswani attention paper to Zotero and cite it in
> `thesis.docx` after the sentence about sequence tasks.
What Claude does:
1. `zotero_import_identifier("10.48550/arXiv.1706.03762")` → the reference is
in your library, with proper metadata.
2. `word_document_outline("thesis.docx")` → reads the real paragraph text.
3. `word_insert_citation(..., anchor_text="…changed how we approach sequence tasks.")`
→ a live field appears exactly there.
4. `word_insert_bibliography("thesis.docx", heading="References")`.
Then you open `thesis.docx` in Word and click **Zotero → Refresh**. The
citations are Zotero's now: switch the style to IEEE and everything renumbers,
including the bibliography.
---
## Development
```bash
pip install -e ".[dev]"
pytest
```
The test suite builds `.docx` files from scratch, so it runs without Word or
Zotero installed. The tests that matter most are in
`tests/test_docx_fields.py`: they assert that the field codes we emit parse
back correctly, that the preference blob chunks and reassembles the way Zotero
expects, and that saving a document leaves every package part we did not touch
byte-identical.
### Layout
```
src/zotero_mcp/
├── server.py MCP server and setup diagnostics
├── config.py Environment configuration
├── context.py Shared client, path safety
├── zotero/
│ ├── client.py Hybrid local/web facade
│ ├── web.py Zotero Web API v3
│ └── local.py Zotero 7 local API
├── docxfields/
│ ├── ooxml.py Word field plumbing (fldChar/instrText runs)
│ ├── zotero_fields.py ZOTERO_ITEM / ZOTERO_BIBL / ZOTERO_PREF payloads
│ └── document.py High-level document operations
└── tools/ MCP tool definitions
scripts/
├── setup.ps1 One-command setup + registration (Windows)
├── setup.sh One-command setup + registration (macOS/Linux)
└── doctor.py Layer-by-layer health check
```
## Roadmap
- File attachment upload (the three-step Zotero upload protocol)
- LibreOffice Writer field support
- Real Word footnotes, unlocking note-based styles
- Bundled CSL processor so multi-source citations render exactly before Refresh
## License
MIT
TDQS
Scored across 27 tools
Every tool targets a distinct resource or action. zotero_add_tags and zotero_update_item are differentiated by their replacement semantics, and the word_* tools are clearly separated from the zotero_* tools. No two tools appear to serve the same purpose.
The vast majority follow a consistent zotero_/word_ + verb_noun pattern (list_styles, create_item, insert_citation). Two exceptions, zotero_item_type_template and word_document_outline, are noun phrases rather than verb-first, but they remain readable and the overall convention is strong.
At 27 tools, the set is slightly above the typical 'heavy' range, but it covers two substantial domains: Zotero library management (items, collections, tags, notes, attachments) and Word citation integration. Each tool earns its place, so the count is justified.
The core CRUD lifecycle for items is complete (create, read, update, delete), and the Word integration workflow is fully covered. Minor gaps exist—no delete collection, no explicit tag removal, no file attachment upload—but these can be worked around (update_item replaces tags, attach_link covers links) and do not block primary usage.