Skip to content

arxiv mcp server changelog

io.github.cyanheads/arxiv-mcp-server

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

  • server.json 1 field changed
    version
    1.8.1 1.8.2

  • Resources +1
    • + arxiv-guide

  • AGENTS.md +31 −21

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

    − **Version:** 1.7.0− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`− **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1− - **Content fallback.** `arxiv_read_paper` tries native arXiv HTML first (`arxiv.org/html/{id}`), falls back to ar5iv (`ar5iv.labs.arxiv.org/html/{id}`), then to PDF text extraction via the framework's `pdfParser` (`unpdf`). ar5iv returns 307 redirect (not 404) for missing papers — don't follow redirects, treat 3xx as not-found. When a conversion aborted, ar5iv answers 200 with an empty article and LaTeXML's "Conversion to HTML had a Fatal error" notice; a page whose only text is that notice cou− - **Raw HTML output.** `arxiv_read_paper` cuts the page to its `<article>` — through the last `</article>`, so the site header and footer go while appendices and the bibliography stay — strips LaTeXML class/id noise, collapses MathML to LaTeX, and resolves relative `src`/`href`/`data` values against the URL that served the page (`#fragment`, `mailto:`, and absolute values stay as written), then returns the body HTML — the LLM interprets the content directly. On the `pdf_text` rung the body is pl− - **Query field prefixes mean one thing on both search paths.** `ti:` / `au:` / `abs:` / `co:` / `jr:` route to the five columns of the mirror's FTS5 index; `all:` — like an unprefixed term — spans all five, matching arXiv's own all-fields behavior. `comment` and `journal_ref` joined that index in mirror schema v3, so a prefix added to `FIELD_MAP` must name an indexed column or FTS5 raises `no such column` at query time. A `cat:` operand written inside `query` is expanded through `categorySearch− - **Writers own the mirror file; the server's connection is query-only.** Only writers create, seed, or migrate the file: `MirrorStore.open`, behind `runHarvest` (`mirror:init`, `mirror:refresh`, the cron's child) and `mirror:verify`. The server reads through `MirrorStore.openReader`: `PRAGMA query_only = ON`, no busy timeout (a WAL reader never waits on the writer), and no DDL, seed, or migration. A missing file, one with no recorded schema version, or an older version reads as not ready: logge− - **Mirror harvest: the writer owns its index, its resume point, and its stop.** `runHarvest` — the only writer, behind `mirror:init`, `mirror:refresh`, and the cron's child process — ensures `paper_categories(paper_id)` before its first page (each record write and tombstone cascade looks the junction up by `paper_id`; without it every lookup scans the table). The index is outside the schema version and `MirrorStore.open` never builds it. A run that finds a saved token (`in_progress` or `error`)− - **Mirror search is bounded by its FTS match set.** The probe and the search run in the reader worker (next note). A search with a free-text term first runs `MirrorStore.matchReachesCap`, a doclist count stopped at `MIRROR_SEARCH_MATCH_CAP` (50,000). At the cap the query goes live for every sort order, and a live failure on it is returned, never answered from the mirror (the recency-bypass `catch` included). That `catch` also rethrows a caller cancellation for every query: a cancelled call is n− - **Paper ID normalization.** arXiv API always returns versioned IDs (`2401.12345v1`). Every ID input — `arxiv_get_metadata`, `arxiv_read_paper`, and `arxiv://paper/{paperId}` after percent-decoding — goes through `normalizePaperId` (`src/services/arxiv/paper-id.ts`) before any request: an `arXiv:` prefix is stripped, an arxiv.org `abs`/`pdf`/`html` URL yields its ID (scheme optional; a trailing slash, `?query`, or `#fragment` is ignored), the archive and `v` are lowercased, and an old-style sub− - **Dependencies:** `fast-xml-parser` (v5, class-based API) for Atom XML parsing. No HTML parsing library needed. `unpdf` is a direct dependency only because the framework's `pdfParser` lazy-loads it as an optional peer — keep the range inside mcp-ts-core's declared peer range; never call `unpdf` directly.−       when: 'Paper ID is not present in the arXiv index.',−       recovery: 'Verify the paper ID format and confirm via arxiv_search_papers before retrying.' },−       throw ctx.fail('no_match', `Paper '${params.paperId}' not found.`, {−     when: 'Requested paper ID is not present in the arXiv index.',−     recovery: 'Verify the paper ID format and confirm via arxiv_search_papers before retrying.' },−     throw ctx.fail('no_match', `Paper '${input.paper_id}' 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. `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 + **Version:** 1.8.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+ - **Content fallback.** `arxiv_read_paper` tries native arXiv HTML first (`arxiv.org/html/{id}`), falls back to ar5iv (`ar5iv.labs.arxiv.org/html/{id}`), then to PDF text extraction via the framework's `pdfParser` (`unpdf`). ar5iv returns 307 redirect (not 404) for missing papers — don't follow redirects, treat 3xx as not-found. When a conversion aborted, ar5iv answers 200 with an empty article and LaTeXML's "Conversion to HTML had a Fatal error" notice; a page whose only text is that notice cou+ - **Raw HTML output.** `arxiv_read_paper` cuts the page to its `<article>` — through the last `</article>`, so the site header and footer go while appendices and the bibliography stay — strips LaTeXML class/id noise (the structural classes the outline reads stay), collapses MathML to LaTeX, renders span-built tabulars as `<table>` / `<tr>` / `<td>` / `<th>`, drops presentation markup — every `style`, the `title` on each link, `\phantom` (`visibility:hidden`) spans with their content, and spans l+ - **Query field prefixes mean one thing on both search paths.** `ti:` / `au:` / `abs:` / `co:` / `jr:` route to the five columns of the mirror's FTS5 index; `all:` — like an unprefixed term — spans all five, matching arXiv's own all-fields behavior. `comment` and `journal_ref` joined that index in mirror schema v3, so a prefix added to `FIELD_MAP` must name an indexed column or FTS5 raises `no such column` at query time. A `cat:` operand written inside `query` is expanded through `categorySearch+ - **Writers own the mirror file; the server's connection is query-only.** Only writers create, seed, or migrate the file: `MirrorStore.open`, behind `runHarvest` (`mirror:init`, `mirror:refresh`, the cron's child) and `mirror:verify`. The server reads through `MirrorStore.openReader`: `PRAGMA query_only = ON`, no busy timeout (a WAL reader never waits on the writer), and no DDL, seed, or migration. A missing file, one with no recorded schema version, or an older version reads as not ready: logge+ - **Mirror harvest: the writer owns its resume point and its stop.** `runHarvest` — the only writer, behind `mirror:init`, `mirror:refresh`, and the cron's child process — looks the junction up by `paper_id` on every record write and tombstone cascade through `paper_categories_paper_id_idx (paper_id, category)`, which the schema owns from v5 (a new file creates it, v4 → v5 redefines the runner's old `(paper_id)`), so the run builds no index of its own ([#50](https://github.com/cyanheads/arxiv-mc+ - **Mirror search is bounded by its FTS match set.** The probe and the search run in the reader worker (next note). A search with a free-text term first runs `MirrorStore.matchReachesCap`, a doclist count stopped at `MIRROR_SEARCH_MATCH_CAP` (50,000). At the cap the query goes live for every sort order, and a live failure on it is returned, never answered from the mirror (the recency-bypass `catch` included). That `catch` also rethrows a caller cancellation for every query: a cancelled call is n+ - **Paper ID normalization.** arXiv API always returns versioned IDs (`2401.12345v1`). Every ID input — `arxiv_get_metadata`, `arxiv_read_paper`, and `arxiv://paper/{paperId}` after percent-decoding — goes through `normalizePaperId` (`src/services/arxiv/paper-id.ts`) before any request: an `arXiv:` prefix is stripped, an arxiv.org `abs`/`pdf`/`html` URL yields its ID (scheme optional; a trailing slash, `?query`, or `#fragment` is ignored), the archive and `v` are lowercased, and an old-style sub+ - **Dependencies:** `fast-xml-parser` (v5, class-based API) for Atom XML parsing, with `@nodable/entities` (its own decoder, a direct dependency held to its `^3.0.1` range) supplying the decoder both parsers take, `newArxivEntityDecoder`: numeric references decode in the parse's single pass, never after it, which would turn arXiv's literal `&#8722;` into `−` ([#88](https://github.com/cyanheads/arxiv-mcp-server/issues/88)). No HTML parsing library needed. `unpdf` is a direct dependency only becau+       when: 'arXiv has no paper with this ID, or no such version when the ID pins one.',+       recovery: 'Find the paper with arxiv_search_papers, or recheck the ID against its source; if the ID pins a version, retry without the suffix.' },+       throw ctx.fail('no_match', `arXiv has no paper with ID '${params.paperId}'.`, {+     when: 'arXiv has no paper with the requested ID, or no such version when the ID pins one.',+     recovery: 'Find the paper with arxiv_search_papers, or recheck the ID against its source; if the ID pins a version, retry without the suffix.' },+     throw ctx.fail('no_match', `arXiv has no paper with ID '${input.paper_id}'.`, {+ 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.+ 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' })`. `lint:packaging` validates the tree. This server publishes `skills/arxiv-guide/` and opts in.+ **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 such as the mirror database under `data/` — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line for the paths it kno+ - [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = `arxiv-mcp-server`; `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 `arxiv-mcp-server`, `npx -y @cyanheads/arxiv-mcp-server`, and the same fixed `env` as `.codex-plugin/mcp.json`. Neither file ca+ - [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass+ - [ ] `skills/arxiv-guide/SKILL.md` still holds after any tool change: `lint:packaging` passes, and `field-test` verified every gate and identifier claim in it against live tool behavior
  • server.json 1 field changed
    version
    1.8.0 1.8.1

  • server.json 1 field changed
    version
    1.7.2 1.8.0

  • server.json 1 field changed
    version
    1.7.1 1.7.2

  • server.json 1 field changed
    version
    1.7.0 1.7.1

  • Tools +1 −1
    • + arxiv_search_papers
    • − arxiv_search
  • AGENTS.md +26 −21

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

    − **Version:** 1.5.4− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.12`− **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0− **Zod:** ^4.6.5− - **Content fallback.** `arxiv_read_paper` tries native arXiv HTML first (`arxiv.org/html/{id}`), falls back to ar5iv (`ar5iv.labs.arxiv.org/html/{id}`), then to PDF text extraction via the framework's `pdfParser` (`unpdf`). ar5iv returns 307 redirect (not 404) for missing papers — don't follow redirects, treat 3xx as not-found. An ar5iv failure never ends the chain either: a 5xx, network error, or timeout there is held and re-raised only when the PDF rung also comes up empty, so a third-party o− - **API quirks.** arXiv API returns HTTP 200 for everything — empty results, not-found IDs, and rate limiting. Rate limiting returns plain text `"Rate exceeded."` (not XML) — check content-type before parsing.− - **Raw HTML output.** `arxiv_read_paper` strips HTML head/boilerplate, then returns raw paper body HTML — the LLM interprets the content directly. On the `pdf_text` rung the body is plain text and skips both strippers, so `total_characters` equals `body_characters`. `max_characters` (default: 100,000, `null` for the entire body) controls slice size; `start` (default: 0) is the character offset into the cleaned body — together they page through long papers without re-reading the prefix. Keep the− - **Query field prefixes mean one thing on both search paths.** `ti:` / `au:` / `abs:` / `co:` / `jr:` route to the five columns of the mirror's FTS5 index; `all:` — like an unprefixed term — spans all five, matching arXiv's own all-fields behavior. `comment` and `journal_ref` joined that index in mirror schema v3, so a prefix added to `FIELD_MAP` must name an indexed column or FTS5 raises `no such column` at query time. A `cat:` operand written inside `query` is expanded through `categorySearch− - **Paper ID normalization.** arXiv API always returns versioned IDs (`2401.12345v1`). Inputs accept both versioned and unversioned forms and reach arXiv verbatim — `id_list` and the HTML endpoints honor a version suffix. Resolution is version-aware: an ID pinning `vN` matches only that version (a mirror row at another version is a miss, not a substitute), an unversioned ID matches the latest. Returned `id` fields always include the version, and so do the `pdf_url` / `abstract_url` derived from − - **Dependencies:** `fast-xml-parser` (v5, class-based API) for Atom XML parsing. No HTML parsing library needed. `unpdf` is a direct dependency only because the framework's `pdfParser` lazy-loads it as an optional peer — the version range mirrors mcp-ts-core's declared peer floor; never call `unpdf` directly.− export const arxivSearch = tool('arxiv_search', {−     max_results: z.number().min(1).max(50).default(10).describe('Maximum results to return (1-50).'),−     const result = await service.search(input.query, { maxResults: input.max_results }, ctx);−       recovery: 'Verify the paper ID format and confirm via arxiv_search before retrying.' },− `teardown(core)` is the `setup()` counterpart — release a watcher, socket, or non-`unref()`'d timer there. It runs after the transport stops and before the logger closes, on every shutdown path, and a signal-triggered shutdown then exits the process explicitly (0, or 1 if a step never settles within the framework's 10 s ceiling). This server declares none: the only ref'd timer `setup()` leaves behind is the `node-cron` mirror-refresh job, and the framework disposes `schedulerService` itself furt− **1. Typed error contract (recommended).** Add `errors[]` to the tool/resource and throw via `ctx.fail(reason, …)`. The contract `recovery` string (≥ 5 words, lint-validated) is the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason', msg)` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contai−     recovery: 'Verify the paper ID format and confirm via arxiv_search before retrying.' },−       arxiv-search.tool.ts              # arxiv_search — query search with filters−       arxiv-read-paper.tool.ts          # arxiv_read_paper — raw HTML content− | Files | kebab-case with suffix | `arxiv-search.tool.ts` |− | Tool/resource/prompt names | snake_case | `arxiv_search` |+ **Version:** 1.7.0+ **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+ - **Content fallback.** `arxiv_read_paper` tries native arXiv HTML first (`arxiv.org/html/{id}`), falls back to ar5iv (`ar5iv.labs.arxiv.org/html/{id}`), then to PDF text extraction via the framework's `pdfParser` (`unpdf`). ar5iv returns 307 redirect (not 404) for missing papers — don't follow redirects, treat 3xx as not-found. When a conversion aborted, ar5iv answers 200 with an empty article and LaTeXML's "Conversion to HTML had a Fatal error" notice; a page whose only text is that notice cou+ - **API quirks.** arXiv API returns HTTP 200 for everything — empty results, not-found IDs, and rate limiting. Rate limiting returns plain text `"Rate exceeded."` (not XML) — check content-type before parsing. The exceptions are two HTTP 500s. A query request whose `start + max_results` passes 10,000 answers 500 whatever the total, so the live search path cuts a page at result 10,000 and refuses a `start` at or past it (`start_beyond_result_limit`) before any request; the mirror has no such limi+ - **Raw HTML output.** `arxiv_read_paper` cuts the page to its `<article>` — through the last `</article>`, so the site header and footer go while appendices and the bibliography stay — strips LaTeXML class/id noise, collapses MathML to LaTeX, and resolves relative `src`/`href`/`data` values against the URL that served the page (`#fragment`, `mailto:`, and absolute values stay as written), then returns the body HTML — the LLM interprets the content directly. On the `pdf_text` rung the body is pl+ - **Query field prefixes mean one thing on both search paths.** `ti:` / `au:` / `abs:` / `co:` / `jr:` route to the five columns of the mirror's FTS5 index; `all:` — like an unprefixed term — spans all five, matching arXiv's own all-fields behavior. `comment` and `journal_ref` joined that index in mirror schema v3, so a prefix added to `FIELD_MAP` must name an indexed column or FTS5 raises `no such column` at query time. A `cat:` operand written inside `query` is expanded through `categorySearch+ - **Writers own the mirror file; the server's connection is query-only.** Only writers create, seed, or migrate the file: `MirrorStore.open`, behind `runHarvest` (`mirror:init`, `mirror:refresh`, the cron's child) and `mirror:verify`. The server reads through `MirrorStore.openReader`: `PRAGMA query_only = ON`, no busy timeout (a WAL reader never waits on the writer), and no DDL, seed, or migration. A missing file, one with no recorded schema version, or an older version reads as not ready: logge+ - **Mirror harvest: the writer owns its index, its resume point, and its stop.** `runHarvest` — the only writer, behind `mirror:init`, `mirror:refresh`, and the cron's child process — ensures `paper_categories(paper_id)` before its first page (each record write and tombstone cascade looks the junction up by `paper_id`; without it every lookup scans the table). The index is outside the schema version and `MirrorStore.open` never builds it. A run that finds a saved token (`in_progress` or `error`)+ - **Mirror search is bounded by its FTS match set.** The probe and the search run in the reader worker (next note). A search with a free-text term first runs `MirrorStore.matchReachesCap`, a doclist count stopped at `MIRROR_SEARCH_MATCH_CAP` (50,000). At the cap the query goes live for every sort order, and a live failure on it is returned, never answered from the mirror (the recency-bypass `catch` included). That `catch` also rethrows a caller cancellation for every query: a cancelled call is n+ - **Mirror searches run in the reader worker.** `searchInWorker` (`src/services/arxiv/mirror/reader.ts`) runs `searchWithinCap` (the probe, then `store.search`) in one long-lived worker thread on its own `MirrorStore.openReader` connection, started by the first mirror search and reused, one search at a time in arrival order, so no mirror search holds the request thread longer than posting the call and receiving its page. ID lookups and the harvest-state read stay on the request thread. A cancell+ - **Paper ID normalization.** arXiv API always returns versioned IDs (`2401.12345v1`). Every ID input — `arxiv_get_metadata`, `arxiv_read_paper`, and `arxiv://paper/{paperId}` after percent-decoding — goes through `normalizePaperId` (`src/services/arxiv/paper-id.ts`) before any request: an `arXiv:` prefix is stripped, an arxiv.org `abs`/`pdf`/`html` URL yields its ID (scheme optional; a trailing slash, `?query`, or `#fragment` is ignored), the archive and `v` are lowercased, and an old-style sub+ - **Dependencies:** `fast-xml-parser` (v5, class-based API) for Atom XML parsing. No HTML parsing library needed. `unpdf` is a direct dependency only because the framework's `pdfParser` lazy-loads it as an optional peer — keep the range inside mcp-ts-core's declared peer range; never call `unpdf` directly.+ export const arxivSearchPapers = tool('arxiv_search_papers', {+     max_results: z.number().int().min(1).max(10_000).default(10).describe('Maximum results to return; at most 50 per call.'),+     const result = await service.search(input.query, { maxResults: Math.min(input.max_results, 50) }, ctx);+       recovery: 'Verify the paper ID format and confirm via arxiv_search_papers before retrying.' },+ `teardown(core)` is the `setup()` counterpart — release a watcher, socket, or non-`unref()`'d timer there. It runs after the transport stops and before the logger closes, on every shutdown path, and a signal-triggered shutdown then exits the process explicitly (0, or 1 if a step never settles within the framework's 10 s ceiling). This server's `teardown` terminates the mirror reader worker (`closeReaderWorker()`) without awaiting it: termination cannot interrupt a running statement, and the exit+ **1. Typed error contract (recommended).** Add `errors[]` to the tool/resource and throw via `ctx.fail(reason, …)`. The contract `recovery` string (≥ 5 words, lint-validated) is the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason', msg)` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contai+     recovery: 'Verify the paper ID format and confirm via arxiv_search_papers before retrying.' },+       paper-id.ts                       # normalizePaperId — the parser every ID input runs through+       arxiv-search-papers.tool.ts       # arxiv_search_papers — query search with filters+       arxiv-read-paper.tool.ts          # arxiv_read_paper — full text (HTML or PDF) + outline+ | Files | kebab-case with suffix | `arxiv-search-papers.tool.ts` |+ | Tool/resource/prompt names | snake_case | `arxiv_search_papers` |

  • server.json 1 field changed
    version
    1.6.1 1.7.0

  • server.json 2 fields changed
    env_vars
    ARXIV_API_BASE_URL, ARXIV_REQUEST_DELAY_MS, ARXIV_CONTENT_TIMEOUT_MS, ARXIV_API_TIMEOUT_MS, MCP_LOG_LEVEL ARXIV_API_BASE_URL, ARXIV_REQUEST_DELAY_MS, ARXIV_CONTENT_TIMEOUT_MS, ARXIV_API_TIMEOUT_MS, ARXIV_READ_TIMEOUT_MS, MCP_LOG_LEVEL
    version
    1.6.0 1.6.1

  • server.json 1 field changed
    version
    1.5.4 1.6.0

  • AGENTS.md +23 −23

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

    − **Version:** 1.5.3− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`− **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0−         ...ctx.recoveryFor('no_match'),− `parseEnvConfig` maps Zod schema paths → env var names so validation errors name the actual variable (`ARXIV_API_BASE_URL`) rather than the internal path (`apiBaseUrl`).− | `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. |− **1. Typed error contract (recommended).** Add `errors[]` to the tool/resource and throw via `ctx.fail(reason, …)`. The contract `recovery` string (≥ 5 words, lint-validated) is the single source of truth for the agent's next move; spread `ctx.recoveryFor(reason)` into `data` to mirror it onto the wire as `data.recovery.hint`. The framework also mirrors the hint into `content[]` text so format-only clients see the same guidance, unless the message already contains it verbatim; forwarding it is l−       ...ctx.recoveryFor('no_match'),− **Service-thrown reasons.** Services don't have `ctx.fail`, but they receive `ctx`. Pass `data: { reason, ...ctx.recoveryFor(reason) }` from a factory throw — the auto-classifier preserves `data` so clients see the same `error.data.reason` they'd see from `ctx.fail`.−   ...ctx.recoveryFor('unknown_category'),− 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. Keep root `skills/` free for skills intended for installing agents: plugin hosts auto-load that directory.− | `git-wrapup` | Land verified changes as a versioned commit stack; opens a release PR only when the project declares that mode. |− | `release-pr-review` | Review an open release PR, land fixes as ordinary commits on top of the stack, and keep the PR body current. Release PR mode only. |− | `release-and-publish` | Tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup`. |− | `api-canvas` | DataCanvas (Tier 3, opt-in) — register tabular data, run SQL, export, plus `spillover()` for big result sets |− | `bun run audit:fix` | Run `bun audit fix` to patch vulnerable packages within existing ranges. Try first, then `bun update <name>`, then `bun dedupe`. |− | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Last resort: every ranged dependency re-resolves, including the framework. |− **Adding an env var requires both files:** `server.json` (`environmentVariables[]`) and `manifest.json` (`mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match. Wire each user option as `${user_config.<key>}`; optional string options use `"default": ""`. A raw `${ENV_VAR}` is not substituted by the MCPB host.− **Section order** (Keep a Changelog): Added, Changed, Deprecated, Removed, Fixed, Security, then Dependencies. Include only sections with entries — don't ship empty headers.− **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, plain push — pushed history is never rewritten, PR body kept in sync, one summary comment)− - [ ] Tools/resources that throw domain failures declare `errors[]` with `recovery` (≥ 5 words) and route through `ctx.fail(reason, …)` — no try/catch in handlers+ **Version:** 1.5.4+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.12`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0+ - **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.+ `parseEnvConfig` maps Zod schema paths → env var names so validation errors name the actual variable (`ARXIV_API_BASE_URL`) rather than the internal path (`apiBaseUrl`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.+ 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`. |+ **1. Typed error contract (recommended).** Add `errors[]` to the tool/resource and throw via `ctx.fail(reason, …)`. The contract `recovery` string (≥ 5 words, lint-validated) is the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason', msg)` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contai+ **Service-thrown reasons.** Services don't have `ctx.fail`. A factory throw carries only `{ reason, … }` in `data` — the auto-classifier preserves it, so clients see the same `error.data.reason` they'd see from `ctx.fail`, and the framework fills the contract's `recovery`.+ 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+ | `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |+ | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync. Release PR mode only |+ | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |+ | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |+ | `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` |+ **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": ""`.+ **Section order:** the Keep a Changelog sequence — Added, Changed, Deprecated, Removed, Fixed, Security — then `Dependencies` last. Include only sections with entries — don't ship empty headers.+ **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, plain push — pushed history is never rewritten, PR bo+ - [ ] Tools/resources that throw domain failures declare `errors[]` with `recovery` (≥ 5 words) and route through `ctx.fail(reason, …)` — no try/catch in handlers; a service throw carries only `{ reason, … }`

  • server.json 1 field changed
    version
    1.5.3 1.5.4