Skip to main content
Glama

Attach a file (PDF, snapshot) to an item

zotero_attach_file

Attach a file to an existing Zotero item from a URL, local path, or by finding its open-access PDF via DOI.

Instructions

Add a stored file attachment (e.g. a PDF full text) under an existing item. Give parent (the item key) and one of url (Zoteus downloads it, then stores it), path (a file on the machine running Zoteus), or find_oa: true (Zoteus looks the parent item's DOI up in OpenAlex and attaches the open-access PDF, if there is one). find_oa finds only copies OpenAlex already knows about, which is arXiv, PubMed Central, DOAJ journals and institutional repositories: it is not a way past a paywall, and it says so plainly when there is no free copy. The copy it finds is often the author's accepted or submitted manuscript rather than the published version, so the source, the version and the licence come back in the result and go into the attachment's title. filename and content_type are inferred when omitted. Saves through the Zotero desktop app when one is reachable (Zotero 10+ local API; you may be asked once to allow Zoteus write access, choose "Always Allow"), and otherwise through the cloud Web API, which needs ZOTERO_API_KEY with file access and uses your Zotero file-storage quota. url works on every setup including a remote/hosted Zoteus that cannot see your desktop, so prefer it over path unless the file really is on the server. Returns the new attachment key.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlNoURL to download the file from; works on remote/hosted servers, where it must be an https link to a public host (no private or loopback addresses, 64 MB at most).
pathNoFilesystem path to the file, on the machine running Zoteus.
titleNoAttachment title, e.g. "Full Text PDF".
parentYesKey of the parent item to attach the file to.
find_oaNoFind the open-access PDF for the parent item by its DOI (OpenAlex) and attach it. Use instead of `url`/`path`, not alongside them. Refuses, saying why, when the item has no DOI, when OpenAlex reports no open-access copy, when the item already has a PDF, or when what the link serves is not a PDF.
filenameNoFile name to store; inferred from path/url if omitted.
library_idNoGroup library to attach the file in (from zotero_groups); forces the cloud path instead of the desktop app.
content_typeNoMIME type; inferred from the extension if omitted (pdf -> application/pdf).
library_typeNoWhich library to address: "user" (a personal library) or "group" (a shared group library). Omit to use the library this server is configured for. "group" on its own is refused: pass library_id with it.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
oaNoWhere an automatically discovered open-access PDF came from, and what version it is. Present only for find_oa.
bytesYesSize of the stored file.
parentYesThe item it hangs off.
targetNoWhere the write went: "local" (Zotero desktop local API), "desktop" (connector protocol) or "cloud" (Zotero Web API).
filenameYesFile name stored.
attachmentYesKey of the attachment item created.
contentTypeYesMIME type stored, e.g. "application/pdf".
alreadyInStorageNoTrue when Zotero already held these bytes and only the item was created (cloud path).

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed4 schema fields changedv1.21.0
    • addedInput schema / properties / find_oa
      Added value: +{
      +  "description": "Find the open-access PDF for the parent item by its DOI (OpenAlex) and attach it. Use instead of `url`/`path`, not alongside them. Refuses, saying why, when the item has no DOI, when OpenAlex reports no open-access copy, when the item already has a PDF, or when what the link serves is not a PDF.",
      +  "type": "boolean"
      +}
    • addedInput schema / properties / library_id / exclusiveMinimum
      Added value: +0
    • changedInput schema / properties / url / description
      Previous value: -"URL to download the file from; works on remote/hosted servers."New value: +"URL to download the file from; works on remote/hosted servers, where it must be an https link to a public host (no private or loopback addresses, 64 MB at most)."
    • addedOutput schema / properties / oa
      Added value: +{
      +  "additionalProperties": true,
      +  "description": "Where an automatically discovered open-access PDF came from, and what version it is. Present only for find_oa.",
      +  "properties": {
      +    "landingPage": {
      +      "description": "The human landing page for this copy, when the location has one.",
      +      "type": "string"
      +    },
      +    "licence": {
      +      "description": "The licence the host declares, as OpenAlex reports it, e.g. \"cc-by\". Absent when none is stated.",
      +      "type": "string"
      +    },
      +    "source": {
      +      "description": "Who hosts the copy, as OpenAlex names them, e.g. \"arXiv\" or \"PubMed Central\".",
      +      "type": "string"
      +    },
      +    "url": {
      +      "description": "The open-access PDF link OpenAlex reported, and where these bytes came from.",
      +      "type": "string"
      +    },
      +    "version": {
      +      "description": "Which version this copy is: \"published\" (the version of record), \"accepted\" (the reviewed author manuscript) or \"submitted\" (a preprint). Absent when OpenAlex does not say.",
      +      "enum": [
      +        "published",
      +        "accepted",
      +        "submitted"
      +      ],
      +      "type": "string"
      +    },
      +    "versionCaveat": {
      +      "description": "Why this copy is not the publisher’s version of record; absent when it is.",
      +      "type": "string"
      +    }
      +  },
      +  "required": [
      +    "url"
      +  ],
      +  "type": "object"
      +}
  2. Changed2 schema fields changedv1.20.2
    • removedInput schema / $schema
      Removed value: -"http://json-schema.org/draft-07/schema#"
    • removedOutput schema / $schema
      Removed value: -"http://json-schema.org/draft-07/schema#"
  3. Changed3 schema fields changedv1.20.0
    • changedInput schema / properties / library_id / description
      Previous value: -"Group library to attach in; forces the cloud path."New value: +"Group library to attach the file in (from zotero_groups); forces the cloud path instead of the desktop app."
    • addedInput schema / properties / library_type / description
      Added value: +"Which library to address: \"user\" (a personal library) or \"group\" (a shared group library). Omit to use the library this server is configured for. \"group\" on its own is refused: pass library_id with it."
    • changedOutput schema / (root)
      Previous value: -nullNew value: +{
      +  "$schema": "http://json-schema.org/draft-07/schema#",
      +  "additionalProperties": true,
      +  "properties": {
      +    "alreadyInStorage": {
      +      "description": "True when Zotero already held these bytes and only the item was created (cloud path).",
      +      "type": "boolean"
      +    },
      +    "attachment": {
      +      "description": "Key of the attachment item created.",
      +      "type": "string"
      +    },
      +    "bytes": {
      +      "description": "Size of the stored file.",
      +      "type": "number"
      +    },
      +    "contentType": {
      +      "description": "MIME type stored, e.g. \"application/pdf\".",
      +      "type": "string"
      +    },
      +    "filename": {
      +      "description": "File name stored.",
      +      "type": "string"
      +    },
      +    "parent": {
      +      "description": "The item it hangs off.",
      +      "type": "string"
      +    },
      +    "target": {
      +      "description": "Where the write went: \"local\" (Zotero desktop local API), \"desktop\" (connector protocol) or \"cloud\" (Zotero Web API).",
      +      "type": "string"
      +    }
      +  },
      +  "required": [
      +    "attachment",
      +    "parent",
      +    "filename",
      +    "bytes",
      +    "contentType"
      +  ],
      +  "type": "object"
      +}
  4. Changed4 schema fields changedv1.6.0
    • addedInput schema / properties / library_id
      Added value: +{
      +  "description": "Group library to attach in; forces the cloud path.",
      +  "type": "integer"
      +}
    • addedInput schema / properties / library_type
      Added value: +{
      +  "enum": [
      +    "user",
      +    "group"
      +  ],
      +  "type": "string"
      +}
    • changedInput schema / properties / path / description
      Previous value: -"Local filesystem path to the file."New value: +"Filesystem path to the file, on the machine running Zoteus."
    • changedInput schema / properties / url / description
      Previous value: -"URL to download the file from."New value: +"URL to download the file from; works on remote/hosted servers."
  5. Addedv1.3.1

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only carry readOnlyHint=false and destructiveHint=false, so the description carries the behavioral burden and delivers thoroughly: it discloses auth prompts ('choose Always Allow'), the ZOTERO_API_KEY requirement with file access, file-storage quota consumption, the desktop-vs-cloud decision, find_oa's refusal behavior, and the manuscript-version caveat with source/version/licence returned in the result. No contradiction with annotations (a write operation matches readOnlyHint=false).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long, but the tool is genuinely complex (three modes, dual save paths, auth, version caveats), and nearly every sentence earns its place. Purpose is front-loaded. The find_oa paragraph is somewhat verbose and could be tightened, but it communicates essential constraints rather than padding.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given an output schema covers return values, the description covers everything an agent needs to call this correctly: all three input modes, mutual exclusivity, auth and quota implications, library handling, inference behavior, and failure conditions. Nothing material is left unspecified for a tool of this complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real value beyond the schema: it articulates the mutual exclusivity of url/path/find_oa, explains that filename/content_type are inferred when omitted, and clarifies that find_oa depends on the parent's DOI. These relationships are not captured in the individual property descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb-resource pair: 'Add a stored file attachment (e.g. a PDF full text) under an existing item.' The scope is precise and clearly differentiates from siblings like zotero_get_fulltext (reads) and zotero_pdf_images (extracts images), making selection unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit mode-selection guidance: 'prefer it over `path` unless the file really is on the server' for url, and 'Use instead of `url`/`path`, not alongside them' for find_oa. It also explains when the desktop app vs cloud path is taken and conditions under which find_oa refuses. This is direct, actionable when-to-use guidance with no inference required.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.