Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
arxiv_idNo
doiNo
collection_keyNo

Implementation Reference

  • 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}"})
  • 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)

Latest Blog Posts

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/eric-tramel/zoty'

If you have feedback or need assistance with the MCP directory API, please join our Discord server