zotero-mcp
by harrig12
README.md
# zotero-mcp
MCP server that gives Cursor AI live read access to your Zotero library. Ask Cursor to look up papers by author, topic, or collection — it queries your real library on demand.
This projcet is 100% vibe coded
## Inspiration
The starting point was wanting Cursor to have context about academic papers alongside an analysis project . Result: a lightweight MCP server giving Cursor live read access to the Zotero web API on demand. The goal isn't to dump your whole library into context — it's to let the AI look things up, surface your notes, follow connections between papers, and reason about what you've been reading.
## How it works
Cursor calls these tools exposed by this server:
| Tool | Purpose |
| ----------------------------- | -------------------------------------------------------- |
| `zotero_search` | Full-text search across your library (personal or group) |
| `zotero_get_item` | Full metadata + abstract for one item |
| `zotero_list_collections` | Browse your collections (personal or group) |
| `zotero_get_collection_items` | Items within a collection (personal or group) |
| `zotero_list_groups` | List all group libraries you have access to |
The server runs as a subprocess on stdio transport — Cursor spawns it, passes credentials via environment variables, and kills it when done.
## Setup
### 1. Get credentials
Go to [zotero.org/settings/keys](https://www.zotero.org/settings/keys):
- **API key** → "Create new private key", grant read-only library access
- **User ID** → shown on the same page as "Your userID for use in API calls" (a plain number)
### 2. Install dependencies
```bash
# in the zotero-mcp mamba/conda env
pip install -r requirements.txt
```
### 3. Add to Cursor (WSL + mamba)
Edit `%APPDATA%\Cursor\User\settings.json` and add to `mcpServers`:
```json
{
"mcpServers": {
"zotero": {
"command": "wsl",
"args": [
"/home/cait/miniforge3/envs/zotero-mcp/bin/python",
"/mnt/c/Users/Cait/Projects/zotero-mcp/server.py"
]
}
}
}
```
Credentials are read from `.env` in the project directory — no need to repeat them in `settings.json`.
Restart Cursor after saving.
### 4. Smoke test
In your WSL terminal:
```bash
conda activate zotero-mcp
ZOTERO_API_KEY=yourkey ZOTERO_USER_ID=yourid python server.py
# Should hang waiting for stdin — that means it started correctly. Ctrl+C to exit.
```
## Tool reference
### `zotero_search(query, limit=10, group_id=None)`
Full-text search across your library. Includes notes; excludes attachments. Pass `group_id` to search a group library.
```
zotero_search("attention mechanism", limit=5)
zotero_search("chromatin potential", group_id="12345")
```
Returns: `[{key, title, authors, year, abstract, item_type}, ...]`
### `zotero_get_item(item_key, group_id=None)`
Full metadata for one item. `item_key` is the 8-character Zotero key (e.g. `"ABC12345"`).
```
zotero_get_item("ABC12345")
zotero_get_item("ABC12345", group_id="12345")
```
Returns: `{key, title, authors, year, abstract, item_type, doi, url, publication, volume, issue, pages, publisher, tags, collections, date_added}`
### `zotero_list_groups()`
All group libraries you have access to.
Returns: `[{id, name, description, num_items}, ...]`
### `zotero_list_collections(group_id=None)`
All collections in your library. Pass `group_id` to list a group library's collections.
Returns: `[{key, name, item_count}, ...]`
### `zotero_get_collection_items(collection_key, limit=20, group_id=None)`
Items within a specific collection. Pass `group_id` to fetch from a group library.
```
zotero_get_collection_items("XYZ98765", limit=10)
zotero_get_collection_items("XYZ98765", group_id="12345")
```
Returns: `[{key, title, authors, year, abstract, item_type}, ...]`
## Security
- API key is passed only via environment variable — never in code or on disk
- The server validates credentials at startup and exits with a clear error if any are missing
- `.env` is gitignored; use `.env.example` as a template if you prefer local `.env` over Cursor's `env` block
- Server has read-only access to Zotero (enforced by the API key permissions you set)
## Changelog
### Recent changes
- **Group library support** — new `zotero_list_groups()` tool to discover group IDs; all existing tools now accept an optional `group_id` parameter to query a group library instead of your personal one
- **Notes included in search** — Zotero notes are no longer excluded from results; note content (HTML-stripped) is surfaced as title/abstract
## Potential improvements
- **Dedicated note search** — `zotero_search_notes(query)` tool that searches only within user notes, with full note content returned
- **Tag support** — expose `zotero_get_tags()` to list all tags in library, and `zotero_search_by_tag(tag)` to filter by tag
- **Tag colors** — Zotero supports colored tags (position 1–9); surface these in tag metadata so Cursor can reason about your personal tagging system
- **Auto-tag awareness** — distinguish between user-created tags and auto-generated ones (e.g. from Zotero connectors or imports); Zotero marks these with `type: 1` vs `type: 0`
- **Tag search in `zotero_search`** — include tags in search results by default so Cursor sees how you've categorised a paper
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues