# Pyzotero: a Python client for the Zotero web API, with a local API, a CLI and an MCP server

> Pyzotero wraps the Zotero API in a Zotero class that takes a library ID, a library type and an API key, and in v1.16 it also ships an optional CLI and MCP server that talk to a running Zotero 7 instance instead of the web. Its licence is the Blue Oak Model Licence, and the maintainer says new features are currently rare.

**urschrei/pyzotero** — Pyzotero: a Python client for the Zotero API

- Repository: https://github.com/urschrei/pyzotero
- Website: https://pyzotero.readthedocs.org
- Stars: 1,438 · Forks: 131
- Language: Python
- License: NOASSERTION
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/urschrei-pyzotero

## The Zotero constructor and what each argument means

Pyzotero's whole surface starts with a single class.

```python
from pyzotero import Zotero

zot = Zotero(
    library_id, library_type, api_key
)  # local=True to use a running Zotero instead of the web API
items = zot.top(limit=5)
for item in items:
    print(f"Item: {item['data']['itemType']} | Key: {item['data']['key']}")
```

The three arguments are the library ID, the library type and the API key, and the README's quickstart is careful about where each one comes from. Your personal library ID is on the Zotero API keys settings page, in the section labelled Your userID for use in API calls. For a group library, the ID is the integer after /groups/ in the group's page URL, which you can find by hovering over the group settings link.

The library type is a plain string with two possible values, 'user' for your own Zotero library and 'group' for a shared one. That distinction is not cosmetic, because the same client object is scoped to exactly one library, and changing which library you are pointing at means constructing a different Zotero instance.

The quickstart then calls zot.top(limit=5), which retrieves the latest five top-level items in the library. Top level matters: it excludes child items such as notes and attachments, which are what you want when you are listing references rather than every object in the database.

## local=True, authorize_local and the consent model

The recent addition to Pyzotero is support for the Zotero local API, which lets the client talk to a running Zotero installation on the same machine instead of going over the web.

```python
from pyzotero import Zotero

zot = Zotero("0", "user", local=True)
auth = zot.authorize_local("My Application")  # Zotero prompts the user
zot.create_items([item])
```

Two rules govern this mode. Reads do not require authentication, while writes require consent through a dialog in Zotero, and a Zotero version that supports local writes. So the same class handles read and write, but they follow different permission paths.

The authorization step is user-facing. authorize_local takes an application name and Zotero prompts the user, which is a deliberate design choice for a local API rather than a headless token. A key granted with Always Allow can be stored and passed back later as local_api_key, which is how a scheduled job or a repeat script avoids a prompt every run.

Scope is per instance. Versions and keys are scoped to a single Zotero instance, identified by zot.server_id. That identifier is worth noting for anyone running several profiles on one machine, since a key from one instance is not valid against another.

The library_id of "0" in the example is not a placeholder mistake. It is a special value for the local mode, where there is no remote library to name.

## The optional CLI for searching and editing a local library

Pyzotero ships an optional command line interface that works against a local Zotero library, and it is installed as an extra.

```bash
uv add "pyzotero[cli]"
```

The pip equivalent is pip install "pyzotero[cli]", and the README also shows how to run it without installing anything, using uvx.

```bash
uvx --from "pyzotero[cli]" pyzotero search -q "your query"
```

The commands cover searching, adding items and managing collections.

```bash
pyzotero search -q "machine learning" --json
pyzotero search -q "climate change" --fulltext
pyzotero listcollections
pyzotero authorize --app-name "My tool"
pyzotero createitem items.json --collection FD9AUNP2 --tag "to read"
```

Note the two search modes in that list. The default search appears to cover metadata, while --fulltext reaches into the text of attached documents, which is the flag you want for searching inside PDFs. The --json switch gives machine readable output, and pyzotero --help lists everything.

The authorize command is the gate for writing, since it stores a local API key and the write commands need it. createitem takes a JSON file, and --collection and --tag attach the new item to a collection and give it a tag, with FD9AUNP2 in the example being a collection key.

One important detail about extras: the README says both console scripts are installed whichever extra you choose. So installing the mcp extra still gives you the pyzotero CLI binary on your path, which is convenient but means the extra boundary is about dependencies rather than about which commands appear.

## The MCP server, read-only by default

The second optional extra is an MCP server that exposes your local Zotero library and Semantic Scholar integration as tools.

```bash
uv add "pyzotero[mcp]"
```

It can also be installed as a standalone tool with uv tool install "pyzotero[mcp]", and the README gives the Claude Desktop configuration.

```json
{
  "mcpServers": {
    "zotero": {
      "command": "pyzotero-mcp"
    }
  }
}
```

The permission model is layered, and that layering is the most important operational fact here. The server is read-only by default. Starting it with --enable-writes registers tools that create and modify items, notes and collections, add items by DOI, arXiv ID or ISBN with open access PDFs, or from BibTeX and CSL JSON, and merge duplicates. A further flag, --enable-deletes, additionally registers permanent deletion.

So there are three levels: read only, read and write, and read and write with permanent deletion. Enabling deletion is irreversible, and the flag naming makes that explicit.

Two more switches shape the deployment. --group selects a group library rather than the personal one. --transport streamable-http serves over HTTP, which matters if the client is not on the same machine, though that is a different trust boundary from a local process talking to a local Zotero.

## Versioning, packaging and the pace of changes

Pyzotero is on PyPI, on conda-forge and on readthedocs, and the README lists three install paths, uv add pyzotero, pip install pyzotero and conda install conda-forge::pyzotero. The badges show the supported Python versions, the PyPI version and an Anaconda-Server badge.

Since v1.0.0 the project follows Semver, and the README spells out the three increment rules. A MAJOR increment accompanies incompatible API changes, a MINOR increment when functionality is added in a backwards compatible manner, and a PATCH increment for backwards compatible bug fixes. For a client wrapping someone else's HTTP API, that policy is the main compatibility signal you get, and it is a good one to pin against.

The recent releases are v1.16.0 on 2026-10-13, v1.15.2 on 2026-09-18 and v1.15.1 on 2026-08-30, so the release cadence is regular and recent, and the last push was on 2026-10-13. Against that, the README says the latest commits are on the main branch and that new features are currently rare. The two statements are consistent if you read them as maintenance releases with occasional additions.

The project is citable, with a DOI through Zenodo, a CITATION.cff file, and a sample APA citation naming Stephan Hügel and the Pyzotero Authors.

For development, the README gives the source install as a git clone of git://github.com/urschrei/pyzotero.git followed by a checkout of main and uv sync, with a note to specify --dev if you plan to run tests. Tests are run with pytest . from the top-level directory, which needs the dev dependency group, installed either with uv sync --dev or pip install --group dev. Pull requests are welcomed, and the README asks that they be based on the main branch.

## Conclusion

Adopt Pyzotero if your research tooling needs to read or write a Zotero library from Python, since the client covers both the web API and a local Zotero instance and now includes a CLI and an MCP server. Do not adopt it expecting a fast moving upstream, because the README states that new features are currently rare, and the last push was on 2026-10-13 alongside the v1.16.0 release. Verify first whether your Zotero version supports local writes, since the README requires Zotero 7 for the CLI and MCP server and Zotero 10 for local writes, and read LICENSE.md because the Blue Oak Model Licence is a custom licence GitHub cannot classify.

## FAQ

### How do I install Pyzotero and find my library ID?

Install with uv add pyzotero, pip install pyzotero or conda install conda-forge::pyzotero. Your personal library ID is the Your userID for use in API calls value on the Zotero API keys page, while a group library ID is the integer after /groups/ in the group page URL.

### What is the difference between library_type 'user' and 'group' in Pyzotero?

The README says 'user' is for your own Zotero library and 'group' is for a shared group library. The Zotero client object is scoped to a single library, so the type has to match the ID you pass.

### How do writes work through the Pyzotero local API?

With local=True the client talks to a running Zotero instead of the web API. Reads need no authentication, but writes need consent through a dialog in Zotero via authorize_local, or a stored local_api_key from a key granted with Always Allow, and a Zotero version that supports local writes.

### How do I enable write access in the Pyzotero MCP server?

Start it with --enable-writes to register tools that create and modify items, notes and collections, add items by DOI, arXiv ID or ISBN, or merge duplicates. A further --enable-deletes flag registers permanent deletion, and the server is read-only by default.

## Sources

- [Issues](https://github.com/urschrei/pyzotero/issues)
- [Project website](https://pyzotero.readthedocs.org)
- [README](https://github.com/urschrei/pyzotero/blob/main/README.md)
- [Releases](https://github.com/urschrei/pyzotero/releases)
- [urschrei/pyzotero on GitHub](https://github.com/urschrei/pyzotero)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/urschrei-pyzotero
