zotero-mcp-server
# zotero-mcp-server
An MCP (Model Context Protocol) server for the [Zotero Web API v3](https://www.zotero.org/support/dev/web_api/v3) — search, read, and write items, collections, tags, and notes in a Zotero library.
Built for use with Claude (or any MCP client), aimed at literature-review workflows: pulling existing references into a "registro maestro," filing newly-found sources back into Zotero, tagging items by theoretical line, and attaching notes.
## Tools
| Tool | Read/Write | Description |
|---|---|---|
| `zotero_search_items` | Read | Quick-search items by text, type, tag, or collection |
| `zotero_get_item` | Read | Full metadata for one item, optionally with a formatted citation |
| `zotero_get_item_children` | Read | Notes/attachments of an item |
| `zotero_get_fulltext` | Read | **Full text** of a PDF/HTML attachment — indexed Zotero text first, local PDF extraction as fallback. Paginated for long documents |
| `zotero_search_notes` | Read | Search within notes and PDF annotations/highlights, not the bibliographic items |
| `zotero_list_collections` | Read | List collections, optionally scoped to a parent |
| `zotero_list_tags` | Read | List tags in use, with item counts |
| `zotero_generate_bibliography` | Read | Formatted bibliography (APA, Chicago, MLA, etc.) for up to 50 items in one call |
| `zotero_create_item` | Write | Create a new bibliographic item |
| `zotero_add_note` | Write | Attach a note to an existing item |
| `zotero_update_item_tags` | Write | Add/remove tags on an item (version-checked) |
| `zotero_create_collection` | Write | Create a new collection/subcollection |
This is a **focused** set covering the core research workflow, not full API coverage. Not included (would need to be added if you need them): saved searches, file uploads, full item updates beyond tags, deletion, group-membership management.
### Deep reading: `zotero_get_fulltext`
This is what lets Claude read an actual PDF's argument, not just its title and abstract. Resolution is automatic:
1. Pass either a parent item's key or an attachment's key directly.
2. Zotero's own server-indexed full text is tried first (fast — works when Zotero desktop has already synced and indexed the PDF).
3. If not indexed, the server downloads the attachment and extracts text locally with a PDF parser.
4. Long documents are paginated via `char_offset`/`char_limit` — call again with a higher offset to keep reading.
**Limitation:** scanned/image-only PDFs without OCR text return little or garbled text. The tool detects this (flags a warning when extracted text is too short for the page count) rather than silently returning garbage, but it cannot OCR a document itself.
## 1. Get a Zotero API key
1. Go to <https://www.zotero.org/settings/keys> and create a new private key.
2. Grant it **read/write access** to the library you want to use (your personal library, and/or specific groups).
3. Note your **numeric user ID**, shown on the same page — this is *not* your username.
- For a group library, the group ID is the number in the group's URL, or from `GET https://api.zotero.org/users/<userID>/groups`.
## 2. Configure environment variables
| Variable | Required | Description |
|---|---|---|
| `ZOTERO_API_KEY` | Yes | The key generated above |
| `ZOTERO_LIBRARY_TYPE` | Yes | `user` or `group` |
| `ZOTERO_LIBRARY_ID` | Yes | Numeric user ID or group ID |
| `ZOTERO_API_BASE_URL` | No | Override for the local Zotero desktop API (`http://localhost:23119/api/`) instead of `https://api.zotero.org` |
| `TRANSPORT` | No | `stdio` (default) or `http` |
| `PORT` | No | Port for `TRANSPORT=http` (default: `3000`) |
Never commit these values or hardcode them in source — set them as environment variables or secrets in whatever you use to run the server.
## 3. Build
```bash
npm install
npm run build
```
## 4. Run
**Locally, over stdio** (for local MCP clients, e.g. Claude Desktop's local server config):
```bash
ZOTERO_API_KEY=... ZOTERO_LIBRARY_TYPE=user ZOTERO_LIBRARY_ID=... node dist/index.js
```
**As a remote server, over Streamable HTTP** (needed to add it as a custom connector in claude.ai):
```bash
TRANSPORT=http PORT=3000 \
ZOTERO_API_KEY=... ZOTERO_LIBRARY_TYPE=user ZOTERO_LIBRARY_ID=... \
node dist/index.js
```
This exposes `POST /mcp` (the MCP endpoint) and `GET /health` (a plain liveness check). Deploy it somewhere reachable over HTTPS (Render, Fly.io, Railway, a small VM behind a reverse proxy with TLS, etc.) — claude.ai's custom connector setup needs a public `https://` URL, not `localhost`.
A `Dockerfile` is included, so any of the platforms below can build and run it with no extra configuration beyond setting environment variables.
## Deploying to a public HTTPS endpoint
Pick one. Render is the easiest if you don't mind pushing to GitHub; Fly.io is the easiest if you'd rather stay in a terminal.
### Option A — Render (dashboard, needs a GitHub repo)
1. Push this project (this whole folder) to a new GitHub repository.
2. In the [Render dashboard](https://dashboard.render.com/), click **New → Web Service** and connect that repository.
3. Render will detect the `Dockerfile` automatically — leave **Environment** as **Docker**. Leave build/start commands blank (the Dockerfile handles both).
4. Under **Environment Variables**, add:
- `ZOTERO_API_KEY`
- `ZOTERO_LIBRARY_TYPE` (`user` or `group`)
- `ZOTERO_LIBRARY_ID`
- (`TRANSPORT=http` is already set inside the Dockerfile — no need to add it.)
5. Click **Create Web Service**. Render builds the image and deploys it; this takes a few minutes the first time.
6. Once live, Render shows a URL like `https://zotero-mcp-server.onrender.com`. Your MCP endpoint is `https://zotero-mcp-server.onrender.com/mcp`.
7. Sanity check: `curl https://zotero-mcp-server.onrender.com/health` should return `{"status":"ok"}`.
Free-tier note: Render's free web services sleep after inactivity and take a few seconds to wake on the next request — fine for testing, worth upgrading if you'll use this daily.
### Option B — Fly.io (CLI, no GitHub needed)
1. Install the CLI: `curl -L https://fly.io/install.sh | sh` (or see [fly.io/docs/flyctl](https://fly.io/docs/flyctl/install/)).
2. `fly auth login`
3. From inside the `zotero-mcp-server` folder: `fly launch`
- It detects the `Dockerfile` and proposes an app name and region — accept or edit.
- Say **no** to adding a Postgres/Redis database (not needed).
- Say **no** to deploying immediately if it asks — set secrets first (next step).
4. Set your credentials as secrets (never as plain fly.toml values):
```bash
fly secrets set ZOTERO_API_KEY=your_key ZOTERO_LIBRARY_TYPE=user ZOTERO_LIBRARY_ID=your_id
```
5. Open the generated `fly.toml` and confirm `internal_port = 3000` under `[http_service]` (it should be auto-detected from the Dockerfile's `EXPOSE 3000`; fix it manually if not).
6. Deploy: `fly deploy`
7. Your MCP endpoint is `https://<your-app-name>.fly.dev/mcp`. Check `https://<your-app-name>.fly.dev/health`.
### Option C — Railway (CLI or dashboard)
1. Install the CLI (`npm install -g @railway/cli`) or use the [Railway dashboard](https://railway.app/) connected to a GitHub repo — same idea as Render.
2. CLI path: `railway login`, then from the project folder `railway init` and `railway up`. Railway detects the `Dockerfile` automatically.
3. Set env vars: `railway variables set ZOTERO_API_KEY=... ZOTERO_LIBRARY_TYPE=user ZOTERO_LIBRARY_ID=...` (or via the dashboard's Variables tab).
4. Railway services aren't public by default — go to **Settings → Networking → Generate Domain** to get a public `https://your-app.up.railway.app` URL.
5. MCP endpoint: `https://your-app.up.railway.app/mcp`.
### Option D — Self-hosted VM (full control, more steps)
1. Provision a small Ubuntu 22.04+ VM (DigitalOcean, Linode, a spare EC2 instance, etc.) and point a DNS A record at its IP, e.g. `zotero-mcp.yourdomain.com`.
2. SSH in and install Node 20 and Caddy (Caddy handles HTTPS automatically via Let's Encrypt — much less setup than nginx + certbot):
```bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt-get install -y nodejs
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update && sudo apt-get install -y caddy
```
3. Copy the project to the VM (`scp` the zip, then `unzip`) and build:
```bash
cd zotero-mcp-server && npm install && npm run build
```
4. Create `/etc/systemd/system/zotero-mcp.service`:
```ini
[Unit]
Description=Zotero MCP server
After=network.target
[Service]
Environment=TRANSPORT=http
Environment=PORT=3000
Environment=ZOTERO_API_KEY=your_key
Environment=ZOTERO_LIBRARY_TYPE=user
Environment=ZOTERO_LIBRARY_ID=your_id
ExecStart=/usr/bin/node /home/youruser/zotero-mcp-server/dist/index.js
Restart=always
User=youruser
[Install]
WantedBy=multi-user.target
```
Then: `sudo systemctl daemon-reload && sudo systemctl enable --now zotero-mcp`
5. Point Caddy at it — edit `/etc/caddy/Caddyfile`:
```
zotero-mcp.yourdomain.com {
reverse_proxy localhost:3000
}
```
Then: `sudo systemctl reload caddy`. Caddy fetches a TLS certificate automatically on first request.
6. Check `https://zotero-mcp.yourdomain.com/health`. MCP endpoint: `https://zotero-mcp.yourdomain.com/mcp`.
## Actualizar un despliegue ya existente
Ya tienes esto corriendo en Render con un repositorio en GitHub — actualizar es mucho más simple que el despliegue inicial, no hay que tocar Render para nada:
1. Descomprime el nuevo `.zip` en tu computadora, igual que la primera vez.
2. Ve a tu repositorio en GitHub (`zotero-mcp-server-v2` o el nombre que le hayas puesto).
3. **Add file → Upload files**, y arrastra TODO el contenido de la carpeta descomprimida — igual que la vez pasada. GitHub reemplaza automáticamente los archivos que ya existían (por ejemplo `package.json`, `src/index.ts`) y agrega los nuevos (`src/tools/fulltext.ts`).
4. **Commit changes**.
5. Eso es todo — Render detecta el cambio en GitHub y vuelve a desplegar solo, normalmente en 3-8 minutos. No hace falta volver a tocar las variables de entorno ni ninguna otra configuración de Render.
6. Verifica en el panel de Render que el nuevo deploy diga "Live" antes de probar las herramientas nuevas.
## 5. Connect it to Claude
Once deployed and reachable over HTTPS:
1. In claude.ai, go to **Settings → Connectors → Add custom connector**.
2. Enter the deployed URL, e.g. `https://your-deployment.example.com/mcp`.
3. Claude will discover the 9 tools above automatically.
For **Claude Desktop** with a local stdio server instead, add an entry to its MCP server config pointing at `node /absolute/path/to/dist/index.js`, with the environment variables from step 2 set in that config.
## Testing
```bash
npx @modelcontextprotocol/inspector node dist/index.js
```
This opens a local UI to call each tool by hand before wiring it up to Claude.
## Notes on the Zotero API this server relies on
- **Auth**: `Zotero-API-Key` header, per request.
- **Versioning for writes**: `zotero_update_item_tags` reads the item's current `version` before patching, and sends it via `If-Unmodified-Since-Version` — if the item changed elsewhere in the meantime, Zotero returns `412` and the tool reports it clearly instead of silently overwriting.
- **Rate limits**: Zotero may return `429` with a `Retry-After` header, or a `Backoff` header on any response. This server surfaces both as actionable error text; it does not currently auto-retry.
- **Item creation** fetches the field template for the requested `itemType` from `GET /items/new` first, so only valid fields for that type are sent.
Official API docs: <https://www.zotero.org/support/dev/web_api/v3/>
TDQS
Scored across 9 tools
Each tool targets a distinct resource and operation: search vs. fetch by key, items vs. children, collections vs. tags, creating items vs. notes vs. collections. The descriptions explicitly cross-reference each other to prevent misselection.
All tools follow a consistent `zotero_<verb>_<noun>` pattern with snake_case verbs. The only minor deviation is `zotero_get_item_children`, which uses a compound noun, but it's still clear and predictable.
Nine tools cover the essential read and write operations for a Zotero library without bloat. The count is well within the ideal range for a domain-specific server.
The server covers search, retrieval, collection management, item creation, note attachment, and tag updates, but lacks update/delete operations for items and collections. There is no way to edit item metadata, remove items, or reorganize collection membership, which are notable gaps for full lifecycle management.