Skip to main content
Glama
jmlon

pydantic-zotero-mcp

by jmlon

pydantic-zotero-mcp

An MCP server that gives AI agents read access to a Zotero library — search, item metadata, collections, tags, the researcher's own notes, and the indexed full text of attached PDFs.

See PRD.md for requirements.

Status: M1 (read core) + M2 (full text) implemented. Citation formatting and export (M3), prompts (M4), and write tools (M5) are not built yet — see Not yet implemented.

Install

As a tool (pipx)

Installs the zotero-mcp command into its own isolated environment:

pipx install pydantic-zotero-mcp        # or: pipx install /path/to/checkout
zotero-mcp --help

Into another project's environment

uv add pydantic-zotero-mcp              # or: uv pip install pydantic-zotero-mcp

For development on this server

git clone https://github.com/jmlon/pydantic-zotero-mcp
cd pydantic-zotero-mcp
uv sync           # creates ./.venv from this project's own lock file
uv run pytest
uv run ruff check

Related MCP server: zotero-cli-cc

Configure

Get a read-only API key and your numeric user ID from https://www.zotero.org/settings/keys. The library ID is the number, not your username.

export ZOTERO_API_KEY=...
export ZOTERO_LIBRARY_ID=123456      # numeric
export ZOTERO_LIBRARY_TYPE=user      # or group

Variable

Default

Purpose

ZOTERO_API_KEY

Web API key (required unless ZOTERO_LOCAL=true)

ZOTERO_LIBRARY_ID

Numeric user or group ID

ZOTERO_LIBRARY_TYPE

user

user or group

ZOTERO_LOCAL

false

Read the Zotero 7 desktop API instead: no key, no rate limit, read-only

ZOTERO_ALLOW_WRITES

false

Reserved for M5; no write tools exist yet

ZOTERO_FULLTEXT_MAX_CHARS

100000

Default full-text ceiling; per-call max_chars overrides it

ZOTERO_DEFAULT_STYLE

chicago-note-bibliography

Reserved for M3

ZOTERO_MAX_CONCURRENCY

4

Upstream request cap (Zotero asks for ≤ 4)

ZOTERO_MCP_TRANSPORT

stdio

stdio or http

ZOTERO_MCP_HOST

127.0.0.1

HTTP bind address

ZOTERO_MCP_PORT

8000

HTTP port

ZOTERO_MCP_PATH

/mcp

HTTP mount path

ZOTERO_MCP_AUTH_TOKEN

Bearer token; required for HTTP

CLI flags override environment variables.

Run

Once installed, zotero-mcp is the entry point — no interpreter path, no python -m, no working directory to get right, which is what an MCP client's command: wants:

# stdio (default) — an agent launches this as a subprocess
zotero-mcp

# streamable HTTP — requires ZOTERO_MCP_AUTH_TOKEN
ZOTERO_MCP_AUTH_TOKEN=secret zotero-mcp --transport http --port 8000

# read the Zotero desktop app instead of the web API
zotero-mcp --local

From a checkout, without installing, python -m zotero_mcp still works:

uv run python -m zotero_mcp

Starting with --transport http and no token exits 2 rather than serving unauthenticated: this is a read channel into a personal library.

In-memory (embedded in an agent process)

No subprocess, no socket. Settings are injected, so the host never needs environment variables:

from fastmcp import Client
from zotero_mcp import ZoteroSettings, create_server

server = create_server(
    ZoteroSettings(
        api_key=key,
        library_id="123456",
        library_type="user",
    )
)

async with Client(server) as client:  # lifespan opens here
    result = await client.call_tool("search_items", {"query": "attention"})
    print(result.structured_content["items"])  # dict; result.data is a model

Importing zotero_mcp has no side effects — no config read, no client built, no network — which is what makes embedding possible. There is a test that enforces it.

Discovery via entry point

For host applications that discover bundled MCP servers through Python entry points, this package declares one in the deep_research.mcp_servers group:

[project.entry-points."deep_research.mcp_servers"]
zotero = "zotero_mcp:build_server"

build_server() takes no arguments and derives settings from the environment — install this package into the host's environment and the host can resolve and run the server in-process by the name zotero, without importing anything by path from a config file.

One tuning note for automated hosts: this server's default full-text ceiling is 100,000 characters (~25–30k tokens for a single get_item_fulltext call), which is generous for interactive use and far too large for an agent making many calls against a token budget — pass a smaller max_chars per call, or lower ZOTERO_FULLTEXT_MAX_CHARS.

Tools

Tool

Purpose

get_library_info

Size, mode, permissions. Cheap orientation call — use it first

search_items

Primary entry point. mode="metadata" or "fulltext" (searches PDF text)

list_recent_items

Recently added items, newest first

find_item_by_identifier

"Do I already have this?" by DOI, ISBN, arXiv ID, or key

get_item

Full metadata; include_children=True also lists attachments and notes

get_item_children

Attachments and notes, with may_have_fulltext per attachment

get_item_notes

The researcher's own notes, HTML stripped

get_item_fulltext

Indexed attachment text; resolves parent → attachment

list_collections

Nested collection tree

list_collection_items

Items in one collection

list_tags

Tag vocabulary, optionally prefix-filtered

Resources: zotero://library/info, zotero://collections, zotero://items/{key}, zotero://items/{key}/fulltext, zotero://collections/{key}/items, zotero://schema/item-types, zotero://schema/item-types/{type}/fields.

Design notes

Projection is the point. Raw Zotero JSON is ~1 KB per item of links, library, meta, and empty type fields. zotero_mcp/projection.py reduces a 25-item page from ~6,100 to ~2,400 estimated tokens (39% of raw), under the PRD's 4,000 budget. Null fields are dropped at serialization by CompactModel.

pyzotero is synchronous and stateful. Zotero.request and Zotero.links are overwritten by each call, and Total-Results is read back off the instance afterwards — so one shared client used concurrently would report another call's totals. gateway.py keeps a pool of up to ZOTERO_MAX_CONCURRENCY clients, checks one out per operation, and reads response metadata inside the same worker thread that holds it. Every call goes through anyio.to_thread.run_sync so the event loop never blocks.

Backoff is pyzotero's job. pyzotero ≥ 1.13 already honours Backoff / Retry-After and retries 429 internally, so the gateway does not reimplement it. It adds a bounded 3-attempt retry for transient transport and 5xx failures only.

Nothing is silently truncated. Searches report total_matched, truncated, and next_start; full text reports total_chars and truncated.

Results are candidates, not verdicts (PRD D3). find_item_by_identifier returns matched_on (key / doi / title / identifier / none) plus a confidence and all plausible candidates — a pre-print and its published version both survive. The caller filters.

Deviations from the PRD

Worth knowing about, since each was a judgment call made during implementation:

  1. No module-level mcp object. PRD 7.2 asked for both a module-level mcp = create_server() and no import-time side effects. Those conflict: building the server validates config, so a module-level instance raises ImportError on any machine without Zotero env vars, and breaks the in-memory path it was meant to support. Only create_server() / build_default_server() exist.

  2. Write tools will be registered conditionally, not enabled=False. PRD 5.5 specified @mcp.tool(enabled=False), but FastMCP 3.x has no enabled kwarg, and a disabled-but-listed tool still costs context. When M5 lands, write tools will simply not be registered unless ZOTERO_ALLOW_WRITES=true.

    This server targets FastMCP 3.x. Two 3.x specifics shape the code here: enabled is gone from the decorators, and result.data is a generated pydantic model while result.structured_content is the plain dict — the tests assert on the latter, which also verifies null-omission on the wire.

  3. has_fulltext is three-valued. PRD 6 typed it bool, but determining it for a parent item requires a separate children request per item, which would make a 25-item search 26 requests. It is False when an item has no children at all, True/False for attachments and after get_item(include_children=True), and null (omitted) when undetermined. ItemSummary.num_children gives the cheap signal.

  4. find_item_by_identifier returns CitationMatch, not ItemSummary | None. Follows from D3 — the old signature made exactly the identity call that decision moved to the client.

  5. matched_on gained key and identifier beyond the PRD's four values, to distinguish an exact key hit from a weak search hit.

  6. list_recent_items(since_days=...) filters locally. Zotero has no server-side date filter, so a narrow window can return fewer items than limit; the response hint says when that happened.

Tests

uv run pytest      # 80 passed

The suite uses FastMCP's in-memory transport against a FakeZotero that reproduces pyzotero's read-metadata-off-the-instance behaviour. No network, no subprocess, no real credentials. Coverage: schema surface, projection and token budget, pagination and truncation reporting, full-text ceiling and parent resolution, match recall (pre-print/published pairs both returned), error message quality, resource template validation including traversal attempts, config validation, CLI precedence, gateway retry/caching, and an import-purity check that fails if importing the package touches the network.

Not yet implemented

  • M3format_citation, format_bibliography, export_items

  • M4 — the four prompts (literature_review, find_related_work, check_citations, summarize_reading), Logfire instrumentation

  • M5 — write tools (create_item, update_item_fields, add_item_tags, add_items_to_collection, create_note) with version-checked PATCH semantics. Deletion is out of scope permanently.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    A lightweight MCP server that connects AI agents to a local Zotero library for paper management and metadata retrieval. It enables users to search titles and abstracts, browse collections, and automatically ingest papers via arXiv ID or DOI with PDF attachments.
    8
    15
    MIT
  • A
    license
    -
    quality
    A
    maintenance
    MCP server that exposes 45 tools for Zotero reference management, enabling AI agents to read/write items, search, extract PDF text, and manage workspaces via the Zotero CLI.
    198
    AGPL 3.0

View all related MCP servers

Related MCP Connectors

  • Remote MCP server for full read/write access to a Zotero library

  • Agentic search over your Dewey document collections from any MCP-compatible client.

  • An MCP server that gives your AI access to the source code and docs of all public github repos

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmlon/pydantic-zotero-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server