MCP for Zotero — connect your library to Claude, ChatGPT, and other AI assistants
Hi everyone,
I built a small tool called MCP for Zotero. It lets you connect your Zotero library to AI assistants like Claude or
ChatGPT using the Model Context Protocol (MCP).
Once connected, you can do things like:
- Search your library by title, author, tag, or collection
- Add new items (books, articles, theses, etc.) just by describing them in natural language
- Create and organize collections
- Update item metadata, add tags
- Export citations in BibTeX, RIS, or formatted styles like APA or Chicago
- Search inside your PDFs (if indexed by Zotero Desktop)
- Browse and switch between group libraries
The setup is straightforward: you sign in, enter your Zotero API key, and you get an MCP endpoint URL with a token.
You paste that into your AI client settings and that's it — the assistant can read and write to your library directly.
The service acts as a proxy to the Zotero API, so nothing is stored on my end beyond your encrypted credentials. It's
free to use.
You can try it at https://mcpforzotero.alejandroarnaud.dev
Happy to hear any feedback or answer questions.
I built a small tool called MCP for Zotero. It lets you connect your Zotero library to AI assistants like Claude or
ChatGPT using the Model Context Protocol (MCP).
Once connected, you can do things like:
- Search your library by title, author, tag, or collection
- Add new items (books, articles, theses, etc.) just by describing them in natural language
- Create and organize collections
- Update item metadata, add tags
- Export citations in BibTeX, RIS, or formatted styles like APA or Chicago
- Search inside your PDFs (if indexed by Zotero Desktop)
- Browse and switch between group libraries
The setup is straightforward: you sign in, enter your Zotero API key, and you get an MCP endpoint URL with a token.
You paste that into your AI client settings and that's it — the assistant can read and write to your library directly.
The service acts as a proxy to the Zotero API, so nothing is stored on my end beyond your encrypted credentials. It's
free to use.
You can try it at https://mcpforzotero.alejandroarnaud.dev
Happy to hear any feedback or answer questions.
Upgrade Storage
https://forums.zotero.org/discussion/128934/zotero-mcp-plugin
https://forums.zotero.org/discussion/comment/505280#Comment_505280
https://forums.zotero.org/discussion/128857/clean-and-solid-mcp-server-for-zotero
There might be more.
I've got it installed and running on Claude Desktop under macOS 26.3.1.
It took some time to finesse the config file, so I wanted to share here how I finally got it to load correctly:
```
{
"preferences": {
"coworkWebSearchEnabled": true,
"coworkScheduledTasksEnabled": true,
"ccdScheduledTasksEnabled": true,
"sidebarMode": "chat"
},
"mcpServers": {
"mcp-for-zotero": {
"command": "npx",
"args": [
"mcp-remote@latest",
"https://mcpforzotero.alejandroarnaud.dev/mcp/sse",
"--header",
"Authorization: Bearer mcp_your key here"
]
}
}
}
```
It did require the installation of Node.js
Yes the original config didn't work. I tried yours and it worked! Hope the creator can update the config setup.
Huge thanks for sharing your working config and for taking the time to debug this. You were
absolutely right — the original snippet I was handing out was the remote SSE format, which
Claude Desktop's claude_desktop_config.json doesn't actually support. It only loads stdio
servers spawned via command/args, so every macOS user trying our config was hitting a silent
failure. Your npx mcp-remote@latest bridge approach is exactly the right fix.
I just shipped an update to the dashboard so the Claude Desktop tab now shows the working
config out of the box:
{
"mcpServers": {
"mcp-for-zotero": {
"command": "npx",
"args": [
"mcp-remote@latest",
"https://mcpforzotero.alejandroarnaud.dev/mcp/sse",
"--header",
"Authorization: Bearer YOUR_MCP_TOKEN"
]
}
}
}
The Claude Desktop tab also now shows a small info banner reminding people they need Node.js
installed, and a credit line thanking both of you for finding this approach. It felt right
to put your names where future users can see them — this would have kept biting people for
weeks without your post.
If anyone else in this thread had Claude Desktop silently failing on them, head back to your
dashboard, copy the new snippet from the Claude Desktop tab, and you should be good to go.
Thanks again — this is exactly the kind of community feedback that makes the project better.
https://github.com/dougwyu/claude-zotero-skills
"2026-04-30T14:02:28.290Z [mcp-for-zotero] [info] Server started and connected successfully { metadata: undefined }
'C:\Program' is not recognized as an internal or external command,
operable program or batch file.
2026-04-30T14:02:28.478Z [mcp-for-zotero] [info] Server transport closed { metadata: undefined }
2026-04-30T14:02:28.478Z [mcp-for-zotero] [info] Server transport closed unexpectedly, this is likely due to the process exiting early. If you are developing this MCP server you can add output to stderr (i.e. `console.error('...')` in JavaScript, `print('...', file=sys.stderr)` in python) and it will appear in this log. { metadata: undefined }
"
Authentication Failed
Unable to verify credentials. Please check your API key permissions and User ID." But before that, I saw "Zotero connected, your credentials have been saved and validated." So is it successful or not? 2) I have pasted the codes into my Claude Desktop with the authorization code and successfully rewrote the config.json. And what is next? It seems nothing have changed in my zotero.
"name": "MCP for Zotero",
"server_url": "https://mcpforzotero.alejandroarnaud.dev/mcp/sse",
but I see no field to insert the access token
"authorization_header": "Bearer MY_TOKEN"
that I created on the mcpforzotero dashboard. Any tip?
1. Setup is much easier now.
You only enter your Zotero API key — no more user ID field, no more "what does that even mean" confusion. The Tools page has copy-paste configs for Claude Desktop (Mac and Windows), Cursor, VS Code and Codex. The Windows config now actually works on the first try. After you create your token there's a "Getting started" page that confirms everything works and gives you example prompts to try.
2. ChatGPT now works.
You can add the MCP server straight from ChatGPT's connector UI — no manual config needed. Heads-up: this only works on **ChatGPT Pro, Team, or Enterprise**. Plus doesn't support MCP connectors — that's an OpenAI limitation, not ours.
3. You can chat with me directly from inside the app.
There's a new messaging panel in the dashboard. If anything breaks, if you have feedback, or if you're working on a project where you'd like help from an AI-specialist developer, just open a thread and I'll get back to you. I read every message personally.
The URL stays the same: https://mcpforzotero.alejandroarnaud.dev
If you already have an account and it works, you don't have to do anything — your existing setup keeps running. If you ran into one of the problems mentioned earlier in this thread, please give it another shot with the new flow.
Thanks for your patience and for the feedback that made this possible.
I am able to connect my 'personal' libraries but NOT my group libraries. Please let me know, thank you!
mcpServers": {
"mcp-for-zotero": {
"command": "cmd",
"args": [
"/c",
"npx",
"mcp-remote@latest",
"https://mcpforzotero.alejandroarnaud.dev/mcp/sse",
"--header",
"Authorization: Bearer Your_key"
]
}
}
I have taken my MCP token from the mcpforzotero. But I don't know how to add it to my Claude desktop. Here in the window to connect a custom connector, the MCP URL, I have tried to put it after "https://mcpforzotero.alejandroarnaud.dev/mcp/sse" but still get the 404 error (Page not found: /login?next=/oauth/authorize?response_type=code&client_id ...)
I am sorry I am not educated on these topics.
https://s3.amazonaws.com/zotero.org/images/forums/u5087071/t0b6lntgporuvawoc5cu.png
When I try to connect via Claude's Connectors settings, I get the following error returned from the authorization endpoint:
{
"detail": [
{
"type": "missing",
"loc": ["query", "client_id"],
"msg": "Field required",
"input": null
},
{
"type": "missing",
"loc": ["query", "redirect_uri"],
"msg": "Field required",
"input": null
}
]
}
It looks like the authorization request Claude sends isn't including client_id and redirect_uri, which your endpoint expects. I've tried disconnecting and reconnecting the connector fresh, and get the same error each time.
Happy to share any additional details (timestamps, browser/app version, etc.) that would help you trace it. Thanks for your work on this — it'd be a big help to my research workflow once it's working.
Best,
Fosh
Flagging something for anyone else who cites counts from it: the item_count your tool reports for a collection (via zotero_get_collections) doesn't always match what Zotero itself displays for the same collection(looking myself at my desktop app).
I checked on three separate days (Aug 26, Sep 1, Sep 2) — two of my larger collections were consistently off (one +5, one -5 from the true count), and the wrong numbers held steady for a day or two before shifting once, which looks like caching rather than a live read each time (you mention above that nothing is cached and it's a live proxy, so this may be worth a look).
Also, get_collection_items' total field just echoes back the limit parameter rather than the collection's real size.
Happy to share exact numbers/screenshots — sending you a direct message with details too.
Thanks again for the tool!
Warmly,
LBHE
server take advantage of it yet, so I wrote one: zotero-native-mcp.
The practical difference from the servers discussed above is that nothing
touches the network. It talks to the local API Zotero itself serves on
127.0.0.1:23119, which as of Zotero 10 accepts POST, PUT, PATCH and DELETE, not
just reads. So there is no zotero.org API key to create, no user ID to look up,
and no cloud round trip. Reads come back in 8 to 60 ms.
That also removes a few of the problems people have hit in this thread. There is
no API key to authenticate, no user ID to get wrong, and reads go straight to the
running application, so counts match what Zotero shows.
Writes are gated: the first one raises a dialog inside Zotero asking you to allow
it, the same mechanism the Zotero developers built for this. Deleting moves items
to Zotero's trash and is reversible; erasing permanently has to be requested
explicitly.
Requirements: Zotero 10 or newer, since earlier versions have a read-only local
API, and the "Allow other applications on this computer to communicate with
Zotero" setting under Advanced.
claude mcp add --scope user zotero-native-mcp -- npx -y zotero-native-mcp
28 tools covering collections, items, attachments from disk, indexed full text,
BibTeX and CSL export, tags and saved searches. MIT licensed. Tested against a
real Zotero on macOS, Windows 11 and Ubuntu 24.04.
https://github.com/dvdsosa/zotero-native-mcp
Happy to answer questions here. If you try it somewhere I have not tested, a
report either way is genuinely useful.
Trashed items. Item listings leave trashed items out unless you ask for them (that is what the includeTrashed parameter exists for), but an item you delete keeps its collection membership until the trash is emptied, which is why restoring it puts it back where it was. So a count derived from collection membership can still be counting items the client has stopped showing you. I have not confirmed that this is what your +5 is, and it is cheap to check: see whether five of that collection's items are sitting in the trash (/items/trash, or the Trash row in the desktop app).
Subcollections and child items, for the other direction. collections/KEY/items includes child notes and attachments, while collections/KEY/items/top returns only the rows Zotero displays. And if View, "Show Items from Subcollections" is switched on in the desktop, the client is showing you more than any flat count of that one collection can. A -5 is usually one of those two.
Your second point is straightforward to fix: the real size of a multi-object read is in the Total-Results response header, not in the body. Anything that fills a "total" field from the limit parameter, or from the length of the page it just received, is right only when everything fits in one page. That header is where the number lives.
Whichever it turns out to be, one thing I would say for a scoping review specifically: do not cite a count that came through an MCP server, mine included. Take the PRISMA numbers from Zotero itself or from an export, and use the assistant for the reading rather than the counting. Wrappers sit at least one layer away from what the client shows you, and the layer is not always transparent.
prishhh_16, on group libraries, since nobody answered in May: the cause is almost always the API key rather than the client. On zotero.org/settings/keys, personal-library access and group access are separate. The key form has its own default group permissions setting with per-group rows under it, and a key created with only personal permissions sees no groups at all. Edit the key, set that group to Read Only or Read/Write, save. You do not need to regenerate it. The other half is the server: a group library lives at /groups/GROUPID/..., not /users/USERID/..., so the tool needs a way to say which library you mean. If it has no such parameter, no amount of key permission will get you there.
dvdsosa, agreed that the writable local API in Zotero 10 is the right direction, and that removing the API key and user ID removes most of the failures reported in this thread.
Since servers are being compared here, and since aborel's point about fragmentation from March still stands, I will declare my own: I maintain another one, Zoteus, which has not come up in this thread. I am not going to argue that it wins.
What it is: TypeScript, MIT, run by your MCP client through npx, so no Python environment and no plugin inside Zotero. With the desktop app running, reads go straight to it and need no cloud key, and so do some personal-library writes: adding items by identifier, PDF annotations, file attachments, and trash or restore. On Zotero 10 and newer those use the app's local API, behind a key you grant once in an in-app dialog; on 9 and earlier, whose local API is read-only, they go through the connector protocol the browser extensions use, which can create but not update or delete. Editing item fields, creating collections, and tag edits still go through the Web API and need a cloud key, which is worth knowing before you set it up. The Web API is also the fallback for group libraries, for sync, and for when the app is closed. Bibliographies are formatted with citeproc-js in any CSL style. PDF highlights are anchored by quoting the passage: you give the text, it locates the lines in the PDF, so no rectangles. Full-text search returns the passage with a page locator, exact when the PDF itself is parsed and a proportional estimate otherwise. Groups are listed by zotero_groups and then addressed with library_type "group" plus library_id, subject to the same key permission above. DOI and arXiv identifiers resolve out of the box; ISBN and PMID need a Zotero translation-server running.
Compared with 54yyyu/zotero-mcp, which is what most people in this thread are already running: that one is the more established project, it writes too these days (add by DOI, ISBN, BibTeX, create collections, update metadata, create annotations), and it has things mine does not, including looking an item up by its BetterBibTeX citation key and a Scite integration. Mine talks to BBT in one direction only, to run a better-biblatex export. It is Python and its semantic search wants the ChromaDB extra; mine is Node, and keyword search works out of the box, but vector ranking is an extra there too, either @huggingface/transformers installed locally (a large download, onnxruntime binaries included) or an embedding API key. On that axis we are even rather than different. If zotero-mcp is working for you, that is not a reason to switch. The cases where I would look at mine are: no Python on the machine, heavy CSL-style formatting, or annotating PDFs from quoted text.
One caveat that cuts against me as well: my collection listing passes the API's numItems straight through, so if the trash behaviour above is what is happening here, my counts have it too. That is a property of the API, not of any one wrapper.
Code and issues: https://github.com/oscardvs/zoteus
Docs: https://zoteus.com/docs
Happy to answer setup questions here or on the tracker. L.BHE, if you do work out which of the two it was, please post the answer here, it will save the next person the same hunt.