Releases: herbertkokholm/cite-caddy
Release list
v2.3.4
Serve /.well-known/glama.json and /llms.txt
Glama.ai's Connector listing requires glama.json to be served at
/.well-known/glama.json to verify server ownership -- confirmed missing in
production (nginx access log showed Glama's crawler getting a 404 there).
Also adds llms.txt (the llmstxt.org convention),
served at /llms.txt, following the same pattern. Both are unauthenticated
routes, same as the existing /.well-known/mcp/server-card.json//healthz/
/status routes -- no change to /mcp's auth behavior. See #14.
Dependencies
Bumps pyzotero 1.13.4 -> 1.13.6 (#13).
v2.3.3
Enable DNS-rebinding protection on the streamable-http transport
The mcp SDK ships Host/Origin header validation
(TransportSecurityMiddleware) but disables it by default unless a
deployment explicitly opts in -- this server wasn't. Flagged by an
external MCP compliance scanner (mcpdebugger.dev)
as failing the 2026 Streamable HTTP transport contract.
main()'s streamable-http path now wires up TransportSecuritySettings,
scoped to MCP_PUBLIC_URL (host + origin) -- see #11.
Dependencies
Bumps pyzotero 1.13.3 -> 1.13.4 (#9).
v2.3.2
server-card.json: stop emitting explicit nulls
/.well-known/mcp/server-card.json was serializing each tool's
outputSchema/annotations with a plain model_dump(), so a tool
without an output schema -- or any unset annotation hint like
destructiveHint/idempotentHint -- came through as an explicit JSON
null instead of being omitted. That's inconsistent with the MCP SDK's
own tools/list serialization, which always excludes unset fields, and
a strict client-side JSON Schema validator could reasonably reject a
null where it expects either a missing key or a concrete value.
Fixed by using exclude_none=True and only including outputSchema/
annotations in a tool's card entry when the tool actually has them.
v2.3.1
Server-card metadata: fill in description, homepage, icon, output schemas, annotations
/.well-known/mcp/server-card.json was omitting serverInfo.description/
homepage/icon and each tool's outputSchema/annotations, even
though that data already exists via MCP_WEBSITE_URL and each tool's own
@mcp.tool() registration -- it just wasn't being copied over into the
card. Filling these in is what Smithery's own quality-score dashboard
flagged as missing after publishing there.
Smithery badges
README and the landing page now link to the
Smithery listing,
alongside the existing Glama badge.
v2.3.0
/.well-known/mcp/server-card.json discovery endpoint
A pragmatic approximation of SEP-2127
("MCP Server Cards -- HTTP Server Discovery", superseding the withdrawn
SEP-1649) -- which is not a ratified part of the MCP spec: the PR is
still open, and even its own well-known path has shifted between drafts
(some revisions use .well-known/ai-catalog.json instead of this one).
If SEP-2127 (or a successor) ratifies with a different
path/shape, this endpoint will need to change to match.
Generated live from the actual tool registry (mcp.list_tools()) on every
request rather than a static file, so it reports the real 39 tools with
accurate schemas and can't drift out of sync as tools are added or changed.
v2.2.0
list_creator_fields
Zotero's /creatorFields lists the name-shape fields (firstName,
lastName, name, ...) valid on a creators entry -- distinct from
list_item_creator_types, which lists creatorType roles (author,
editor, ...) for one item type. Nothing in this server previously
exposed it.
Idempotency-key + update_publication_status
delete_item_permanently, delete_collection, delete_tag,
delete_saved_search, and move_item_to_different_library now accept
an optional idempotency_key. A retry with the same key and arguments
replays the original outcome -- success or error -- instead of
running against Zotero again.
Caching failures, not just successes, is the point: it's what actually
closes move_item_to_different_library's documented duplicate-on-retry
risk. That tool recreates the item in the target library, then deletes
it from the source. If the create succeeds but the delete then fails,
a bare retry today redoes the whole thing -- since the source item's
version hasn't changed -- creating a second duplicate in the target
library. With idempotency_key, the retry replays the cached failure
instead of touching Zotero again.
Also new: update_publication_status, for "a preprint just got a
DOI." Same in-place, key-preserving patch machinery as update_item,
but the one tool allowed to change item_type too (e.g. preprint ->
journalArticle) -- update_item still forbids that, since changing
itemType changes which fields are valid and this is the one place
that's the deliberate point of the call. Idempotency-key-protected by
default.
export_bibliography
Formatted HTML bibliography/citation entries (in a given CSL style)
or portable export data (csljson, bibtex) for a list of item keys.
BibTeX comes back as one combined, ready-to-paste text blob rather than
a fragmented parsed structure. RIS isn't supported yet -- pyzotero's
response-format dispatch only recognizes it by exact Content-Type
header, and that path is unverified.
v2.1.0
/status and /status.html monitoring endpoints
Two new unauthenticated GET routes, HTTP mode only (alongside /healthz):
/status-- JSON snapshot of process uptime, onboarded-tenant count,
and per-tool call/error totals. Aggregate only, by tool name -- no
per-tenant or per-library breakdown, which is what keeps it safe to
leave unauthenticated./status.html-- the same data rendered as a small page (connector
icon + name, a metrics table, a per-tool call/error table) for a human
checking in a browser rather than a script.
Counts are tracked by a new tools/call middleware and in-memory
counters in app/metrics.py -- deliberately not a metrics/analytics
system, just an "is it up and roughly how busy is it" signal that
resets on every restart/redeploy.
The login page (/login) also gets the same connector icon now,
top-centered above the heading.
v2.0.0
Structured creators
get_item/search_items/list_trash/create_item/update_item's
creators field was a single flattened "Fornavn Efternavn, ..."
string. That discarded each creator's creatorType (author vs. editor
vs. seriesEditor vs. translator vs. contributor) and the
firstName/lastName vs. single-field name (institutions) distinction --
already leading to a real misread, where a proceedings' editors were
parsed as co-authors.
creators is now a list of {creatorType, firstName, lastName} (or
{creatorType, name} for single-field/institutional creators) entries --
the same shape create_item/update_item already accepted as input, so
a result from get_item/search_items can be passed straight back into
an update_item call.
Breaking change: creators's type changes from str to list[dict]
across all five tools built on the shared item summary. No
backward-compat shim -- any client parsing the old comma-joined string
needs updating to read the structured entries instead.
v1.5.0
Upgraded to mcp 2.0.0
FastMCP was renamed to MCPServer and moved from mcp.server.fastmcp
to mcp.server; host/port/stateless_http moved from the
constructor to mcp.run()'s kwargs. Reporting our own package version
in serverInfo no longer needs the _mcp_server private-attribute
workaround, since MCPServer now takes version= directly.
Verified against the real mcp==2.0.0 package: the
OAuthAuthorizationServerProvider protocol app/oauth_provider.py
implements is unchanged, and all 139 tests pass.
No user-facing behavior change -- same tools, same OAuth flow, same
transport selection.
v1.4.0
Correct serverInfo.version, pin Docker builds to uv.lock, and make the
connector website/icon configurable.
Surfaced while verifying v1.3.0's icon/website_url wiring end-to-end
against the live server via MCPJam Inspector's OAuth debugger: it showed
v1.29.0, which turned out to be the installed mcp SDK package's own
version, not zotero-mcp's (FastMCP has no version= constructor param, so
the SDK silently falls back to its own package version). Digging into why
production had mcp==1.29.0 while the local lockfile pins 1.28.1 found
the real bug: the Dockerfile installed via pip install . straight from
pyproject.toml, completely bypassing uv.lock, so production dependency
versions could silently drift from what's tested locally/in CI.
serverInfo.versionnow correctly reports zotero-mcp's own installed
version (mcp._mcp_server.versionset fromimportlib.metadata),
matchingpyproject.toml.Dockerfilenow installs viauv sync --frozen --no-dev, so the image
gets exactly what's pinned inuv.lock-- a stale/drifted lock now
fails the build loudly instead of silently resolving to newer releases.MCP_WEBSITE_URLis now an optional env var (previously hardcoded) --
website_url/iconsare cleanly omitted when it's unset instead of
building a broken icon URL. Documented in.env.example, the README's
Configuration section, and the module docstring.- The docs landing page now shows the connector icon too (favicon +
header), not just the MCP server's own serverInfo.