Skip to content

zenodo mcp server changelog

io.github.cyanheads/zenodo-mcp-server

8 changes recorded. MCP Harbor re-reads this listing from the official registry every six hours and asks the live server what it exposes, and keeps what changed between one look and the next.

  • Resources +1
    • + zenodo-guide
  • AGENTS.md +19 −8

    https://raw.githubusercontent.com/cyanheads/zenodo-mcp-server/HEAD/AGENTS.md

    − **Version:** 0.1.3− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`− **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1− Six read-only tools over the Zenodo REST API (`https://zenodo.org/api`, InvenioRDM): `zenodo_search_records`, `zenodo_get_record`, `zenodo_list_versions`, `zenodo_list_files`, `zenodo_read_file`, `zenodo_lookup_vocabulary`. No resources, no prompts. No credentials are needed; the optional `ZENODO_ACCESS_TOKEN` raises the rate limit and changes nothing else. [`docs/design.md`](./docs/design.md) holds the per-tool contracts, the verified upstream behavior behind them, and the decisions log — read − Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant f− **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.− | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, plugin manifest identity, README version badge (run by devcheck) |− `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was + **Version:** 0.2.0+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0+ Six read-only tools over the Zenodo REST API (`https://zenodo.org/api`, InvenioRDM): `zenodo_search_records`, `zenodo_get_record`, `zenodo_list_versions`, `zenodo_list_files`, `zenodo_read_file`, `zenodo_lookup_vocabulary`. No research-data resources or prompts; the SDK serves the published `zenodo-guide` through the Skills extension, skill resources, and the `skill://{+path}` template. No credentials are needed; the optional `ZENODO_ACCESS_TOKEN` raises the rate limit. [`docs/design.md`](./docs+   skills: 'skills',+ Two skill trees live at the project root, written for different readers:+ | Tree | For | Written by |+ |:-----|:----|:-----------|+ | `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |+ | `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Absent by default; usually none is needed |+ Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.+ The published [zenodo-guide](./skills/zenodo-guide/SKILL.md) covers release selection, funding identities, and inspection coverage. Root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image, and is served over MCP by `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.+ **Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.+ | `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, .mcpbignore coverage, plugin manifests, the published `skills/` tree, README version badge (run by devcheck) |+ `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by+ - [ ] `plugin.json` + `mcp.json` populated from `package.json`; the root stdio entry carries fixed env only, while `.codex-plugin/plugin.json` retains `"mcpServers": "./.codex-plugin/mcp.json"` to forward `env_vars`+ - [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; root `skills/` ships, and `bun run bundle` + `lint:packaging` pass+ - [ ] If publishing a skill in root `skills/`: `lint:packaging` passes and `field-test` verifies its gates and identifier claims against live tools

  • server.json 1 field changed
    version
    0.1.4 0.2.0

  • server.json 1 field changed
    version
    0.1.3 0.1.4

  • AGENTS.md +22 −15

    https://raw.githubusercontent.com/cyanheads/zenodo-mcp-server/HEAD/AGENTS.md

    − **Version:** 0.1.2− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.14`− **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0− **Zod:** ^4.6.5−     page: z.number().int().min(1).default(1).describe('Result page, starting at 1. page × size may not exceed 10,000.'),−     size: z.number().int().min(1).max(25).default(25).describe('Versions per page (1–25).'),−     cap: z.number().describe('Page size applied.'),−     notice: z.string().optional().describe('Guidance on paging or a page past the end.'),−     ctx.enrich({ truncated: false, shown: 0, cap: input.size, totalCount: 0 }); // required keys, first line−     const lookup = await service.listVersions(recid, input.page, input.size, ctx);−     const hasMore = input.page * input.size < lookup.total;−         cap: input.size,−         guidance: `Showing ${lookup.hits.length} of ${lookup.total} versions; call again with page ${input.page + 1}.`,−     query-builder.ts                    # Composes the search q from filters− **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the + **Version:** 0.1.3+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1+ **Zod:** ^4.6.7+ - **A filtered query must parse.** Zenodo matches a `q` its parser (luqum 1.0.0) rejects as plain text, dropping every filter clause. With any filter in `q`, `zenodo_search_records` checks the balanced query with `querySyntaxError()` (`query-builder.ts`) and fails `query_syntax` before any upstream call; with none, it sends the query and says so in `notice`. A `^`/`~` value luqum cannot convert (`querySyntaxError()` reports it as `boost`, `fuzzy`, or `proximity`) and a query part Zenodo's search+ - **One paging rule.** Every paged tool takes its 10,000-hit window guard and `has_more` from `windowRejection()` / `hasMorePages()` (`paging.ts`); the guard runs before any upstream call. A budget input above its per-call cap (`size`, `limit`, `max_bytes`) is served at the cap through `applyCap()`, never rejected: the applied value drives the request, cache key, guard, continuation, output, and `cap`, and the notice carries the `capNote` when the cap withheld results.+ import { applyCap, hasMorePages, PAGE_SIZE_CAP, windowRejection } from '../paging.js';+     page: z.number().int().min(1).default(1).describe('Result page, starting at 1. Zenodo pages through only the first 10,000 versions, …'),+     size: z.number().int().min(1).max(10_000).default(25).describe('Versions per page, at most 25 per call; larger values return 25.'),+     cap: z.number().describe('Page size applied (at most 25).'),+     notice: z.string().optional().describe('Guidance on paging, the 25-per-call size cap, or a page past the end.'),+     const { applied: size, capNote } = applyCap('size', input.size, PAGE_SIZE_CAP); // 26+ runs at 25+     ctx.enrich({ truncated: false, shown: 0, cap: size, totalCount: 0 }); // required keys, first line+     const pastWindow = windowRejection(input.page, size, 'versions', capNote); // before any upstream call+     if (pastWindow) throw ctx.fail('result_window_exceeded', pastWindow);+     const lookup = await service.listVersions(recid, input.page, size, ctx);+     const hasMore = hasMorePages(input.page, size, lookup.total); // page × size < min(total, 10,000)+         cap: size,+         guidance: `Showing ${lookup.hits.length} of ${lookup.total.toLocaleString('en-US')} versions${capNote ? ` (${capNote})` : ''}; call again with page ${input.page + 1}.`,+     query-builder.ts                    # Composes the search q from filters; checks the query against Zenodo's grammar+     paging.ts                           # 10,000-hit window guard, has_more, per-call caps+ **Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewri

  • server.json 1 field changed
    version
    0.1.2 0.1.3

  • AGENTS.md +12 −11

    https://raw.githubusercontent.com/cyanheads/zenodo-mcp-server/HEAD/AGENTS.md

    − **Version:** 0.1.1− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`− **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0−       throw ctx.fail('invalid_identifier', ref.message, ctx.recoveryFor('invalid_identifier'));− No handler calls `ctx.requestInput`, so no `sessionMode` requirement is declared; the Dockerfile and `.env.example` run HTTP as `stateless`.− | `ctx.fail` / `ctx.recoveryFor` | Throw a declared error-contract reason with its recovery hint: `throw ctx.fail('reason', message, ctx.recoveryFor('reason'))`. |− | `ctx.requestId` | Unique request ID. |− **Typed error contract.** Every tool declares `errors: [{ reason, code, when, recovery, retryable?, thrownBy? }]` inline and throws with `ctx.fail(reason, …, ctx.recoveryFor(reason))`. `when` is model-facing text (the framework advertises it), so write it for the calling model — no implementation terms like "record GET" or "pacer shed". `recovery` names the next tool call. Reasons the service throws (`rate_limited`, `record_unavailable`, `upstream_timeout`, `query_failed`, `archive_unavailable`)−     throw ctx.fail('unknown_community', `No Zenodo community matches "${inline(input.community)}" …`, ctx.recoveryFor('unknown_community'));− **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's bri− - [ ] Handlers throw on failure via declared contract reasons (`ctx.fail` + `ctx.recoveryFor`), no try/catch+ **Version:** 0.1.2+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.14`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0+       throw ctx.fail('invalid_identifier', ref.message);+   sessionMode: 'stateless',+ `sessionMode: 'stateless'` declares the HTTP session posture in source: no handler calls `ctx.requestInput`, so nothing needs a session, and no `require: 'stateful'` is set. The Dockerfile and `.env.example` set `MCP_SESSION_MODE=stateless` to match; a deployment's own `MCP_SESSION_MODE` still wins when it carries a value.+ | `ctx.fail` | Throw a declared error-contract reason: `throw ctx.fail('reason', message)`. The framework adds the reason's declared recovery hint. |+ | `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |+ **Typed error contract.** Every tool declares `errors: [{ reason, code, when, recovery, retryable?, thrownBy? }]` inline and throws with `ctx.fail(reason, …)`; services throw with `data: { reason }`. The framework puts the declared `recovery` on the wire whenever a failure carrying that `reason` arrives without a hint — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; pass an explicit `{ recovery: { hint: '…' } }` only when runtime context +     throw ctx.fail('unknown_community', `No Zenodo community matches "${inline(input.community)}" …`);+ **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the + - [ ] Handlers throw on failure via declared contract reasons (`ctx.fail`), no try/catch

  • server.json 3 fields changed
    remote_url
    — https://zenodo.caseyjhand.com/mcp
    transport
    stdio streamable-http
    version
    0.1.1 0.1.2