MCP server¶
zotio-mcp exposes the CLI to MCP hosts like Claude Desktop and Claude Code. Install and register it as shown in Install.
Command-orchestration facade (default)¶
By default the server exposes three framework tools — context, search, and sql — plus a command-orchestration facade (command_search and command_run) and workflow_submit, a validated inline multi-step workflow tool. Agents can read domain context, search/query the synced local store directly, drive the CLI the same way a human would (search for the right command, then run it), and submit whole workflows — all on the same trust model as the CLI. Writes applied over MCP are journaled and replayed into the local mirror exactly like CLI writes.
The rationale and trade-offs are summarized in Architecture decisions › MCP command surface, with the full records in the repo.
Switching surfaces¶
Set ZOTIO_MCP_SURFACE=mirror to expose each MCP-eligible CLI command as one lean tool (global flags stripped). Commands annotated mcp:hidden, including the arbitrary-argument local-file workflow run runner, remain CLI-only — agents run multi-step workflows through the validated workflow_submit tool instead (see Workflows & triggers). The retired spec-derived typed endpoint tools (collections_*, items_*, schema_*, tags_*, …) are no longer part of either surface; use command_run or the mirror for those workflows.
Context resources¶
Beyond tools, the server serves live Zotero context as MCP resources:
zotero://context·zotero://agent-context— CLI + library introspectionzotero://status·zotero://freshness— connectivity and cache statezotero://schema— Zotero item-type and field schemazotero://capabilities— the read/write trust registry (reference)
Authentication¶
The ZOTERO_API_KEY env var is optional for read-only local-desktop use (the local API needs no key). Set it to enable writes and reach group libraries — see Authentication.
Concurrent access¶
Only one zotio writer may update an installation or independent output at a time. Installation writers use the host-user lock ~/.zotio/.writer.lock: --config, ZOTERO_CONFIG, and ZOTIO_DATA_DIR do not make concurrent writer scopes because profiles remain shared at ~/.zotio/profiles.json. A concurrent write fails immediately with exit code 9 and is safe to retry after the active writer finishes; read-only commands and dry-run previews remain available.
Library content is data, never instruction¶
An item's title, abstract, note, tag, or annotation is authored by whoever can write to the library — a group co-member, a scraped web page, a metadata provider — and this same server exposes the CLI's write surface. Results that carry library content are therefore framed before they reach the host model, so an injected "ignore previous instructions" reads as a quoted string rather than as an operator directive:
- JSON results (
search,sql, and the library resources: collections, items, bundles, manifests, reading notes) carry a top-level_zotio_provenanceobject —source: zotero_library,trust: untrusted_data, and a notice naming the content as data. Existing shapes are preserved:searchkeeps itsitems/countenvelope,sqlkeeps itsrowsobject, and resources keep their object shape andapplication/jsonMIME type. A field named_zotio_provenancein the library itself cannot forge it, because the authoritative value is written after decoding. - Non-JSON text results — including mirrored CLI stdout that is not JSON — are wrapped in a preamble plus a per-call nonce-delimited
<<<ZOTERO-DATA …>>>block, so content cannot close the block and resume as trusted text. Unsafe C0/DEL bytes are neutralized in the same pass (tab, LF, and CR survive); mirrored stdout that is valid JSON keeps its shape and is neither nonce-framed nor marker-injected, and JSON encoding escapes any control byte inside it. - Trusted resources stay unframed:
zotero://context,zotero://agent-context,zotero://status,zotero://freshness,zotero://schema, andzotero://capabilitiesdescribe this CLI, not your library.
Bounds: every result stays under 60 KB, a top-level array payload is capped at 50 items, and an oversized object is truncated recursively — its nested arrays reduced to at most 50 entries — with the truncation recorded in the payload. Small object results, such as a compact sql row envelope, are bounded by bytes rather than by a 50-row cap. Library resources newly adopt this bound; the native tool bounds are unchanged.
Framing is mitigation, not a boundary. What actually decides whether a model's request mutates anything is the CLI's own write gate: command_run forwards the flags the model supplies, and every mutating command previews unless yes is passed. There is no allowlist of mutating commands — every runnable command is reachable — so the gate is the boundary, and it now has no exceptions: a model that withholds yes cannot write. dry_run overrides yes, which is what makes a workflow_submit preview safe, since the runner previews each step by injecting --dry-run.