Skip to main content
Glama
README.md
# 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

A4.6/5.0

Scored across 9 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness3/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues