zotero-mcp-server
Provides tools for searching, reading, and writing items, collections, tags, and notes in a Zotero library via the Zotero Web API v3.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@zotero-mcp-server Find recent articles in my Zotero tagged 'cybersecurity'"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
zotero-mcp-server
An MCP (Model Context Protocol) server for the Zotero 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 |
| Read | Quick-search items by text, type, tag, or collection |
| Read | Full metadata for one item, optionally with a formatted citation |
| Read | Notes/attachments of an item |
| Read | Full text of a PDF/HTML attachment — indexed Zotero text first, local PDF extraction as fallback. Paginated for long documents |
| Read | Search within notes and PDF annotations/highlights, not the bibliographic items |
| Read | List collections, optionally scoped to a parent |
| Read | List tags in use, with item counts |
| Read | Formatted bibliography (APA, Chicago, MLA, etc.) for up to 50 items in one call |
| Write | Create a new bibliographic item |
| Write | Attach a note to an existing item |
| Write | Add/remove tags on an item (version-checked) |
| 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:
Pass either a parent item's key or an attachment's key directly.
Zotero's own server-indexed full text is tried first (fast — works when Zotero desktop has already synced and indexed the PDF).
If not indexed, the server downloads the attachment and extracts text locally with a PDF parser.
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.
Related MCP server: zotero-mcp
1. Get a Zotero API key
Go to https://www.zotero.org/settings/keys and create a new private key.
Grant it read/write access to the library you want to use (your personal library, and/or specific groups).
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 |
| Yes | The key generated above |
| Yes |
|
| Yes | Numeric user ID or group ID |
| No | Override for the local Zotero desktop API ( |
| No |
|
| No | Port for |
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
npm install
npm run build4. Run
Locally, over stdio (for local MCP clients, e.g. Claude Desktop's local server config):
ZOTERO_API_KEY=... ZOTERO_LIBRARY_TYPE=user ZOTERO_LIBRARY_ID=... node dist/index.jsAs a remote server, over Streamable HTTP (needed to add it as a custom connector in claude.ai):
TRANSPORT=http PORT=3000 \
ZOTERO_API_KEY=... ZOTERO_LIBRARY_TYPE=user ZOTERO_LIBRARY_ID=... \
node dist/index.jsThis 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)
Push this project (this whole folder) to a new GitHub repository.
In the Render dashboard, click New → Web Service and connect that repository.
Render will detect the
Dockerfileautomatically — leave Environment as Docker. Leave build/start commands blank (the Dockerfile handles both).Under Environment Variables, add:
ZOTERO_API_KEYZOTERO_LIBRARY_TYPE(userorgroup)ZOTERO_LIBRARY_ID(
TRANSPORT=httpis already set inside the Dockerfile — no need to add it.)
Click Create Web Service. Render builds the image and deploys it; this takes a few minutes the first time.
Once live, Render shows a URL like
https://zotero-mcp-server.onrender.com. Your MCP endpoint ishttps://zotero-mcp-server.onrender.com/mcp.Sanity check:
curl https://zotero-mcp-server.onrender.com/healthshould 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)
Install the CLI:
curl -L https://fly.io/install.sh | sh(or see fly.io/docs/flyctl).fly auth loginFrom inside the
zotero-mcp-serverfolder:fly launchIt detects the
Dockerfileand 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).
Set your credentials as secrets (never as plain fly.toml values):
fly secrets set ZOTERO_API_KEY=your_key ZOTERO_LIBRARY_TYPE=user ZOTERO_LIBRARY_ID=your_idOpen the generated
fly.tomland confirminternal_port = 3000under[http_service](it should be auto-detected from the Dockerfile'sEXPOSE 3000; fix it manually if not).Deploy:
fly deployYour MCP endpoint is
https://<your-app-name>.fly.dev/mcp. Checkhttps://<your-app-name>.fly.dev/health.
Option C — Railway (CLI or dashboard)
Install the CLI (
npm install -g @railway/cli) or use the Railway dashboard connected to a GitHub repo — same idea as Render.CLI path:
railway login, then from the project folderrailway initandrailway up. Railway detects theDockerfileautomatically.Set env vars:
railway variables set ZOTERO_API_KEY=... ZOTERO_LIBRARY_TYPE=user ZOTERO_LIBRARY_ID=...(or via the dashboard's Variables tab).Railway services aren't public by default — go to Settings → Networking → Generate Domain to get a public
https://your-app.up.railway.appURL.MCP endpoint:
https://your-app.up.railway.app/mcp.
Option D — Self-hosted VM (full control, more steps)
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.SSH in and install Node 20 and Caddy (Caddy handles HTTPS automatically via Let's Encrypt — much less setup than nginx + certbot):
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 caddyCopy the project to the VM (
scpthe zip, thenunzip) and build:cd zotero-mcp-server && npm install && npm run buildCreate
/etc/systemd/system/zotero-mcp.service:[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.targetThen:
sudo systemctl daemon-reload && sudo systemctl enable --now zotero-mcpPoint 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.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:
Descomprime el nuevo
.zipen tu computadora, igual que la primera vez.Ve a tu repositorio en GitHub (
zotero-mcp-server-v2o el nombre que le hayas puesto).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).Commit changes.
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.
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:
In claude.ai, go to Settings → Connectors → Add custom connector.
Enter the deployed URL, e.g.
https://your-deployment.example.com/mcp.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
npx @modelcontextprotocol/inspector node dist/index.jsThis 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-Keyheader, per request.Versioning for writes:
zotero_update_item_tagsreads the item's currentversionbefore patching, and sends it viaIf-Unmodified-Since-Version— if the item changed elsewhere in the meantime, Zotero returns412and the tool reports it clearly instead of silently overwriting.Rate limits: Zotero may return
429with aRetry-Afterheader, or aBackoffheader 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
itemTypefromGET /items/newfirst, so only valid fields for that type are sent.
Official API docs: https://www.zotero.org/support/dev/web_api/v3/
Available Tools
9 toolszotero_add_noteAdd Note to Zotero ItemA
Attach a child note to an existing item.
This tool WRITES to the library and requires an API key with write access.
Args:
parent_item_key (string): 8-character key of the item to attach the note to
note_text (string): note content — plain text or basic HTML
tags (string array, optional): tags for the note itself
Returns: the key of the newly created note.
Examples:
Use when: "add a note to item ABCD1234 saying it belongs to the 'resistencia cotidiana' line" -> parent_item_key="ABCD1234", note_text="Línea teórica: resistencia cotidiana."
Don't use when: you want to change the item's own fields (use zotero_update_item_tags for tags, or edit directly in Zotero for other fields)
Error Handling:
Returns "Error: Not found (404)" if parent_item_key doesn't exist
Returns "Error: Permission denied (403)" if the API key lacks write access
Inputs: note_text (required), parent_item_key (required), tags.
zotero_create_collectionCreate Zotero CollectionA
Create a new collection (folder), optionally as a subcollection of an existing one.
This tool WRITES to the library and requires an API key with write access.
Args:
name (string): collection name, 1-255 characters
parent_collection_key (string, optional): parent collection key, for a subcollection
Returns: the key of the newly created collection.
Examples:
Use when: "create a collection called 'Estado del arte - Resistencia'" -> name="Estado del arte - Resistencia"
Don't use when: the collection may already exist — check with zotero_list_collections first to avoid duplicates
Error Handling:
Returns "Error: Permission denied (403)" if the API key lacks write access
Returns "Error: Library locked (409)" if a sync is in progress — retry shortly
Inputs: name (required), parent_collection_key.
zotero_create_itemCreate Zotero ItemA
Create a new bibliographic item (article, book, webpage, report, etc.) in the library.
This tool WRITES to the library and requires an API key with write access. It fetches the correct field template for item_type from Zotero before submitting, so only the fields you provide are set — you don't need to know the full schema for every item type.
Args:
item_type (string): e.g. 'journalArticle', 'book', 'thesis', 'report', 'webpage'
title (string): required
creators (array, optional): [{creator_type, first_name, last_name}] or [{creator_type, name}] for institutional authors
date, abstract_note, url, doi, publication_title, publisher, place, language, extra (all optional strings)
tags (string array, optional)
collection_keys (string array, optional): file the item into one or more existing collections
Returns: the key of the newly created item.
Examples:
Use when: "add this article to my library: Hollander & Einwohner (2004), 'Conceptualizing Resistance', Sociological Forum" -> item_type="journalArticle", title="Conceptualizing Resistance", creators=[{creator_type:"author", first_name:"Jocelyn A.", last_name:"Hollander"}, {creator_type:"author", first_name:"Rachel L.", last_name:"Einwohner"}], publication_title="Sociological Forum", date="2004"
Don't use when: the item may already be in the library — search first with zotero_search_items to avoid duplicates
Error Handling:
Returns "Error: Permission denied (403)" if the API key lacks write access
Returns "Error: Zotero rejected the item" with the specific field/type problem if item_type is invalid
Inputs: title (required), item_type (required), doi, url, +10 more.
zotero_get_itemGet Zotero ItemARead-onlyIdempotent
Retrieve full metadata for a single item by its key, optionally including a formatted bibliography entry.
Args:
item_key (string): 8-character Zotero item key
include_bibliography (boolean): also return a formatted reference (default: false)
citation_style (string): CSL style name for the bibliography, e.g. 'apa' (default: 'apa')
response_format ('markdown' | 'json')
Returns: full item data (type, creators, title, date, DOI/URL, abstract, tags, collections, notes count), plus a formatted bibliography entry if requested.
Examples:
Use when: "get the full record for item X42A7DEE" -> item_key="X42A7DEE"
Use when: "give me an APA citation for this item" -> include_bibliography=true, citation_style="apa"
Don't use when: searching by title/author (use zotero_search_items instead)
Error Handling:
Returns "Error: Not found (404)" if the item key doesn't exist in this library
Inputs: item_key (required), citation_style, response_format, include_bibliography.
zotero_get_item_childrenGet Zotero Item ChildrenARead-onlyIdempotent
List the child items (notes and file attachments) of a parent item.
Args:
item_key (string): 8-character Zotero key of the PARENT item
only_notes (boolean): return only notes, excluding attachments (default: false)
limit (number): 1-100 (default: 25)
offset (number): pagination offset (default: 0)
response_format ('markdown' | 'json')
Returns: child items with their type (note/attachment), and for notes, the note text.
Examples:
Use when: "does this item have any notes attached?" -> item_key=, only_notes=true
Don't use when: you want top-level library items (use zotero_search_items instead)
Error Handling:
Returns "Error: Not found (404)" if the parent item key doesn't exist
Inputs: item_key (required), limit, offset, only_notes, +1 more.
zotero_list_collectionsList Zotero CollectionsARead-onlyIdempotent
List the collections (folders) in a Zotero library, optionally scoped to one parent collection.
Args:
top_level_only (boolean): only top-level collections (default: false)
parent_collection_key (string, optional): list subcollections of this collection instead of the whole library
limit (number): 1-100 (default: 25)
offset (number): pagination offset (default: 0)
response_format ('markdown' | 'json')
Returns: collections with their key, name, parent, and item count.
Examples:
Use when: "what collections do I have?" -> top_level_only=true
Use when: "list the subfolders inside my 'Putumayo' collection" -> parent_collection_key=
Don't use when: you want the items inside a collection (use zotero_search_items with collection_key instead)
Inputs: limit, offset, top_level_only, response_format, +1 more.
zotero_list_tagsList Zotero TagsARead-onlyIdempotent
List the tags used in a Zotero library, with the count of items carrying each tag.
Useful for discovering what tag vocabulary is already in use before tagging new items (e.g. for the "línea teórica" or "línea de tiempo" categories in a state-of-the-art review), or for checking whether a specific tag already exists.
Args:
name_filter (string, optional): substring (or prefix, with starts_with) to match against tag names
starts_with (boolean): match only the start of tag names (default: false)
collection_key (string, optional): restrict to tags used within one collection
limit (number): 1-100 (default: 25)
offset (number): pagination offset (default: 0)
response_format ('markdown' | 'json')
Returns: tag names with the number of items carrying each one.
Examples:
Use when: "what tags start with 'linea-'?" -> name_filter="linea-", starts_with=true
Use when: "list all tags used in my Putumayo collection" -> collection_key=
Don't use when: you want the items with a specific tag (use zotero_search_items with tag= instead)
Inputs: limit, offset, name_filter, starts_with, +2 more.
zotero_search_itemsSearch Zotero ItemsARead-onlyIdempotent
Search for bibliographic items (articles, books, webpages, etc.) in a Zotero library.
This is a READ-ONLY quick search. It matches titles and creator names by default (query_mode='titleCreatorYear'); pass query_mode='everything' to also search full text of PDF attachments. Combine with item_type and/or tag filters to narrow results, and collection_key to search inside one collection only.
Args:
query (string, optional): Quick-search text
query_mode ('titleCreatorYear' | 'everything'): search scope (default: 'titleCreatorYear')
item_type (string, optional): Zotero item type filter, supports '||' (OR) and leading '-' (NOT)
tag (string, optional): tag filter, supports the same boolean syntax
collection_key (string, optional): restrict to one collection
include_trashed (boolean): include trashed items (default: false)
limit (number): 1-100 (default: 25)
offset (number): pagination offset (default: 0)
response_format ('markdown' | 'json')
Returns: matching items with type, creators, year, title, DOI/URL, tags, and a trimmed abstract.
Examples:
Use when: "find items about resistencia cotidiana in my library" -> query="resistencia cotidiana"
Use when: "list all journal articles tagged putumayo" -> item_type="journalArticle", tag="putumayo"
Don't use when: you need items from a specific known key (use zotero_get_item instead)
Error Handling:
Returns "Error: Permission denied (403)" if the API key lacks access to this library
Returns "No items found matching the given criteria" if the search is empty
Inputs: tag, limit, query, offset, +5 more.
zotero_update_item_tagsUpdate Zotero Item TagsAIdempotent
Add and/or remove tags on an existing item, leaving all other fields untouched.
This tool WRITES to the library and requires an API key with write access. It reads the item's current version first and sends a version-checked PATCH, so it will report a clear conflict error rather than silently overwriting a concurrent change.
Args:
item_key (string): 8-character key of the item to update
add_tags (string array): tags to add
remove_tags (string array): tags to remove
Returns: the item's updated tag list.
Examples:
Use when: "tag item ABCD1234 as verified and remove the 'pendiente' tag" -> item_key="ABCD1234", add_tags=["verificado"], remove_tags=["pendiente"]
Don't use when: you're creating a new item (use zotero_create_item, which accepts tags directly)
Error Handling:
Returns "Error: Version conflict (412)" if the item changed since being read — retry the call, which re-fetches the current version each time
Returns "Error: Not found (404)" if item_key doesn't exist
Inputs: no named parameters.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
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.
Maintenance
Related MCP Connectors
Zotero MCP server for Claude and ChatGPT: search, citations, safe writes, PDF passages and pages.
Remote MCP server for full read/write access to a Zotero library
MCP server for Altmetric APIs - track research attention across news, policy, social media, and more
Auditable MCP server for PubMed, Europe PMC, ClinicalTrials.gov, and bioRxiv/medRxiv queries
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceMCP server for interacting with a Zotero library via the local API. Enables searching, retrieving, creating, updating, and deleting Zotero items, managing collections and tags, and generating citations.-
- AlicenseNot gradedqualityBmaintenanceMCP server that connects to a local Zotero library, enabling search, citation generation with CSL styles, and automatic bibliography updates in Markdown documents.167 npm1MIT
- AlicenseNot gradedqualityBmaintenanceEnables searching, reading, annotating, and managing a Zotero library from any MCP client.AGPL 3.0
- AlicenseAqualityAmaintenanceEnables MCP clients to search a Zotero library, read items, take notes, organize collections, and perform semantic search over PDFs.8MIT