pydantic-zotero-mcp
pydantic-zotero-mcp
Un servidor MCP que da a los agentes de IA acceso de lectura a una biblioteca de Zotero: búsqueda, metadatos de elementos, colecciones, etiquetas, las notas propias de la persona investigadora y el texto completo indexado de los PDF adjuntos.
Consulta PRD.md para los requisitos.
Estado: M1 (núcleo de lectura) + M2 (texto completo) implementados. El formato de citas y la exportación (M3), los prompts (M4) y las herramientas de escritura (M5) aún no están construidos; consulta No implementado todavía.
Instalación
Como herramienta (pipx)
Instala el comando zotero-mcp en su propio entorno aislado:
pipx install pydantic-zotero-mcp # or: pipx install /path/to/checkout
zotero-mcp --helpEn el entorno de otro proyecto
uv add pydantic-zotero-mcp # or: uv pip install pydantic-zotero-mcpPara desarrollo en este servidor
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 checkRelated MCP server: zotero-cli-cc
Configuración
Obtén una clave de API de solo lectura y tu ID de usuario numérico en https://www.zotero.org/settings/keys. El ID de biblioteca es el número, no tu nombre de usuario.
export ZOTERO_API_KEY=...
export ZOTERO_LIBRARY_ID=123456 # numeric
export ZOTERO_LIBRARY_TYPE=user # or groupVariable | Default | Propósito |
| — | Clave de la API web (obligatoria salvo que |
| — | ID numérico de usuario o grupo |
|
|
|
|
| Lee en su lugar la API de escritorio de Zotero 7: sin clave, sin límite de peticiones, solo lectura |
|
| Reservado para M5; aún no existen herramientas de escritura |
|
| Límite predeterminado de texto completo; |
|
| Reservado para M3 |
|
| Límite de peticiones ascendentes (Zotero pide ≤ 4) |
|
|
|
|
| Dirección de enlace HTTP |
|
| Puerto HTTP |
|
| Ruta de montaje HTTP |
| — | Token Bearer; obligatorio para HTTP |
Las opciones de la CLI anulan las variables de entorno.
Ejecución
Una vez instalado, zotero-mcp es el punto de entrada: sin ruta de intérprete, sin python -m, sin necesidad de acertar con el directorio de trabajo, que es lo que espera el command: de un cliente MCP:
# 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 --localDesde un checkout, sin instalar, python -m zotero_mcp también funciona:
uv run python -m zotero_mcpArrancar con --transport http y sin token termina con código 2 en lugar de servir sin autenticación: es un canal de lectura hacia una biblioteca personal.
En memoria (integrado en un proceso de agente)
Sin subprocesos, sin socket. La configuración se inyecta, por lo que el host nunca necesita variables de entorno:
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 modelImportar zotero_mcp no tiene efectos secundarios: no lee configuración, no crea ningún cliente, no usa red; eso es lo que hace posible la integración. Hay una prueba que lo garantiza.
Descubrimiento mediante punto de entrada
Para aplicaciones host que descubren servidores MCP incluidos a través de puntos de entrada de Python, este paquete declara uno en el grupo deep_research.mcp_servers:
[project.entry-points."deep_research.mcp_servers"]
zotero = "zotero_mcp:build_server"build_server() no acepta argumentos y obtiene la configuración del entorno: instala este paquete en el entorno del host y el host podrá resolver y ejecutar el servidor en proceso con el nombre zotero, sin importar nada por ruta desde un archivo de configuración.
Una nota de ajuste para hosts automatizados: el límite predeterminado de texto completo de este servidor es de 100.000 caracteres (~25–30k tokens para una única llamada a get_item_fulltext), lo cual es generoso para uso interactivo y demasiado grande para un agente que hace muchas llamadas con un presupuesto de tokens: pasa un max_chars más pequeño por llamada, o baja ZOTERO_FULLTEXT_MAX_CHARS.
Herramientas
Tool | Propósito |
| Tamaño, modo, permisos. Llamada de orientación barata: úsala primero |
| Punto de entrada principal. |
| Elementos añadidos recientemente, los más nuevos primero |
| «¿Ya tengo esto?» por DOI, ISBN, ID de arXiv o clave |
| Metadatos completos; |
| Adjuntos y notas, con |
| Las notas propias de la persona investigadora, sin HTML |
| Texto indexado del adjunto; resuelve de elemento principal → adjunto |
| Árbol de colecciones anidadas |
| Elementos de una colección |
| Vocabulario de etiquetas, opcionalmente filtrado por prefijo |
Recursos: 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.
Notas de diseño
La proyección es la clave. El JSON crudo de Zotero es de ~1 KB por elemento entre links, library, meta y campos de tipo vacíos. zotero_mcp/projection.py reduce una página de 25 elementos de ~6.100 a ~2.400 tokens estimados (39 % del crudo), por debajo del presupuesto de 4.000 del PRD. Los campos nulos se eliminan en la serialización mediante CompactModel.
pyzotero es síncrono y con estado. Zotero.request y Zotero.links se sobrescriben en cada llamada, y Total-Results se lee de la instancia después; así que un cliente compartido usado de forma concurrente informaría de los totales de otra llamada. gateway.py mantiene un pool de hasta ZOTERO_MAX_CONCURRENCY clientes, retira uno por operación y lee los metadatos de la respuesta dentro del mismo hilo de trabajo que lo tiene. Cada llamada pasa por anyio.to_thread.run_sync para que el bucle de eventos nunca se bloquee.
El backoff es trabajo de pyzotero. pyzotero ≥ 1.13 ya respeta Backoff / Retry-After y reintenta el 429 internamente, así que la puerta de enlace no lo reimplementa. Solo añade un reintento acotado de 3 intentos para fallos transitorios de transporte y errores 5xx.
Nada se trunca en silencio. Las búsquedas informan de total_matched, truncated y next_start; el texto completo informa de total_chars y truncated.
Los resultados son candidatos, no veredictos (PRD D3). find_item_by_identifier devuelve matched_on (key / doi / title / identifier / none) además de una confianza y todos los candidatos plausibles: un preprint y su versión publicada sobreviven ambos. El llamante filtra.
Desviaciones del PRD
Vale la pena conocerlas, ya que cada una fue una decisión de criterio tomada durante la implementación:
No hay objeto
mcpa nivel de módulo. El PRD 7.2 pedía a la vez unmcp = create_server()a nivel de módulo y ausencia de efectos secundarios en la importación. Eso entra en conflicto: construir el servidor valida la configuración, así que una instancia a nivel de módulo lanzaImportErroren cualquier máquina sin las variables de entorno de Zotero y rompe la vía en memoria que se suponía que debía soportar. Solo existencreate_server()/build_default_server().Las herramientas de escritura se registrarán condicionalmente, no con
enabled=False. El PRD 5.5 especificaba@mcp.tool(enabled=False), pero FastMCP 3.x no tiene el kwargenabled, y una herramienta deshabilitada pero listada sigue costando contexto. Cuando llegue M5, las herramientas de escritura simplemente no se registrarán salvo queZOTERO_ALLOW_WRITES=true.Este servidor está dirigido a FastMCP 3.x. Dos particularidades de 3.x dan forma al código:
enabledya no está en los decoradores, yresult.dataes un modelo pydantic generado mientras queresult.structured_contentes el dict simple; las pruebas se basan en esto último, que además verifica la omisión de nulos en el cable.has_fulltexttiene tres valores. El PRD 6 lo tipaba comobool, pero determinarlo para un elemento principal requiere una petición de hijos separada por elemento, lo que convertiría una búsqueda de 25 elementos en 26 peticiones. EsFalsecuando un elemento no tiene ningún hijo,True/Falsepara adjuntos y después deget_item(include_children=True), ynull(omitido) cuando no se puede determinar.ItemSummary.num_childrenda la señal barata.find_item_by_identifierdevuelveCitationMatch, noItemSummary | None. Se deriva de D3: la firma anterior hacía exactamente la llamada de identidad que esa decisión trasladó al cliente.matched_onganókeyeidentifierademás de los cuatro valores del PRD, para distinguir una coincidencia exacta de clave de una coincidencia débil de búsqueda.list_recent_items(since_days=...)filtra localmente. Zotero no tiene filtro de fecha en el servidor, así que una ventana estrecha puede devolver menos elementos quelimit; elhintde la respuesta indica cuándo ha ocurrido.
Pruebas
uv run pytest # 80 passedLa suite usa el transporte en memoria de FastMCP contra un FakeZotero que reproduce el comportamiento de pyzotero de leer los metadatos de la instancia. Sin red, sin subprocesos, sin credenciales reales. Cobertura: superficie del esquema, proyección y presupuesto de tokens, paginación e informe de truncamiento, límite de texto completo y resolución del elemento principal, recuperación de coincidencias (los pares preprint/publicado se devuelven ambos), calidad de los mensajes de error, validación de plantillas de recursos incluidos intentos de traversal, validación de configuración, precedencia de la CLI, reintentos/caché de la puerta de enlace, y una comprobación de pureza de importación que falla si importar el paquete toca la red.
No implementado todavía
M3 —
format_citation,format_bibliography,export_itemsM4 — los cuatro prompts (
literature_review,find_related_work,check_citations,summarize_reading), instrumentación con LogfireM5 — herramientas de escritura (
create_item,update_item_fields,add_item_tags,add_items_to_collection,create_note) con semántica PATCH con verificación de versión. La eliminación queda fuera de alcance permanentemente.
This server cannot be installed
Maintenance
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
- AlicenseAqualityCmaintenanceA 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.815MIT
- AlicenseNot gradedqualityAmaintenanceMCP 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.198AGPL 3.0
- AlicenseNot gradedqualityAmaintenanceMCP server that lets AI assistants search, create, organize, and cite from a Zotero library.3MIT
- AlicenseNot gradedqualityDmaintenanceMCP server that connects AI assistants to your Zotero library, enabling full-text PDF extraction and metadata search.MIT
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
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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