Skip to content

crossref mcp server AGENTS.md changelog

io.github.cyanheads/crossref-mcp-server

3 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.

  • AGENTS.md +13 −8

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

    − **Version:** 0.5.1− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`− **Engines:** Bun ≥1.4.0, Node ≥24.0.0− - **Polite-pool `mailto` is optional but recommended.** Every Crossref request includes `User-Agent: crossref-mcp-server/<version> (mailto:<CROSSREF_MAILTO>)` when set; the not-found registry lookup to `https://doi.org` sends the bare `crossref-mcp-server/<version>` without the mailto. Without it, the server starts but logs a warning and uses the anonymous pool with stricter rate limits. Polite-pool access requires no token — just the email in the header.− **Crossref's rejections of caller input are declared reasons.** A 400, and a 404 whose body is `cursor-invalid`, map on Crossref's own rejection `type` onto the entries beside `UPSTREAM_ERROR_CONTRACT` in `upstream-errors.ts`: `unknown_filter` (`filter-not-available`, with `data.suggestion` when Crossref lists the hyphenated key as valid), `sort_cursor_conflict`, `invalid_cursor`, and `invalid_parameter` for any other type or a body that does not parse. `data.rejected` lists every entry as `{ ty−       crossref-service.ts               # HTTP client, polite-pool User-Agent, retry, pagination−       registration-agency.ts            # notFoundDetail — not-found text branched on the DOI registry's answer− `bun 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+ **Version:** 0.5.2+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.22`+ **Engines:** Bun ≥1.4.4, Node ≥24.0.0+ - **Polite-pool `mailto` is optional but recommended.** Every Crossref request includes `User-Agent: crossref-mcp-server/<version> (mailto:<CROSSREF_MAILTO>)` when set; the not-found registry lookup and the citation request, both to `https://doi.org`, send the bare `crossref-mcp-server/<version>` without the mailto. Without it, the server starts but logs a warning and uses the anonymous pool with stricter rate limits. Polite-pool access requires no token — just the email in the header.+ - **A not-found DOI costs up to two more requests, both to `https://doi.org`.** The registry lookup (`https://doi.org/doiRA/{doi}`) names the agency; when it names DataCite, JaLC, mEDRA, KISTI, or OP, one more request to `https://doi.org/{doi}` asks for CSL-JSON, inside the same one-timeout window as the registry lookup, and the citation reaches `data.citation` and the message. Every other outcome leaves the error as it was without one.+ **Crossref's rejections of caller input are declared reasons.** A 400, and a 404 whose body is `cursor-invalid`, map on Crossref's own rejection `type` onto the entries beside `UPSTREAM_ERROR_CONTRACT` in `upstream-errors.ts`: `unknown_filter` (`filter-not-available`, with `data.suggestion` when Crossref lists the hyphenated key as valid), `sort_cursor_conflict`, `invalid_cursor`, and `invalid_parameter` for any other type or a body that does not parse. `data.rejected` lists every entry as `{ ty+       crossref-service.ts               # HTTP client, polite-pool User-Agent, retry, pagination, registry/citation lookup, rejection mapping+       date-parts.ts                     # parseDateParts / formatDateParts / resolveWorkDate / resolveWorkSummaryDate+       markup-text.ts                    # normalizeText / normalizeMarkupText / normalizeAbstract / normalizeReferenceText, the strip passes and tag tables+       cursor-notice.ts                  # cursorNotice — advisory notice for a cursor that does not decode as a Crossref continuation token+       registration-agency.ts            # notFoundDetail — not-found text branched on the DOI registry's answer, plus the citation of a DOI another agency registered (citationOf, citationSentence)+       work-dates.ts                     # event / approved — fields, projection, line+ `bun 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

  • AGENTS.md +31 −17

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

    − **Version:** 0.4.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− crossref-mcp-server wraps the [Crossref REST API](https://api.crossref.org/) to expose canonical scholarly metadata for ~155 million registered works (journal articles, books, book chapters, conference papers, preprints, datasets, components). It is the authoritative source for DOI-registered metadata — titles, authors, affiliations, abstracts (where deposited), licenses, full-text links, funder acknowledgements, and outgoing reference lists.− - **Polite-pool `mailto` is optional but recommended.** Every request includes `User-Agent: crossref-mcp-server/0.4.2 (mailto:<CROSSREF_MAILTO>)` when set. Without it, the server starts but logs a warning and uses the anonymous pool with stricter rate limits. Polite-pool access requires no token — just the email in the header.− - **Offset paging is capped at ~10K** on `/works` and on both works sub-resources (100K on the `/journals` and `/funders` name searches). Deep paging requires `cursor=*` on the first request, then chaining `next-cursor` tokens; the name-search routes do not accept a cursor. Cursor and offset cannot be combined.− - **A cursor cannot carry a publication-date sort.** Crossref refuses `published`, `published-print`, `published-online`, and `issued` beside `cursor` (`sort-criteria-incompatible-with-cursor`), and a walk with no sort runs oldest-registered first. The journal and funder works lists therefore walk a `works_cursor` by `sort=created&order=desc` (newest registered first) and keep `sort=published&order=desc` on offset pages; `crossref_search_works` passes the caller's `sort` through, and a refusal c− - **`select=` works on `/works` (search) only.** It is not supported on `/works/{doi}` (single-fetch). `crossref_get_references` fetches the full record and extracts `reference[]` client-side.−       markdown-text.ts                  # mdText / mdTextAtLineStart — the content[] escape− 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 (run by devcheck) |− `bun 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 − - [ ] The works sub-resources send `sort=created&order=desc` beside a cursor and `sort=published&order=desc` on an offset page — never a publication-date sort beside a cursor− - [ ] `select=` parameter only passed to `/works` (search), never to `/works/{doi}` (single-fetch)− - [ ] Cursor and offset cannot be combined — throw `cursor_offset_conflict` (`crossref_search_works`) or `works_cursor_offset_conflict` (the journal/funder works sub-resources) if both are supplied+ **Version:** 0.5.1+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0+ **Zod:** ^4.6.7+ crossref-mcp-server wraps the [Crossref REST API](https://api.crossref.org/) to expose canonical scholarly metadata for ~188 million registered works (journal articles, books, book chapters, conference papers, preprints, datasets, components). It is the authoritative source for DOI-registered metadata — titles, authors, affiliations, abstracts (where deposited), licenses, full-text links, funder acknowledgements, and outgoing reference lists.+ - **Polite-pool `mailto` is optional but recommended.** Every Crossref request includes `User-Agent: crossref-mcp-server/<version> (mailto:<CROSSREF_MAILTO>)` when set; the not-found registry lookup to `https://doi.org` sends the bare `crossref-mcp-server/<version>` without the mailto. Without it, the server starts but logs a warning and uses the anonymous pool with stricter rate limits. Polite-pool access requires no token — just the email in the header.+ - **Offset paging is capped at ~10K** on `/works` and `/funders/{id}/works`, the two routes the works lists read (100K on the `/journals` and `/funders` name searches). Deep paging requires `cursor=*` on the first request, then chaining `next-cursor` tokens; the name-search routes do not accept a cursor. Cursor and offset cannot be combined.+ - **A cursor cannot carry a publication-date sort.** Crossref refuses `published`, `published-print`, `published-online`, and `issued` beside `cursor` (`sort-criteria-incompatible-with-cursor`), and a walk with no sort runs oldest-registered first. The journal and funder works lists therefore walk a `works_cursor` by `sort=created&order=desc` (newest registered first) and keep `sort=published&order=desc` on offset pages; `crossref_search_works` passes the caller's `sort` and `order` through — a + - **`select=` works on `/works` and `/funders/{id}/works`, never on `/works/{doi}`** (400 `parameter-not-allowed`), so `crossref_get_references` fetches the full record and extracts `reference[]` client-side. Every works-list request names the fields its summary reads — `crossref_search_works` the caller's `fields`, or every name its summary projects (`SELECTABLE_FIELDS`) when there are none; the journal and funder works lists `WORKS_LIST_SELECT` — since without one Crossref returns whole record+       markdown-text.ts                  # mdText / mdTextAtLineStart / mdTextInHeading — the content[] escape+       metadata-coverage.ts              # Coverage schema, projection, and renderer — member and journal tools+       page-size.ts                      # Per-input page-size caps + clampPageSize — a value above its cap is clamped with a notice+       registration-agency.ts            # notFoundDetail — not-found text branched on the DOI registry's answer+       work-updates.ts                   # updatedBy / updateTo — fields, projection, line+ 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. Publishes `crossref-guide` (`skills/crossref-guide/SKILL.md`): judgment for the `crossref_*` tools — confirming a citation match, journal/funder works lists versus a filtered works search, filter syntax, DOI prefix versus current publisher, deposit-dependent coverage | You, deliberately, with the `add-skill` skill |+ 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.+ A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image, and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })` (wired in `src/index.ts`; served as `skill://crossref-guide/SKILL.md`). `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 (run by devcheck) |+ `bun 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+ - [ ] The journal and funder works lists send `sort=created&order=desc` beside a cursor and `sort=published&order=desc` on an offset page — never a publication-date sort beside a cursor+ - [ ] Every works-list request carries `select=` naming the fields its summary reads — a raw key a projection starts reading joins `SELECTABLE_FIELDS` (`crossref_search_works`) or `WORKS_LIST_SELECT` (the journal and funder works lists) in the same change — and `/works/{doi}` (single-fetch) never gets one+ - [ ] Cursor and offset cannot be combined — throw `cursor_offset_conflict` (`crossref_search_works`) or `works_cursor_offset_conflict` (the journal/funder works lists) if both are supplied+ - [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name (lowercase `a-z`, `0-9`, `-`, `.` only); `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y <full package name>`, and the same fixed `env` as `.code+ - [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass+ - [ ] If the server publishes a skill in root `skills/`: `lint:packaging` passes, and `field-test` verified every gate and identifier claim in it against live tool behavior

  • AGENTS.md +27 −27

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

    − **Version:** 0.4.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− - **Polite-pool `mailto` is optional but recommended.** Every request includes `User-Agent: crossref-mcp-server/0.4.1 (mailto:<CROSSREF_MAILTO>)` when set. Without it, the server starts but logs a warning and uses the anonymous pool with stricter rate limits. Polite-pool access requires no token — just the email in the header.−       throw ctx.fail('doi_not_found', `No record for DOI ${doi}`, ctx.recoveryFor('doi_not_found'));− | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |− | `ctx.inputs` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |− | `ctx.requestId` | Unique request ID. |− **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reaso−     throw ctx.fail('doi_not_found', `No record for DOI ${doi}`, {−       doi,−       ...ctx.recoveryFor('doi_not_found'),−     });− 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.− Run `bun run list-skills` to get an indexed list of all available local skills with their paths.− | `devcheck` | Lint, format, typecheck, audit |− | `bun run lint:mcp` | Validate MCP definitions against spec (rule catalog: `api-linter` skill) |− | `bun run list-skills` | List available local skills with paths |− | `bun run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |− | `bun run audit:fix` | Upgrade vulnerable dependencies within existing ranges with `bun audit fix`. |− | `bun run audit:refresh` | Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe`: delete the lockfile, reinstall, and re-audit. Re-resolves every ranged dependency. |− `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. MCPB is stdio-only — HTTP deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.− **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.− **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 changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — pushed history is never rewritten, so no fixup, no autosquash, and no for− - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name; `interface.shortDescription` from `package.json` description− - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; env vars added for any required API keys− - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name, user-supplied variables declared in `userConfig` and referenced as `${user_config.<option>}`+ **Version:** 0.4.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+ - **Polite-pool `mailto` is optional but recommended.** Every request includes `User-Agent: crossref-mcp-server/0.4.2 (mailto:<CROSSREF_MAILTO>)` when set. Without it, the server starts but logs a warning and uses the anonymous pool with stricter rate limits. Polite-pool access requires no token — just the email in the header.+ - **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.+       throw ctx.fail('doi_not_found', `No record for DOI ${doi}`);+ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.+ | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |+ | `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |+ | `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |+ | `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the+     throw ctx.fail('doi_not_found', `No record for DOI ${doi}`, { doi });+ 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+ **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `framework-skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.+ **Runtime:** Scripts use Bun's native TypeScript execution — `bun run <cmd>` is the standard invocation. `npm run <cmd>` also works (npm delegates to bun).+ | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |+ | `bun run list-skills` | Print the skill registry |+ | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |+ | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |+ | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |+ `bun 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 + **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.+ **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 — pushed history is never rewritten, s+ - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description+ - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset+ - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never wri