@cyanheads/zenodo-mcp-server
Search and resolve Zenodo datasets, software, and publications by DOI; trace versions and funding, list files, and preview text files via MCP. STDIO or Streamable HTTP.
6 Tools
Overview
Datasets, software releases, and publications from Zenodo, CERN's open research repository. Search deposits with filters for funder, grant, community, license, and file type; resolve any Zenodo DOI, concept DOI, or URL to its record; walk a deposit's versions; and list, open, and preview its files, including members of .zip archives.
Tools
The published zenodo-guide covers selecting a cited release, resolving funding identities, and checking inspection coverage. Plugin installs include it; MCP clients can discover it through the Skills extension and its skill resources.
Capability reference
zenodo_search_records tool
- Keyword
query (terms OR-ed unless joined with AND, quoted phrases, field syntax such as metadata.title:"…") plus resource_type, community, funder, award, creator_orcid, file_type, license, access_status, and published_from / published_to
- Up to 25 hits per page; only the first 10,000 matches are reachable (
result_window_exceeded past that); latest versions only unless all_versions is true
sort: bestmatch, newest, oldest, mostviewed, mostdownloaded, updated-desc, updated-asc (default bestmatch with a query, newest without)
- Each hit carries record and concept ids and DOIs, type, version, creators, license ids, access, file totals, and unique views and downloads;
facets count resource types, access statuses, file types, subjects, and years over the full match set
- A bare DOI or record URL as
query fails as query_is_identifier; a community or funder Zenodo doesn't know fails as unknown_community / unknown_funder
- Invalid search syntax fails as
query_syntax, naming the offending token:
- With a filter other than
community or all_versions set, any invalid syntax fails (climate AND AND model, metadata.title: with no value, 'single quotes', an unclosed [a TO b]), because Zenodo would search it as plain text and drop the filters. Without such a filter it runs as plain text and the notice says so
- Constructs Zenodo cannot run fail with or without a filter, unless the query already runs as plain text: an unclosed
/ (escape a literal one as \/), a trailing ! (climate\!), a range bound ending in a backslash (double-quote a bound holding a space, ], or }: [a TO "b]"]), and a boost, fuzziness, or proximity value Zenodo cannot convert (climate^1.2.3, "x y"~0.5; write ^2, ~0.5, "x y"~2)
- An unclosed
" inside a word (12" vinyl; escape it as 12\") fails without a filter; with one, the phrase is closed at the end of the query and the closed query is checked instead
zenodo_get_record tool
id takes a record id, a Zenodo DOI, a concept DOI or concept record id (resolves to the latest version), another DOI registered to a Zenodo record, or a zenodo.org / doi.org URL; input_kind and resolved_from report how it resolved
- Returns the description as plain text (up to 4,000 characters), creators and contributors with ORCIDs and ROR affiliations, rights, access and embargo, funding, related identifiers, communities, version position with
latest_recid, usage counts, and the first 25 files
- Every capped field reports its total, and each cut except the file list (paged with
zenodo_list_files) is listed in continuations with a cursor and the record id it is bound to; pass both back to get that field's next window (8,000 characters of text, or the next page of entries)
citation_style: bibtex, csl-json, apa, chicago-author-date, harvard-cite-them-right, ieee, modern-language-association, or nature
- A miss returns
found: false with miss_kind (not_found, deleted, restricted, not_on_zenodo) and guidance; a deleted record adds its removal tombstone
zenodo_list_versions tool
- Takes the same
id forms as zenodo_get_record; a concept DOI and any version's DOI list the same series
- Newest first, up to 25 per page; reports
total_versions, latest_recid, and the concept record id and DOI
- Each version carries its record id, DOI, version label, publication date,
index, is_latest, file totals, and unique views and downloads
- Misses return
found: false with the same miss_kind, guidance, and tombstone as zenodo_get_record
zenodo_list_files tool
- Manifest entries carry key, size, MIME type, MD5, download URL,
previewable, and listable (a .zip); up to 200 per page via offset / limit
archive_key lists the members of one .zip; Zenodo lists at most 1,000 files and directories per archive, flagged by upstream_truncated
key_contains filters keys or member paths case-insensitively before paging
- Restricted and embargoed files return the access status and embargo date with no entries; unknown and deleted records fail as
record_not_found / record_deleted
zenodo_read_file tool
key from zenodo_list_files, plus archive_member to read inside a .zip without downloading it
max_bytes from 256 (default 16,384), at most 65,536 per call (larger values return 65,536); continue a top-level file from next_offset; .zip members read from byte 0 only
status is text, not_text, restricted, or empty; excerpts end on a line or character boundary
- Extensionless files typed
application/octet-stream (LICENSE, Makefile) are returned only when their content is UTF-8 text
- Every result carries the deposit's
rights and the download_url
zenodo_lookup_vocabulary tool
vocabulary: communities, funders, awards, licenses, or resource_types; query by name, acronym, or keyword, or omit it to browse
- Each entry's
filter_param and filter_value name the zenodo_search_records filter and the id to pass it
funder scopes awards to one funder, as a ROR id, ROR URL, or Crossref Funder DOI
- Up to 25 entries per page; only the first 10,000 entries of a list are reachable, reported as
reachable beside total
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Zenodo-specific:
- Accepts a record id, Zenodo DOI, concept DOI, external DOI, or zenodo.org / doi.org URL in every tool that takes
id; URLs are parsed locally and never fetched
- Search filters compose into Zenodo's query, so facet counts agree with
total; community and funder values are checked before the search runs
- Separate request pacers for Zenodo's search and general rate-limit buckets, backed by its
X-RateLimit-* headers, plus a process-local cache (records 5 min, searches 60 s, vocabularies 1 h)
- Byte-range file reads and
.zip member reads, with binary detection before any text is returned
Agent-friendly output:
- Misses as data:
zenodo_get_record and zenodo_list_versions return found: false with a typed miss_kind, next-step guidance, and a deleted record's tombstone
- Depositor-supplied text is labeled untrusted: descriptions render as quoted blocks, file content in a code fence, and HTML descriptions are converted to plain text
- Search responses echo the
effectiveQuery sent to Zenodo and the appliedSort, and every paged tool reports totals with a next page or offset
- Fields Zenodo omits stay absent rather than defaulting to
0, '', or false
Getting started
Connect to the hosted instance, no install:
Or run it locally:
For other clients, add this to the MCP config file:
The hosted instance shares Zenodo's anonymous rate limit across callers. An optional Zenodo personal access token in ZENODO_ACCESS_TOKEN raises the rate limit of your own instance (-e ZENODO_ACCESS_TOKEN=your-token after the Claude Code server name, or an env entry). For Docker or a source build, see Running the server.
Configuration
See .env.example for the full list of optional overrides.
Rate limits
Zenodo rate-limits anonymous clients per IP address: 60 requests per minute and 2,000 per hour overall, and 30 per minute on record search. The server paces its own requests below those limits: 25 searches per minute, and 55 other requests per minute and 1,900 per hour. Every caller of one server process shares that budget. When it runs out, tools fail with rate_limited and a retryAfter in seconds.
With ZENODO_ACCESS_TOKEN set, Zenodo allows 100 requests per minute and 5,000 per hour, and the server paces other requests at 90 per minute and 4,800 per hour. Search stays at 25 per minute.
The token authenticates as the account that created it, so reads can see what that account can see, including restricted records it owns. On a shared or hosted server, create the token on a dedicated account that owns no restricted records.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
Docker
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/zenodo-mcp-server. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/zenodo-mcp-server:latest"]. To build locally, docker build -t zenodo-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- Use
ctx.log for logging; Zenodo responses are cached process-wide in the service, not in ctx.state
- Register new tools in
src/mcp-server/tools/definitions/index.ts
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.