Skip to main content
Glama

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

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.

Related MCP server: zotero-mcp

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

npm install
npm run build

4. 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.js

As 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.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, 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).

  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):

    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 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):

    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:

    cd zotero-mcp-server && npm install && npm run build
  4. Create /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.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

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/

Available Tools

9 tools
zotero_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 ItemA
Read-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 ChildrenA
Read-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 CollectionsA
Read-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 TagsA
Read-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 ItemsA
Read-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 TagsA
Idempotent

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

    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

    ActivityInactive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      C
      maintenance
      MCP 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.
      -
    • A
      license
      Not graded
      quality
      B
      maintenance
      MCP server that connects to a local Zotero library, enabling search, citation generation with CSL styles, and automatic bibliography updates in Markdown documents.
      167 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables searching, reading, annotating, and managing a Zotero library from any MCP client.
      AGPL 3.0