About zenodo-mcp-server
zenodo-mcp-server lets researchers and data users search Zenodo deposits and inspect datasets, software, and publications. It resolves identifiers, traces versions, and supports file and archive previews.
Key features
- 01Search with research metadata filters
- 02Resolve DOI, concept DOI, and URLs
- 03Inspect record metadata and citations
- 04Trace deposit version histories
- 05List files and read text excerpts
Use cases
- 01Find research records by funder or license
- 02Resolve a DOI to its Zenodo record
- 03Compare versions of a deposit
- 04Preview text files or archive members
MCP support
Exposes six tools for MCP clients to search Zenodo, resolve records, browse versions and files, and read text excerpts. Supports STDIO and Streamable HTTP.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://zenodo.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-zenodo https://zenodo.caseyjhand.com/mcp{"mcpServers":{"cyanheads-zenodo":{"type":"http","url":"https://zenodo.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-zenodo":{"type":"http","url":"https://zenodo.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-zenodo":{"type":"http","url":"https://zenodo.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-zenodo":{"serverUrl":"https://zenodo.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
6| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | No | Result page, starting at 1. page × size may not exceed 10,000. |
| size | integer | No | Results per page (1–25). |
| sort | enum | No | Sort order: bestmatch, newest, oldest, mostviewed, mostdownloaded, updated-desc, updated-asc. Case, spaces, hyphens, and underscores are ignored when matching (MostViewed reads as mostviewed). Defaults to bestmatch with a query, newest without.bestmatch | newest | oldest | mostviewed | mostdownloaded | updated-desc | updated-asc |
| award | string | No | One grant: an award id <funder-ror>::<number> (00k4n6c32::101135562), a bare grant number, or a CORDIS award DOI (10.3030/101135562). Find ids with zenodo_lookup_vocabulary (vocabulary: awards). |
| query | string | No | Keyword query. Terms are OR-ed unless joined with AND; "quoted phrases" match exactly; field syntax such as metadata.title:"…" or doi:"10.5281/zenodo.N" works. Escape a literal / as \/. A bare DOI or record URL is rejected; open it with zenodo_get_record instead. Omit to browse with filters only. |
| funder | string | No | One funder: a ROR id (01cwqze88), a ROR URL, or a Crossref Funder DOI (10.13039/100000002). Resolve a funder name with zenodo_lookup_vocabulary (vocabulary: funders). |
| license | string | No | License id, e.g. cc-by-4.0, cc0-1.0, mit (lowercased). Find ids with zenodo_lookup_vocabulary (vocabulary: licenses). |
| community | string | No | One community: its slug, UUID, or zenodo.org/communities/<slug> URL. Find slugs with zenodo_lookup_vocabulary (vocabulary: communities). |
| file_type | array | No | File extensions, OR-ed (a single string is accepted): csv, zip, pdf, … Lowercased with any leading dot stripped. |
| all_versions | boolean | No | Include superseded versions; by default only the latest version is searched. |
| published_to | string | No | Latest publication date, YYYY, YYYY-MM, or YYYY-MM-DD; a partial date ends at its last day. |
| access_status | enum | No | Access status: open, restricted, embargoed, or metadata-only. Case, spaces, hyphens, and underscores are ignored when matching (Metadata Only reads as metadata-only).open | restricted | embargoed | metadata-only |
| creator_orcid | string | No | Creator ORCID iD, e.g. 0000-0002-1825-0097; an https://orcid.org/ prefix is stripped and a trailing x uppercased. |
| resource_type | array | No | Resource type ids, OR-ed (a single string is accepted): dataset, software, publication, publication-article, image-photo, … Any case; a subtype may also be written in the <type>::<id> form zenodo_lookup_vocabulary shows as its search value (publication::publication-article). A top-level type includes its subtypes. Look ids up with zenodo_lookup_vocabulary (vocabulary: resource_types). |
| published_from | string | No | Earliest publication date, YYYY, YYYY-MM, or YYYY-MM-DD; a partial date starts at its first day. |
Details
From the project README@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.
Public Hosted Server: https://zenodo.caseyjhand.com/mcp
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
| Tool | Description |
|---|---|
zenodo_search_records | Search deposits by keyword plus type, community, funder, grant, ORCID, file type, license, access, and date filters, with facet counts |
zenodo_get_record | Resolve one deposit from a record id, DOI, concept DOI, or URL to its full metadata, first 25 files, and an optional citation |
zenodo_list_versions | List every version of a deposit's version series, newest first |
zenodo_list_files | Page a deposit's file manifest, or list the members of one of its .zip files |
zenodo_read_file | Read a byte-capped text excerpt of one file or .zip member |
zenodo_lookup_vocabulary | Resolve community, funder, grant, license, and resource-type names to the ids search filters take |
Capability reference
zenodo_search_records tool
- Keyword
query(terms OR-ed unless joined withAND, quoted phrases, field syntax such asmetadata.title:"…") plusresource_type,community,funder,award,creator_orcid,file_type,license,access_status, andpublished_from/published_to - Up to 25 hits per page; only the first 10,000 matches are reachable (
result_window_exceededpast that); latest versions only unlessall_versionsis true sort:bestmatch,newest,oldest,mostviewed,mostdownloaded,updated-desc,updated-asc(defaultbestmatchwith a query,newestwithout)- Each hit carries record and concept ids and DOIs, type, version, creators, license ids, access, file totals, and unique views and downloads;
facetscount resource types, access statuses, file types, subjects, and years over the full match set - A bare DOI or record URL as
queryfails asquery_is_identifier; acommunityorfunderZenodo doesn't know fails asunknown_community/unknown_funder - A
querythat isn't valid search syntax (climate AND AND model,metadata.title:with no value,'single quotes', an unclosed[a TO b]) fails asquery_syntax, naming the offending token, whenever a filter other thancommunityorall_versionsis set, because Zenodo would search it as plain text and drop the filters; without such a filter it runs as plain text and thenoticesays so. Parts Zenodo cannot run fail asquery_syntaxwith or without a filter, unless the query already runs as plain text: a/with no closing/(escape a literal one as\/), a!with nothing after it (climate!; escape it asclimate\!), a range bound ending in a backslash ([a TO b\]]; double-quote a bound that holds a space,], or}, as in[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). A"inside a word that never closes (12" vinyl; escape it as12\") fails the same way 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
idtakes 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_kindandresolved_fromreport 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 incontinuationswith acursorand the recordidit is bound to; pass both back to get that field's nextwindow(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, ornature- A miss returns
found: falsewithmiss_kind(not_found,deleted,restricted,not_on_zenodo) andguidance; a deleted record adds its removaltombstone
zenodo_list_versions tool
- Takes the same
idforms aszenodo_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: falsewith the samemiss_kind,guidance, andtombstoneaszenodo_get_record
zenodo_list_files tool
- Manifest entries carry key, size, MIME type, MD5, download URL,
previewable, andlistable(a.zip); up to 200 per page viaoffset/limit archive_keylists the members of one.zip; Zenodo lists at most 1,000 files and directories per archive, flagged byupstream_truncatedkey_containsfilters 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
keyfromzenodo_list_files, plusarchive_memberto read inside a.zipwithout downloading itmax_bytesfrom 256 (default 16,384), at most 65,536 per call (larger values return 65,536); continue a top-level file fromnext_offset;.zipmembers read from byte 0 onlystatusistext,not_text,restricted, orempty; 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
rightsand thedownload_url
zenodo_lookup_vocabulary tool
vocabulary:communities,funders,awards,licenses, orresource_types;queryby name, acronym, or keyword, or omit it to browse- Each entry's
filter_paramandfilter_valuename thezenodo_search_recordsfilter and the id to pass it funderscopes 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
reachablebesidetotal
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
.zipmember reads, with binary detection before any text is returned
Agent-friendly output:
- Misses as data:
zenodo_get_recordandzenodo_list_versionsreturnfound: falsewith a typedmiss_kind, next-stepguidance, 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
effectiveQuerysent to Zenodo and theappliedSort, and every paged tool reports totals with a next page or offset - Fields Zenodo omits stay absent rather than defaulting to
0,'', orfalse
Getting started
Public Hosted Instance
A public instance is available at https://zenodo.caseyjhand.com/mcp — no installation required. It serves Streamable HTTP; each client takes its own config shape.
For Claude Code:
claude mcp add --transport http zenodo-mcp-server https://zenodo.caseyjhand.com/mcp
For Cline (cline_mcp_settings.json):
{
"mcpServers": {
"zenodo-mcp-server": {
"type": "streamableHttp",
"url": "https://zenodo.caseyjhand.com/mcp"
}
}
}
For Cursor (mcp.json):
{
"mcpServers": {
"zenodo-mcp-server": {
"url": "https://zenodo.caseyjhand.com/mcp"
}
}
}
Claude Desktop adds remote servers under Settings → Connectors.
The hosted instance runs without a token, so every caller shares Zenodo's anonymous rate limit. For sustained use, run your own instance with ZENODO_ACCESS_TOKEN set.
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"zenodo-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/zenodo-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"zenodo-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/zenodo-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"zenodo-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/zenodo-mcp-server:latest"]
}
}
}
To raise the rate limit, add "ZENODO_ACCESS_TOKEN": "your-token" to env (or -e ZENODO_ACCESS_TOKEN=… for Docker). See Configuration.
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- Optional: a Zenodo personal access token for a higher rate limit.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/zenodo-mcp-server.git
- Navigate into the directory:
cd zenodo-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# optionally set ZENODO_ACCESS_TOKEN
Configuration
| Variable | Description | Default |
|---|---|---|
ZENODO_ACCESS_TOKEN | Zenodo personal access token, created with no scopes. Raises Zenodo's rate limit; tool behavior and page sizes are unchanged. | none |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
LOGS_DIR | Directory for log files (Node.js only). | <app-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry. | false |
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
-
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:http # or bun run start:stdio -
Run checks and tests:
bun run devcheck # Lints, formats, type-checks, and more bun run test # Runs the test suite
Docker
docker build -t zenodo-mcp-server .
docker run --rm -p 3010:3010 zenodo-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/zenodo-mcp-server. OpenTelemetry peer dependencies are installed by default; build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point: server instructions, tool registration, service setup and teardown. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts) plus shared render, schema, and record-miss helpers. |
src/services/zenodo | Zenodo service: HTTP boundary and pacers, cache, identifier parsing, query building, normalization, text previews. |
tests/ | Unit, service, tool, and fuzz tests against recorded Zenodo fixtures. |
docs/design.md | Tool surface design, verified upstream behavior, and decisions log. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor logging; Zenodo responses are cached process-wide in the service, not inctx.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:
bun run devcheck
bun run test
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.