Zotero MCP Server: Architecture Analysis of an Unstable Codebase
The zotero-mcp-server was our entry point into the MCP route for Zotero. In practice, we observed repeated stability issues: crashes under parallel requests, timeouts, and hanging calls. A source code analysis by DeepSeek V4 Flash identified several architectural factors that contributed to this. This article documents the findings and compares the architecture with the alternative server Zoteus.
Architecture analysis by DeepSeek V4 Flash (Qwen Code), August 2026. Michael Logies chose DeepSeek V4 Flash based on benchmarks from the Zotero collection AI-, KI-Modelle (artificialanalysis.ai, August 2026).
Source code findings
The analysis is based solely on the publicly available source code of both projects on GitHub:
- Zotero-MCP: github.com/54yyyu/zotero-mcp (Python, FastMCP, pyzotero, 4,800 stars)
- Zoteus: github.com/oscardvs/zoteus (TypeScript, MCP SDK, Express, 28 stars)
Files examined (Zotero-MCP v0.9.x (now v0.10.0) / Zoteus v1.7.0, as of Aug 25, 2026): src/zotero_mcp/client.py, src/zotero_mcp/_app.py, pyproject.toml (Zotero-MCP) and src/server.ts, src/transports/http.ts, src/features/search/ (Zoteus). Snapshot as of August 23, 2026.
✅ Update Aug 24, 2026: Bug reports confirmed and fixed
The issues we reported — #456 (advanced_search timeout) and #447 (notes unreadable) — were confirmed and fixed by the developer in PR #481 (v0.10.0). The five architecture problems described in this analysis (RLock, pyzotero timeout, session management, connection pooling, shutdown) remain unchanged.
✅ Update Aug 25, 2026: Zoteus v1.7.0 – configurable embeddings
Zoteus has been updated to v1.7.0 (PR #17). The three hardcoded OpenAI embedding parameters (batch size, rate-limit delay, model) are now configurable via environment variables: ZOTEUS_EMBEDDING_MODEL (text-embedding-3-large), ZOTEUS_EMBED_BATCH_SIZE (500), and ZOTEUS_EMBED_BATCH_DELAY_MS (6000). The local patches from Issue #15 are now obsolete. The test with 50 items was successful. The missing incremental index update (Issue #16) remains — the Zotero-MCP server (ChromaDB) therefore continues to handle semantic search for the full library.
1. The global RLock – bottleneck #1
The central discovery is in src/zotero_mcp/client.py, lines 35–70:
_zotero_api_lock = threading.RLock()
_DEFAULT_LOCK_TIMEOUT = 45.0
class ZoteroApiBusyError(RuntimeError):
"""Raised when the per-process Zotero API lock can't be acquired in time."""The server uses a global Reentrant Lock (RLock) that serializes every single Zotero operation. While one tool is waiting for the API (e.g., PDF upload, large query, hung cloud operation), it blocks all other tools – including pure read access.
The 45-second timeout is a band-aid, not a solution. The developers themselves write in the comment:
"A single slow/stuck op (e.g. a hung cloud write or PDF upload) holds the lock and every other tool — reads included — blocks behind it until FastMCP's ~60s client timeout fires, surfacing as an opaque '-32001 Request timed out' on every queued call."
2. pyzotero: hard dependency, hard timeout
The server uses pyzotero as its only bridge to Zotero. This Python library has a hardcoded 30-second timeout for HTTP requests that the server cannot configure. The only countermeasure is the global RLock with a 45s timeout expectation – an involuntary interplay of two timeouts.
When pyzotero's 30s timeout fires, the RLock holds the error for another 15s until it times out itself. During this time, all other tools are blocked. This explains the repeatedly observed "hanging calls" that returned no error but simply did nothing.
3. Missing session management
The Zotero-MCP server has no concept of sessions. There is one client, one connection, one global state. When the connection drops (e.g., SSH timeout or server restart), there is no recovery logic – the client must restart the entire session.
The server_lifespan handler in _app.py looks like this:
yield {}
sys.stderr.write("Shutting down Zotero MCP server...\n") # that's itNo resource cleanup, no connection closing, no thread synchronization. Worker threads are simply abandoned with the comment "left to finish on their own".
4. No HTTP connection pooling
The method _make_local_http_client() creates a new httpx.Client every time get_zotero_client() is called. No pooling, no TCP connection reuse. Every API call builds a new HTTP connection to the local Zotero API.
5. Architecture comparison: Zotero-MCP vs. Zoteus
| Aspect | Zotero-MCP | Zoteus |
|---|---|---|
| Language | Python | TypeScript |
| Concurrency | Global RLock | Per-session ContextCache (up to 50) |
| Transport | FastMCP internal | Express + MCP SDK StreamableHTTP |
| Session management | None | create → evict → close |
| Shutdown | Print only | Drain hook + timeout |
| Rate limiting | None | Express-rate-limit |
| Health checks | None | Built-in |
| Security | None | Loopback-only / OAuth |
| Connection pooling | None | Express Keep-Alive |
6. Zoteus: Current limitations (v1.7.0)
Despite its superior architecture, Zoteus v1.6.0 had three hardcoded OpenAI embedding parameters that required patching. PR #17 (v1.7.0) made all three configurable via environment variables:
- Batch size:
ZOTEUS_EMBED_BATCH_SIZE(default 2048 → set to 500) - Batch delay:
ZOTEUS_EMBED_BATCH_DELAY_MS(default 0 → set to 6000) - Embedding model:
ZOTEUS_EMBEDDING_MODEL(changed fromtext-embedding-3-smalltotext-embedding-3-large)
The local patches from Issue #15 are now obsolete. The index build was successfully tested with 50 items.
Missing incremental index update
The biggest practical shortcoming in v1.6.0: zotero_index action: "build" always performs a full rebuild — there is no mechanism to only index new or changed items. For a library with 5,000+ items and full text (1M chars per item), a rebuild with OpenAI embeddings takes 10+ minutes. This makes regular updates in production impractical.
A feature request (#16) for incremental updates has been filed. The Zotero-MCP server (ChromaDB) demonstrates the approach with zotero-mcp update-db --fulltext: it scans the local Zotero SQLite database, detects new/changed items, and only indexes those.
Conclusion
The Zotero-MCP server (v0.9.x, now v0.10.0) has fundamental architecture problems that cannot be fixed by HTTP transport or configuration changes:
- Global RLock – serializes all access, blocks reads during writes
- pyzotero dependency – hardcoded 30s timeout, not configurable
- No session management – no recovery after connection loss
- No connection pooling – new HTTP connection per call
- No graceful shutdown – resources are not released
Zoteus (v1.7.0) is architecturally superior. The hardcoded OpenAI embedding parameters (batch size, rate-limit delay, model) are now configurable via environment variables (PR #17). The remaining hurdle is the lack of incremental index updates (Issue #16). Without incremental updates, semantic search for a growing library is not practical. The Zotero-MCP server (ChromaDB) therefore continues to handle semantic indexing until Zoteus provides this feature.
DeepSeek V4 Flash (0731), August 2026. Commissioned by Michael Logies. Full source code of both projects is publicly available on GitHub.
