Skip to main content
Glama
Zotero MCP Server

Zotero MCP Server

by edouardArmit

Zotero MCP Server — Setup Tutorial

This project is a local MCP server that gives Claude direct, read-only access to your Zotero library: metadata, tags, collections, notes, PDF full text (with OCR fallback for scans), and PDF annotations/highlights — plus search and multi-item comparison tools, so you can do NotebookLM-style Q&A and synthesis over your own research library, entirely on your own machine.

Everything below runs locally on your Mac. Nothing in this project uploads your library anywhere; the only thing that leaves your machine is whatever Claude includes in its replies during a normal conversation, exactly as with any other document you show it.

Target locations on your Mac:

  • Server code: ~/ClaudeFolders/ServerMCP/zotero-mcp/ (pick whatever folder you like — this is just the convention used in the setup steps below)

  • Your Zotero data (already exists): ~/Zotero/ (zotero.sqlite + storage/ with your PDFs)

Origin: the initial idea/prompt for this project came from a post on Reddit; this implementation was then built out interactively with Claude.


Features

  • Read-only access to your whole Zotero library: collections, items/metadata, tags, notes, PDF full text, and PDF annotations/highlights.

  • OCR fallback (via Tesseract) for scanned/image-only PDF pages that have no extractable text layer.

  • Full-text and notes search backed by a separate SQLite FTS5 index this server builds and maintains itself — it never writes to Zotero's own database.

  • A compare_items tool that bundles several items (metadata, abstracts, tags, annotations, optionally full text) in one call, for cross-paper synthesis.

  • Automatically covers Zotero group/shared libraries as well as your personal library — a list_libraries tool lists what's available, and every search/list tool covers all of them by default (or can be scoped to one).

  • Works as an MCP server for both Claude Desktop and the Claude Code CLI.

  • Optional weekly automatic re-indexing via a macOS launchd job, with an index_last_updated freshness timestamp surfaced in relevant tool responses so Claude always tells you how current a full-text result is.

  • Strictly read-only by design: it cannot create, edit, or delete anything in your Zotero library or on disk.

Related MCP server: Zotero MCP

Limitations

  • macOS only, as packaged. Setup (Terminal steps, ~/Library/... paths) and the optional automatic re-indexing (launchd) are macOS-specific. The Python code itself is plain and cross-platform, so it would likely run on Linux with an equivalent scheduler (cron/systemd) in place of the provided .plist, and probably needs only minor path handling changes for Windows — but neither has been tested, and the setup instructions below assume macOS throughout.

  • Requires Zotero's desktop app to have actually synced your library locally (zotero.sqlite + storage/ with the PDFs present on disk). A library that only exists in the cloud and has never been opened locally won't have anything to read.

  • Only PDF attachments are extracted for full-text search; other attachment types (web page snapshots, EPUBs, images without OCR'd text, etc.) aren't indexed.

  • Full-text/notes search depends on a separately built index (rebuild_search_index tool, or scripts/build_index.py) rather than being always live against the current library state — very recent additions won't show up in full-text search until the next (re)index runs (metadata search and collection/item listing are always live, no indexing needed).

  • Designed as a local MCP server: it's meant to run on the same machine as Zotero, launched by a local Claude Desktop app or the Claude Code CLI. Using it from a remote/cloud Claude session requires that session to be bridged to your computer somehow (e.g. a device-bridge feature of whichever Claude product you're using) — this isn't a built-in feature of the server itself.

  • Group/shared library visibility is limited to whatever your own Zotero account has already synced locally — the server only reads what's there; it can't request, elevate, or otherwise change your Zotero group permissions.

  • No authentication, multi-user, or remote-access layer of any kind — it's a single local process reading a single local SQLite file, intended for one person's own machine.


0. How it works, in one paragraph

Zotero stores everything in a SQLite database (zotero.sqlite) plus a storage/ folder holding the actual PDF files. This server opens that database read-only (it never writes to it, so it can't corrupt your Zotero library), and exposes a set of tools Claude can call: list collections, list/search items, fetch an item's full metadata, pull the full text of its PDF (extracted on demand with PyMuPDF, OCR'd with Tesseract if it's a scanned page), read your notes and PDF highlights/annotations, and run full-text search across the whole library using a separate search index this server builds for itself (never touching Zotero's own database file). A compare_items tool bundles several items at once so Claude can synthesize across papers in one go.


1. Prerequisites

Open Terminal and check you have what's needed:

python3 --version        # should be 3.10 or newer

You'll also want Homebrew for installing Tesseract (OCR engine). Check if it's installed:

brew --version

If that fails, install Homebrew first: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"


2. Get the project files onto your Mac

The project folder is prepared and delivered to you as a zip alongside this tutorial. Once you've saved/unzipped it:

mkdir -p ~/ClaudeFolders
# unzip the delivered project so the result is:
#   ~/ClaudeFolders/ServerMCP/zotero-mcp/...

If you're setting this up with Claude's help (Cowork, or any session linked to your Mac), it can do this step directly once you grant it access to the ~/ClaudeFolders and ~/Zotero folders — just ask.

Resulting layout:

~/ClaudeFolders/ServerMCP/zotero-mcp/
├── zotero_mcp/            # the Python package (server + data access)
│   ├── server.py          # MCP server entry point — the tools Claude calls
│   ├── db.py               # read-only Zotero SQLite access
│   ├── fulltext.py         # PDF text extraction + OCR fallback
│   ├── indexer.py          # full-text search index (separate from Zotero's DB)
│   ├── tools_helpers.py
│   └── config.py
├── scripts/
│   └── build_index.py     # CLI: build/update the full-text search index
├── test_connection.py     # CLI: sanity-check your Zotero setup
├── pyproject.toml         # dependencies + the `zotero-mcp` command
├── requirements.txt
├── .env.example           # copy to .env and edit
└── README.md              # this file

3. Install Tesseract (OCR engine)

You chose to enable OCR, so scanned/image-only PDF pages can be indexed too:

brew install tesseract

Verify:

tesseract --version

(If you ever want to turn OCR off — faster indexing, but scanned PDFs won't be full-text searchable — set OCR_ENABLED=false in .env, step 5, and skip this.)


4. Create a Python virtual environment and install the server

cd ~/ClaudeFolders/ServerMCP/zotero-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -e .

pip install -e . reads pyproject.toml, installs all dependencies (the mcp SDK, PyMuPDF, pytesseract, Pillow, python-dotenv), and registers a zotero-mcp command inside this virtual environment — that command is what Claude will launch later.

Check it installed cleanly:

which zotero-mcp   # should print .../ServerMCP/zotero-mcp/.venv/bin/zotero-mcp

5. Configure .env

cp .env.example .env

Open .env in a text editor (or nano .env) and confirm/adjust:

ZOTERO_DATA_DIR=~/Zotero
OCR_ENABLED=true
OCR_LANGUAGE=eng

ZOTERO_DATA_DIR is the only setting you're likely to need to touch — it should already be correct given your setup. Leave the rest at their defaults unless you know you need to change them (see the comments in .env.example for what each one does — e.g. ZOTERO_LIBRARY_ID only matters if you use Zotero group libraries).

Important: if ~/Zotero is synced by any cloud backup/sync tool, make sure all your PDFs have fully finished downloading to this Mac before you build the search index (step 7) — a file that's only a cloud placeholder won't have content to extract yet.


6. Sanity-check the connection

With the virtual environment still active:

python test_connection.py

This should print your library's item/collection/tag counts and confirm PDF files are found on disk. If it errors, it will tell you what's wrong (wrong path, Zotero database not found, etc.) — fix that before continuing.


7. Build the full-text search index

python scripts/build_index.py

This extracts text from every PDF (OCR'ing scanned pages) and every note, and stores it in cache/fulltext_index.sqlite inside this project — completely separate from Zotero's own database. Depending on your library size this can take a while the first time (OCR especially); it prints progress as it goes. It's incremental: re-running it later only processes new or changed files.

You don't have to wait for this to finish before wiring the server into Claude — get_item_fulltext and get_item_notes/get_item_annotations work immediately without the index (they extract on demand). Only the search_library tool's fulltext/notes scopes need this index built.


8. Connect it to Claude

Set up whichever client(s) you actually use — Claude Desktop, Claude Code, or both.

8a. Claude Desktop app

Edit (creating it if it doesn't exist): ~/Library/Application Support/Claude/claude_desktop_config.json

If this file already has content (it often does — the desktop app stores other settings there too), don't replace the whole file. Add mcpServers as one more top-level key inside the existing { ... } object, alongside whatever's already there — a JSON file can only have one top-level object, so pasting a second { "mcpServers": ... } block after the closing brace will silently break the file. After editing, validate it before restarting the app:

python3 -m json.tool ~/Library/Application\ Support/Claude/claude_desktop_config.json > /dev/null && echo "valid JSON"

If the file is empty or doesn't exist yet, this is the whole content:

{
  "mcpServers": {
    "zotero": {
      "command": "/Users/YOUR_USERNAME/ClaudeFolders/ServerMCP/zotero-mcp/.venv/bin/zotero-mcp"
    }
  }
}

Replace YOUR_USERNAME with your actual Mac username (or run echo ~ in Terminal to get the full path and use that verbatim — use the absolute path, not ~, since the app doesn't expand ~ itself).

Quit and reopen Claude Desktop completely (Cmd+Q, not just closing the window), then check Settings → Developer (or the small hammer/tools icon in a chat) to confirm "zotero" shows as connected.

8b. Claude Code (CLI)

Either add it as a project-scoped .mcp.json in whatever project folder you run claude from:

{
  "mcpServers": {
    "zotero": {
      "command": "/Users/YOUR_USERNAME/ClaudeFolders/ServerMCP/zotero-mcp/.venv/bin/zotero-mcp"
    }
  }
}

...or use the CLI helper (syntax can vary slightly by Claude Code version — run claude mcp add --help if this doesn't match what you have):

claude mcp add zotero /Users/YOUR_USERNAME/ClaudeFolders/ServerMCP/zotero-mcp/.venv/bin/zotero-mcp

Then verify with claude mcp list (or the /mcp slash command inside a session).


9. Try it

Start a fresh Claude conversation (Desktop or Code) and try things like:

  • "List the collections in my Zotero library."

  • "Search my Zotero library for anything about [your topic]."

  • "Summarize the paper '[a title from your library]' and pull out anything I highlighted."

  • "Compare these three papers on [topic] — what do they agree and disagree on?" (give titles/keys, or ask Claude to search first)

  • "What Zotero libraries do you have access to?" (lists your personal library plus any group/shared libraries)

Claude will call list_collections, search_library, get_item, get_item_fulltext, get_item_annotations, compare_items, etc. as needed — you don't need to name the tools yourself.

Whenever Claude relies on the full-text search index (the fulltext/notes scopes of search_library, or get_library_stats), it's instructed to tell you when that index was last built (e.g. "based on your library as of 2026-09-15 03:00") — so you always know how current the results are. get_item_fulltext, get_item_notes, and get_item_annotations don't need this since they read live, on demand.


10. Group / shared libraries

If you're a member of any Zotero group libraries (shown under "Group Libraries" in the Zotero sidebar), the server sees them automatically — they live in the same local zotero.sqlite as your personal library, just under a different internal ID. No extra configuration is needed.

By default, every tool searches and lists across all your libraries at once — personal and group/shared combined — so "search my library for X" already covers shared libraries too. Call list_libraries to see exactly what's available:

"What Zotero libraries can you see?"

This returns each library's library_id, name, and type (user for your personal library, group for a shared one). Pass that library_id to list_collections, list_items, search_metadata, search_library, get_tags, get_library_stats, or rebuild_search_index to scope a call to just one library — e.g. "only search my 'Lab Reading Group' group library." Item-level tools (get_item, get_item_fulltext, get_item_notes, get_item_annotations, compare_items) work the same regardless of which library an item is in — just its key is enough.

Full-text/notes search coverage for group libraries depends on the search index having been built for them, same as your personal library — rebuild_search_index and scripts/build_index.py cover every library by default, and the weekly automatic reindex (next section) picks up new shared items too. If you notice pdfs_indexed/notes_indexed looking low for a group library right after joining it or after a teammate adds new items, run python scripts/build_index.py --full once, or ask Claude to call rebuild_search_index.

Note on permissions: this server only reads whatever your own Zotero desktop app already has synced locally, exactly as Zotero enforces for you — it doesn't grant, request, or bypass any group-library permissions. If a group's items aren't showing up, check that your Zotero library sync (Settings → Sync) includes that group and that it has finished syncing.


11. Keeping the index fresh

Manually

cd ~/ClaudeFolders/ServerMCP/zotero-mcp
source .venv/bin/activate
python scripts/build_index.py

Automatically, once a week

A weekly launchd job (com.zoteromcp.buildindex.plist, included in this project) handles this for you. launchd is the standard macOS scheduler — more reliable here than cron, and it catches up a missed run if your Mac was asleep at the scheduled time.

  1. Edit the paths in the plist first — open com.zoteromcp.buildindex.plist and replace every YOUR_USERNAME with your actual Mac username (run whoami in Terminal if unsure), so the four absolute paths match your real project location.

  2. Install it:

    cp ~/ClaudeFolders/ServerMCP/zotero-mcp/com.zoteromcp.buildindex.plist ~/Library/LaunchAgents/
    launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.zoteromcp.buildindex.plist

    (If bootstrap errors saying the job is already loaded, that's fine — it means a previous attempt already registered it. If your macOS version doesn't recognize bootstrap, use the older launchctl load -w ~/Library/LaunchAgents/com.zoteromcp.buildindex.plist instead.)

  3. Test it immediately rather than waiting for Sunday 3am:

    launchctl start com.zoteromcp.buildindex
    sleep 5
    cat ~/ClaudeFolders/ServerMCP/zotero-mcp/cache/reindex.log

    You should see the same progress output build_index.py prints when run manually.

  4. Check it's scheduled:

    launchctl list | grep zoteromcp

The job runs every Sunday at 03:00 local time by default. To change the day/time, edit the Weekday/Hour/Minute values in the plist (Weekday 0 = Sunday, 1 = Monday, etc.) and re-run the bootstrap/load command.

To remove it later: launchctl bootout gui/$(id -u)/com.zoteromcp.buildindex then delete the file from ~/Library/LaunchAgents/.


12. Troubleshooting

  • "Zotero database not found" — check ZOTERO_DATA_DIR in .env points at the folder that directly contains zotero.sqlite.

  • "database is locked" — rare; the server automatically falls back to a temporary snapshot copy when this happens. If you see it constantly, close Zotero while indexing.

  • PDFs "not found on disk" — usually means a sync tool hasn't finished downloading that file yet, or the attachment is a "linked file" pointing somewhere else. test_connection.py reports how many PDFs resolved.

  • OCR not running / errors mentioning tesseract — confirm tesseract --version works in Terminal in general (not just inside the venv); pytesseract just shells out to it.

  • Claude Desktop doesn't show the server as connected — double-check the command path in claude_desktop_config.json is absolute (starts with /Users/..., not ~), that the JSON is valid (see step 8a), and that you fully quit (Cmd+Q) and reopened the app — closing just the window can leave the old process running in the background.

  • Server shows connected but tool calls fail on launch — check ~/Library/Logs/Claude/mcp-server-zotero.log for a Python traceback. If it's a relative-path/working-directory issue (e.g. something resolves to / instead of the project folder), the app doesn't guarantee a working directory when launching the server — config.py already resolves .env paths against the project root rather than the process's cwd to avoid exactly this, but it's the first thing to suspect if you ever add a new relative path setting.

  • Indexing is slow — normal for a large library, especially with OCR on scanned PDFs. It only needs to happen once per file; subsequent runs skip unchanged files.

  • A collection you deleted in Zotero still shows up somewhere — shouldn't happen: the server excludes any collection with a pending trash tombstone (Zotero's deletedCollections table) from list_collections and from an item's collection list. If you do see a stale one, it's worth re-checking this logic against your Zotero version's schema.


13. Getting help

If something doesn't match this tutorial exactly — a different Claude Desktop version, a different macOS version, an unusual Zotero setup (group libraries, WebDAV-synced attachments, etc.) — the most useful things to share when asking for help are: the output of test_connection.py, and the tail of ~/Library/Logs/Claude/mcp-server-zotero.log after a restart attempt.


License

MIT — see LICENSE.

Available Tools

13 tools
compare_itemsA

Fetch metadata (and optionally truncated full text) for several items in one call, so you can compare/synthesize across them — arguments, methods, findings, etc. Items can come from different libraries (personal and/or group/shared) in the same call — each entry's library_id says which one it's from. Set include_fulltext=True to also pull each item's PDF text (capped at max_chars_per_item per item to keep the combined response manageable); leave it False to compare on metadata, abstracts, tags and annotations only.

Inputs: item_keys (required), include_fulltext, max_chars_per_item.

get_itemA

Get full metadata for one item: all bibliographic fields, creators, tags, the collections it belongs to, its abstract, and a summary of its child notes and attachments (so you know what's available to fetch next with get_item_fulltext / get_item_notes / get_item_annotations). Works for items in any library — personal or group/shared — the response's library_id says which one this item lives in.

Inputs: item_key (required).

get_item_annotationsA

Get every PDF annotation (highlights, notes, underlines) the user has made on an item's PDF: the highlighted/underlined text, any comment the user added, the highlight color, and the page label — in reading order. This surfaces what the user found important, which is often more useful for synthesis than the raw PDF text.

Inputs: item_key (required).

get_item_fulltextA

Extract the full text of an item's PDF attachment(s), with [p. N] page markers so you can cite specific pages back to the user. Optionally restrict to a page range with page_start/page_end (1-indexed, inclusive) — useful for a long document where you only need a section. max_chars caps the response size (defaults to a safe per-item limit); the result says so and suggests narrowing the page range if truncated. This extracts on demand — it does not require the search index to have been built first.

Inputs: item_key (required), page_end, max_chars, page_start.

get_item_notesA

Get all notes attached to an item (child notes), or the content of a standalone note if item_key is itself a note. Note content is converted from Zotero's internal HTML to plain text.

Inputs: item_key (required).

get_library_statsA

Overall library stats: item/collection/tag counts and full-text index coverage (how many PDFs/notes are searchable via search_library's fulltext/notes scopes right now), including fulltext_index.index_last_updated. Omit library_id to get combined totals across your personal library and every group/shared library, with a by_library breakdown (see list_libraries for names); pass a specific library_id to scope to just one. Mention the index_last_updated timestamp to the user whenever you report these stats or otherwise rely on the full-text index.

Inputs: library_id.

get_tagsA

List tags. If item_key is given, list that item's own tags. If omitted, list every tag used anywhere in the library with how many items carry it, most-used first — handy for discovering the user's own taxonomy before filtering list_items/search_library by tag. When item_key is omitted, defaults to aggregating across every library (personal plus all group/shared libraries); pass library_id (see list_libraries) to scope to just one.

Inputs: item_key, library_id.

list_collectionsA

List every collection (and sub-collection) in the Zotero library as a flat list with parent links, so Claude can build or walk the collection tree. Each entry has key, name, library_id, and parent_key (null for top-level collections). Defaults to every library (your personal library plus all group/shared libraries); pass library_id (see list_libraries) to scope to just one.

Inputs: library_id.

list_itemsA

List items (papers, books, etc. — not notes/attachments) in the library, most recently modified first. Filter by collection_key (from list_collections), tag (exact tag name), and/or item_type (e.g. 'journalArticle', 'book', 'conferencePaper'). Use limit/offset to page through a large library. Defaults to every library (personal plus all group/shared libraries); pass library_id (see list_libraries) to scope to just one. Returns key, title, library_id, item_type, creators_summary, year, dateAdded, dateModified for each item.

Inputs: tag, limit, offset, item_type, +2 more.

list_librariesA

List every Zotero library this server can see: your personal library ("My Library", library_id 1) plus every group/shared library you belong to, with its library_id, name, and type. Pass a group's library_id as the library_id argument to list_collections, list_items, search_metadata, get_tags, search_library, or get_library_stats to scope to that shared library specifically — otherwise those tools already search across every library by default. Item-level tools (get_item, get_item_fulltext, get_item_notes, get_item_annotations, compare_items) work the same regardless of which library an item lives in — just pass its key.

Inputs: no named parameters.

rebuild_search_indexA

(Re)build the full-text search index used by search_library's 'fulltext' and 'notes' scopes. Indexes every library — personal plus every group/shared library — by default, so shared libraries become full-text searchable automatically; pass library_id (see list_libraries) to index just one. Incremental by default (full=False): only new or changed PDFs/notes since the last run are processed, which is fast. Pass full=True to wipe and rebuild everything from scratch. For a large library, prefer running python scripts/build_index.py in a terminal the first time, since that gives you progress output; this tool is best for small incremental top-ups from within a conversation.

Inputs: full, library_id.

search_libraryA

Search across the whole library. scope is one of: 'metadata' (titles/authors/abstracts, always available), 'fulltext' (inside PDF text, requires the index to have been built — see rebuild_search_index / scripts/build_index.py), 'notes' (inside your notes, same index requirement), or 'all' (default: every scope, merged). Searches across every library — your personal library and all group/shared libraries — by default; pass library_id (see list_libraries) to scope to just one. Each fulltext/notes result includes a library_id, a snippet, and the exact page (for PDFs) so you can cite it, plus the parent item_key to fetch more with get_item / get_item_fulltext. IMPORTANT: whenever you report fulltext or notes results to the user, mention the index_last_updated timestamp included in the response (e.g. "based on your library as of ") — those two scopes come from a separately built search index, not a live query, so the user should know how current it is. metadata-only results don't need this since they're always live.

Inputs: query (required), limit, scope, library_id.

search_metadataA

Fast search over item titles, abstracts, and creator names (does NOT search inside PDFs or notes — use search_library or search_fulltext for that). Good for quickly locating an item you already know roughly. Searches your personal library and every group/shared library by default; pass library_id (see list_libraries) to scope to one.

Inputs: query (required), limit, library_id.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 13 tools

    Disambiguation4/5

    Most tools have clearly distinct purposes (list libraries/collections/items, get item content, search, rebuild index, stats). However, search_metadata and search_library with scope='metadata' overlap, and compare_items partially duplicates get_item + get_item_fulltext as a batch operation. Detailed descriptions mitigate but do not eliminate the ambiguity.

    Naming Consistency5/5

    All 13 tools follow a consistent verb_noun snake_case pattern: list_* for enumerating multiple entities, get_* for retrieving specific content, search_* for the two search tools, plus rebuild_search_index, compare_items, and get_library_stats. The naming is highly predictable and readable.

    Tool Count5/5

    13 tools is well within the ideal range for this server's scope. Each tool addresses a distinct part of the Zotero read-only workflow (library discovery, collection/item listing, metadata retrieval, fulltext/notes/annotations extraction, search, indexing, comparison, and stats), with no obvious bloat.

    Completeness5/5

    The tool set provides complete read-only coverage of the Zotero research workflow: discover libraries and collections, list and filter items, fetch full metadata and child content, search across all scopes, maintain the search index, compare multiple items, and retrieve library statistics. There are no dead ends for the stated purpose.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Enables AI assistants to search, access, and interact with your Zotero research library, including semantic search, metadata retrieval, PDF annotations, and library management.
      41
      MIT
    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables AI assistants to search a Zotero library, retrieve complete item metadata, and read the full text of attached PDFs through the Zotero desktop app's local API or the Zotero Web API.
      -
    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables AI assistants to search a Zotero library for items, retrieve complete item metadata, and read the full text of PDF attachments. It connects to either the local Zotero desktop API or the Zotero Web API, returning formatted results that clients can use sequentially.
      -