add_paper
Add academic papers to Zotero using arXiv IDs or DOIs. Automatically fetches metadata, downloads PDFs, and organizes items into collections for research management.
Instructions
Add a paper to Zotero by arXiv ID or DOI.
Fetches metadata from arXiv or CrossRef, creates the item via the Zotero connector, downloads the PDF, and optionally assigns to a collection. PDF attachment and collection assignment use the Zotero JS API via the zoty-bridge plugin. Zotero desktop must be running.
Args: arxiv_id: arXiv paper ID (e.g. "2301.07041" or "arxiv:2301.07041") doi: DOI (e.g. "10.1038/s41586-021-03819-2") collection_key: Optional Zotero collection key to add the paper to (from list_collections)
Returns: JSON with the created item's metadata on success, or an error message.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| arxiv_id | No | ||
| doi | No | ||
| collection_key | No |
Implementation Reference
- src/zoty/connector.py:506-616 (handler)Main handler function that implements the add_paper tool logic. Fetches metadata from arXiv or CrossRef, creates Zotero items via the connector endpoint, downloads and attaches PDFs via the zoty-bridge plugin, and optionally assigns to collections. Returns JSON with item metadata and status.
def add_paper(arxiv_id: str = "", doi: str = "", collection_key: str = "") -> str: """Add a paper to Zotero by arXiv ID or DOI. Metadata item is created via the Zotero connector. PDF attachment and collection assignment use the zoty-bridge plugin to call Zotero's JS API. If the bridge plugin is not running, those steps fail gracefully (the metadata item and downloaded PDF are still preserved). """ if not arxiv_id and not doi: return json.dumps({"error": "Provide at least one of arxiv_id or doi"}) try: if arxiv_id: item = _fetch_arxiv_metadata(arxiv_id) source_url = item.get("url", "") else: item = _fetch_crossref_metadata(doi) source_url = item.get("url", "") # Create the metadata item via connector _push_to_connector(item, source_url) # Find the parent key for bridge operations (PDF attach + collection assign) parent_key = _find_parent_key_by_title(item.get("title", "")) # Download PDF and register it with Zotero via bridge pdf_url = item.get("_pdf_url", "") pdf_attached = False rdp_warning = "" if pdf_url and parent_key: filename = _make_pdf_filename( item.get("creators", []), item.get("date", ""), item.get("title", ""), ) dl = _download_pdf(pdf_url, filename) if dl: att_key, dest, file_size = dl try: rdp_result = _attach_pdf_via_rdp(parent_key, str(dest)) if rdp_result.get("error"): print(f"zoty: bridge attach error: {rdp_result}", file=sys.stderr) else: pdf_attached = True print( f"zoty: attached PDF {filename} ({file_size} bytes) via bridge", file=sys.stderr, ) except BridgeError as e: rdp_warning = str(e) print( f"zoty: bridge unavailable, PDF saved to disk but not registered: {e}", file=sys.stderr, ) # Collection assignment via bridge collection_added = False if collection_key and parent_key: try: coll_result = _add_to_collection_with_retry(parent_key, collection_key) if coll_result.get("error"): print(f"zoty: bridge collection error: {coll_result}", file=sys.stderr) else: collection_added = True print( f"zoty: added item {parent_key} to collection {collection_key} via bridge", file=sys.stderr, ) except BridgeError as e: if not rdp_warning: rdp_warning = str(e) print( f"zoty: bridge unavailable for collection assignment: {e}", file=sys.stderr, ) # Format creators for output creators = [] for c in item.get("creators", []): first = c.get("firstName", "") last = c.get("lastName", "") name = c.get("name", "") if first or last: creators.append(f"{first} {last}".strip()) elif name: creators.append(name) result = { "status": "created", "title": item.get("title", ""), "creators": creators, "date": item.get("date", ""), "itemType": item.get("itemType", ""), "DOI": item.get("DOI", ""), "url": item.get("url", ""), "abstract": item.get("abstractNote", "")[:500], "pdf_attached": pdf_attached, "collection_added": collection_added, } if rdp_warning: result["rdp_warning"] = rdp_warning return json.dumps(result) except urllib.error.URLError as e: source = "arXiv" if arxiv_id else "CrossRef" if "Connection refused" in str(e) or "localhost" in str(e): return json.dumps({"error": "Cannot reach Zotero connector at localhost:23119. Is Zotero running?"}) return json.dumps({"error": f"Failed to fetch metadata from {source}: {e}"}) except Exception as e: return json.dumps({"error": f"Failed to add paper: {e}"}) - src/zoty/server.py:88-105 (registration)Registration of the add_paper tool with the MCP server using the @mcp_server.tool() decorator. Delegates to connector.add_paper for implementation. Type hints and docstring serve as schema definition for the FastMCP framework.
@mcp_server.tool() def add_paper(arxiv_id: str = "", doi: str = "", collection_key: str = "") -> str: """Add a paper to Zotero by arXiv ID or DOI. Fetches metadata from arXiv or CrossRef, creates the item via the Zotero connector, downloads the PDF, and optionally assigns to a collection. PDF attachment and collection assignment use the Zotero JS API via the zoty-bridge plugin. Zotero desktop must be running. Args: arxiv_id: arXiv paper ID (e.g. "2301.07041" or "arxiv:2301.07041") doi: DOI (e.g. "10.1038/s41586-021-03819-2") collection_key: Optional Zotero collection key to add the paper to (from list_collections) Returns: JSON with the created item's metadata on success, or an error message. """ return connector.add_paper(arxiv_id=arxiv_id, doi=doi, collection_key=collection_key)