@cyanheads/pubmed-mcp-server 2.10.13 → 2.10.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/AGENTS.md +22 -4
  2. package/CLAUDE.md +22 -4
  3. package/README.md +5 -5
  4. package/changelog/2.0.x/2.0.0.md +32 -0
  5. package/changelog/2.0.x/2.0.1.md +32 -0
  6. package/changelog/2.1.x/2.1.0.md +17 -0
  7. package/changelog/2.1.x/2.1.1.md +29 -0
  8. package/changelog/2.1.x/2.1.2.md +18 -0
  9. package/changelog/2.1.x/2.1.3.md +10 -0
  10. package/changelog/2.1.x/2.1.4.md +12 -0
  11. package/changelog/2.1.x/2.1.5.md +18 -0
  12. package/changelog/2.1.x/2.1.6.md +15 -0
  13. package/changelog/2.10.x/2.10.0.md +17 -0
  14. package/changelog/2.10.x/2.10.1.md +14 -0
  15. package/changelog/2.10.x/2.10.10.md +22 -0
  16. package/changelog/2.10.x/2.10.11.md +21 -0
  17. package/changelog/2.10.x/2.10.12.md +14 -0
  18. package/changelog/2.10.x/2.10.13.md +28 -0
  19. package/changelog/2.10.x/2.10.14.md +28 -0
  20. package/changelog/2.10.x/2.10.2.md +13 -0
  21. package/changelog/2.10.x/2.10.3.md +26 -0
  22. package/changelog/2.10.x/2.10.4.md +11 -0
  23. package/changelog/2.10.x/2.10.5.md +31 -0
  24. package/changelog/2.10.x/2.10.6.md +30 -0
  25. package/changelog/2.10.x/2.10.7.md +17 -0
  26. package/changelog/2.10.x/2.10.8.md +22 -0
  27. package/changelog/2.10.x/2.10.9.md +15 -0
  28. package/changelog/2.2.x/2.2.0.md +67 -0
  29. package/changelog/2.2.x/2.2.1.md +10 -0
  30. package/changelog/2.2.x/2.2.2.md +20 -0
  31. package/changelog/2.2.x/2.2.3.md +17 -0
  32. package/changelog/2.2.x/2.2.4.md +34 -0
  33. package/changelog/2.2.x/2.2.5.md +10 -0
  34. package/changelog/2.2.x/2.2.6.md +17 -0
  35. package/changelog/2.3.x/2.3.0.md +27 -0
  36. package/changelog/2.3.x/2.3.1.md +15 -0
  37. package/changelog/2.3.x/2.3.10.md +20 -0
  38. package/changelog/2.3.x/2.3.11.md +21 -0
  39. package/changelog/2.3.x/2.3.2.md +27 -0
  40. package/changelog/2.3.x/2.3.3.md +38 -0
  41. package/changelog/2.3.x/2.3.4.md +21 -0
  42. package/changelog/2.3.x/2.3.5.md +24 -0
  43. package/changelog/2.3.x/2.3.6.md +26 -0
  44. package/changelog/2.3.x/2.3.7.md +31 -0
  45. package/changelog/2.3.x/2.3.8.md +19 -0
  46. package/changelog/2.3.x/2.3.9.md +22 -0
  47. package/changelog/2.4.x/2.4.0.md +34 -0
  48. package/changelog/2.4.x/2.4.1.md +32 -0
  49. package/changelog/2.5.x/2.5.0.md +35 -0
  50. package/changelog/2.5.x/2.5.1.md +32 -0
  51. package/changelog/2.5.x/2.5.2.md +23 -0
  52. package/changelog/2.5.x/2.5.3.md +22 -0
  53. package/changelog/2.5.x/2.5.5.md +52 -0
  54. package/changelog/2.5.x/2.5.6.md +33 -0
  55. package/changelog/2.6.x/2.6.0.md +32 -0
  56. package/changelog/2.6.x/2.6.1.md +26 -0
  57. package/changelog/2.6.x/2.6.10.md +16 -0
  58. package/changelog/2.6.x/2.6.11.md +24 -0
  59. package/changelog/2.6.x/2.6.12.md +29 -0
  60. package/changelog/2.6.x/2.6.2.md +23 -0
  61. package/changelog/2.6.x/2.6.3.md +17 -0
  62. package/changelog/2.6.x/2.6.4.md +21 -0
  63. package/changelog/2.6.x/2.6.5.md +30 -0
  64. package/changelog/2.6.x/2.6.6.md +25 -0
  65. package/changelog/2.6.x/2.6.7.md +37 -0
  66. package/changelog/2.6.x/2.6.8.md +15 -0
  67. package/changelog/2.6.x/2.6.9.md +36 -0
  68. package/changelog/2.7.x/2.7.0.md +41 -0
  69. package/changelog/2.7.x/2.7.1.md +21 -0
  70. package/changelog/2.7.x/2.7.10.md +13 -0
  71. package/changelog/2.7.x/2.7.11.md +15 -0
  72. package/changelog/2.7.x/2.7.2.md +22 -0
  73. package/changelog/2.7.x/2.7.3.md +18 -0
  74. package/changelog/2.7.x/2.7.4.md +15 -0
  75. package/changelog/2.7.x/2.7.5.md +34 -0
  76. package/changelog/2.7.x/2.7.6.md +14 -0
  77. package/changelog/2.7.x/2.7.7.md +14 -0
  78. package/changelog/2.7.x/2.7.8.md +18 -0
  79. package/changelog/2.7.x/2.7.9.md +16 -0
  80. package/changelog/2.8.x/2.8.0.md +23 -0
  81. package/changelog/2.9.x/2.9.0.md +21 -0
  82. package/changelog/2.9.x/2.9.1.md +12 -0
  83. package/changelog/2.9.x/2.9.10.md +15 -0
  84. package/changelog/2.9.x/2.9.2.md +21 -0
  85. package/changelog/2.9.x/2.9.3.md +11 -0
  86. package/changelog/2.9.x/2.9.4.md +24 -0
  87. package/changelog/2.9.x/2.9.5.md +20 -0
  88. package/changelog/2.9.x/2.9.6.md +22 -0
  89. package/changelog/2.9.x/2.9.7.md +26 -0
  90. package/changelog/2.9.x/2.9.8.md +15 -0
  91. package/changelog/2.9.x/2.9.9.md +35 -0
  92. package/changelog/template.md +151 -0
  93. package/dist/index.js +1 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/services/europe-pmc/europe-pmc-service.d.ts.map +1 -1
  96. package/dist/services/europe-pmc/europe-pmc-service.js +3 -10
  97. package/dist/services/europe-pmc/europe-pmc-service.js.map +1 -1
  98. package/dist/services/ncbi/ncbi-service.d.ts +0 -2
  99. package/dist/services/ncbi/ncbi-service.d.ts.map +1 -1
  100. package/dist/services/ncbi/ncbi-service.js +2 -9
  101. package/dist/services/ncbi/ncbi-service.js.map +1 -1
  102. package/dist/services/openalex/openalex-service.d.ts.map +1 -1
  103. package/dist/services/openalex/openalex-service.js +3 -10
  104. package/dist/services/openalex/openalex-service.js.map +1 -1
  105. package/package.json +17 -8
  106. package/server.json +3 -3
  107. package/dist/services/retry-policy.d.ts +0 -18
  108. package/dist/services/retry-policy.d.ts.map +0 -1
  109. package/dist/services/retry-policy.js +0 -21
  110. package/dist/services/retry-policy.js.map +0 -1
@@ -0,0 +1,22 @@
1
+ ---
2
+ summary: "`pubmed_search_articles` — restores DOI/PMC IDs in brief summaries (closes [#17](https://github.com/cyanheads/pubmed-mcp-server/issues/17)); adds date validation, filter docs, empty-result guidance (closes #18)."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.3.9 — 2026-04-20
7
+
8
+ ## Fixed
9
+
10
+ - **`pubmed_search_articles` — DOI and PMC IDs missing from every brief summary** (`esummary-parser.ts`): `parseSingleDocumentSummary` matched lowercase keys (`idtype`/`value`) from the JSON ESummary shape, but the call site requests `retmode=xml` and fast-xml-parser preserves element casing. Real NCBI XML returns `{ IdType, IdTypeN, Value }`, so every search summary silently dropped its DOI and PMC ID. Normalized via small accessor helpers that accept both shapes; widened `ESummaryArticleId` to reflect the dual casing. Test fixture updated to use the real XML shape (the prior lowercase fixture passed because it tested the implementation, not the behavior).
11
+
12
+ ## Changed
13
+
14
+ - **`pubmed_search_articles` — input validation and empty-result guidance** (`search-articles.tool.ts`):
15
+ - `dateRange.minDate`/`maxDate` now validated by regex (`YYYY`, `YYYY/MM`, or `YYYY/MM/DD` with `/`, `-`, or `.` separators). Empty strings still accepted for the MCP Inspector payload shape; obvious typos like `not-a-date` now fail at the schema boundary with an actionable message instead of degrading silently to 0 results.
16
+ - `publicationTypes` and `meshTerms` descriptions now state their join semantics (OR'd vs AND'd) — the asymmetry wasn't discoverable from the schema alone.
17
+ - New optional `notice` field surfaces guidance when the response would otherwise be a bare empty array: suggests `pubmed_spell_check` on no-filter misses, filter relaxation on filtered misses, and flags pagination overshoot (`offset >= totalFound`). Absent on successful pages. Rendered as a blockquote in `format()` so both human and LLM consumers see it.
18
+
19
+ ## References
20
+
21
+ - Closes [#17](https://github.com/cyanheads/pubmed-mcp-server/issues/17) — DOI/PMC extraction bug surfaced by field-testing `pubmed_search_articles`.
22
+ - Closes [#18](https://github.com/cyanheads/pubmed-mcp-server/issues/18) — UX polish for empty results, input validation, and filter-semantics docs from the same field-test.
@@ -0,0 +1,34 @@
1
+ ---
2
+ summary: "Extends the `content[]`-completeness work from #26 across the rest of the tool surface."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.4.0 — 2026-04-20
7
+
8
+ Extends the `content[]`-completeness work from #26 across the rest of the tool surface. Every tool that previously dropped schema fields from rendered markdown now renders what the LLM sees, and the shared PMID validation logic is deduplicated into a single schema. Also bumps `@cyanheads/mcp-ts-core` 0.4.1 → 0.5.0 and migrates the server config to the new `parseEnvConfig` helper for actionable startup errors.
9
+
10
+ ## Added
11
+
12
+ - **Shared `pmidStringSchema` export** (`src/mcp-server/tools/definitions/_schemas.ts`): Consolidates the `z.string().regex(/^\d+$/, <message>)` guard that four tool files (`fetch-articles`, `fetch-fulltext`, `find-related`, `format-citations`) each duplicated inline. The message is unified so it reads naturally in both array and scalar contexts; a future refinement now updates one file instead of four.
13
+ - **`pubmed_fetch_articles` — MeSH UIs in `content[]`** (`fetch-articles.tool.ts`): Rendered `descriptorUi` and `qualifierUi` inline with their names (`Breast Neoplasms [D001943] * (pathology [Q000473])`). The UI codes are canonical keys the LLM can hand directly to `pubmed_lookup_mesh` or use in `{ui}[MeSH Terms]` search filters without name-matching fuzziness. (#30)
14
+ - **`pubmed_search_articles` — raw PMCID in summaries** (`search-articles.tool.ts`): Summary entries now include `**PMCID:** PMC12345` alongside the existing `**PMC:** {url}` line, so the LLM can copy-paste the canonical ID into downstream `pubmed_fetch_fulltext` or `pubmed_convert_ids` calls without string-parsing the URL. Parallel to the raw-PMCID fix #26 applied to `pubmed_fetch_articles`. (#31)
15
+
16
+ ## Fixed
17
+
18
+ - **`pubmed_fetch_fulltext` — `format()` silently dropped schema fields from `content[]`** (`fetch-fulltext.tool.ts`): Parallel to #26 for `fetch_articles`. Authors now render as a bulleted list with full `givenNames lastName` — no more `first3 + "et al."` truncation that silently hid authors 4+ from the LLM. Collective authors render as `{name} (collective)`. The journal line now includes `ISSN {issn}` when present. Section and subsection headings are prefixed with their JATS `label` when present (`#### 1 Introduction`, `##### 1.1 Background`), aiding cross-reference navigation. Field-tested against `PMC9575052` — confirmed ISSN, section labels "1"/"2", and all four authors now appear in `content[]`. (#29)
19
+ - **`pubmed_convert_ids` — error rows overwrote the DOI column** (`convert-ids.tool.ts`): The prior format stuffed `errmsg` into the DOI cell of the markdown table (`| id | - | - | Error: msg |`), so an LLM parsing by column index would read the error as a DOI, and any partial pmid/pmcid data accompanying an error was silently discarded. Split into two distinct sections: a success table and a separate `### Errors` bulleted list (`- **{id}:** {errmsg}`). The structuredContent shape is unchanged. (#32)
20
+ - **`pubmed_fetch_articles` — grant with only `acronym` rendered `"NIH (NIH)"`** (`fetch-articles.tool.ts`): When a grant carried `acronym` without `grantId`, `format()` produced the acronym duplicated in both slots of the `"{grantId} ({acronym})"` template. Now renders `"NIH"` alone in that case; the happy-path `"R01 EY05922 (EY)"` rendering is unchanged. Not covered by any existing test — discovered during an audit of the surrounding code.
21
+
22
+ ## Changed
23
+
24
+ - **Framework bump — `@cyanheads/mcp-ts-core` 0.4.1 → 0.5.0** (minor) (`package.json`, `bun.lock`): Brings `parseEnvConfig` (opt-in env-var-aware config errors), framework-level ZodError conversion at startup (printed as a banner rather than a JSON dump), and a rewritten `maintenance` skill (v1.2 → v1.3).
25
+ - **`getServerConfig()` migrated to `parseEnvConfig`** (`src/config/server-config.ts`): Validation errors now name the actual environment variable at fault rather than the internal Zod path — `NCBI_REQUEST_DELAY_MS (requestDelayMs): expected number` instead of `requestDelayMs: expected number, received NaN`. Moved the dynamic "API-key-present → 100ms delay" logic out of inline env plumbing into a post-parse override so the Zod schema stays declarative. Added an `emptyAsUndefined` preprocessor on `apiKey` and `adminEmail` to preserve the empty-string-as-unset semantics the previous implementation provided via `env.VAR || undefined` — without it, `NCBI_ADMIN_EMAIL=` would fail `z.email()` validation instead of being treated as "no admin email configured". No runtime behavior change for existing consumers: same field names, same types, same defaults.
26
+ - **`maintenance` skill synced to v1.3** (`skills/maintenance/SKILL.md`, plus agent-directory copies in `.claude/skills/` and `.agents/skills/`): Rewritten around a two-mode flow (Mode A — full update-investigate-adopt flow, Mode B — post-update review), delegates per-package release-note investigation to the `changelog` skill, and documents the two-phase skill sync (package → project → agent dirs).
27
+ - **In-file consistency in `fetch-articles.tool.ts`**: Replaced a lone `a.affiliations.forEach((aff, i) => ...)` with `for (const [i, aff] of a.affiliations.entries())` to match the `for...of` convention used everywhere else in the same file.
28
+
29
+ ## References
30
+
31
+ - Closes [#29](https://github.com/cyanheads/pubmed-mcp-server/issues/29) — `fetch_fulltext` format() dropped fields from `content[]` that were present in `structuredContent`.
32
+ - Closes [#30](https://github.com/cyanheads/pubmed-mcp-server/issues/30) — `fetch_articles` MeSH `descriptorUi` / `qualifierUi` missing from `content[]`.
33
+ - Closes [#31](https://github.com/cyanheads/pubmed-mcp-server/issues/31) — `search_articles` summaries rendered `pmcUrl` but not the raw `pmcId`.
34
+ - Closes [#32](https://github.com/cyanheads/pubmed-mcp-server/issues/32) — `convert_ids` error rows reused the DOI column for `errmsg`, hiding any other fields.
@@ -0,0 +1,32 @@
1
+ ---
2
+ summary: "Adopts `@cyanheads/mcp-ts-core` 0.5.3, whose new `format-parity` lint rule flagged 20 tool fields that were declared in `output` but never rendered by `format()`."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.4.1 — 2026-04-20
7
+
8
+ Adopts `@cyanheads/mcp-ts-core` 0.5.3, whose new `format-parity` lint rule flagged 20 tool fields that were declared in `output` but never rendered by `format()`. Every flagged field now appears in both surfaces, so `content[]`-reading clients (e.g., Claude Desktop) see the same data as `structuredContent`-reading clients (e.g., Claude Code).
9
+
10
+ ## Fixed
11
+
12
+ - **`pubmed_spell_check` — `hasSuggestion` invisible in `content[]`** (`spell-check.tool.ts`): Neither branch of `format()` included the word "suggestion" as a whole word or the boolean value, so a `content[]`-only client had no way to tell whether a correction was offered. Renamed the label to `**Suggestion:**` and reworded the no-suggestion branch to read `No suggestion — query "<q>" appears correct as written.`, so the key name now matches in either case.
13
+ - **`pubmed_convert_ids` — error rows hid pmid/pmcid/doi** (`convert-ids.tool.ts`): The prior split-table layout (introduced by #32) was invisible to the parity rule because synthetic lint input sets `errmsg` on every record, routing all rows through the errors-only branch that never rendered pmid/pmcid/doi. Collapsed back to a single table with an added `Error` column — keeps columns semantically correct (the root concern of #32) while rendering every declared field on every row.
14
+ - **`pubmed_lookup_citation` — `detail` not rendered for `matched` status** (`lookup-citation.tool.ts`): `detail` was emitted only inside the `ambiguous` and `not_found` branches. Moved the render above the branch so it prints whenever present, regardless of status.
15
+ - **`pubmed_fetch_articles` — author/journal/MeSH fields silently dropped** (`fetch-articles.tool.ts`): `formatAuthor` short-circuited on `collectiveName`, hiding `lastName`, `firstName`, `initials`, `affiliationIndices`, and `orcid` for any collective author. `ji.isoAbbreviation ?? ji.title` showed only one. `ji.eIssn ? … : ji.issn ? …` showed only one. `formatPublicationDate` returned `medlineDate` alone and skipped year/month/day. MeSH `isMajorTopic` rendered as a bare `*` that the permissive matcher couldn't tie back to the `isMajorTopic` key. All six paths now render every present field: author lines show `<name> (<initials>) [aff 0,1] · ORCID <id>`, journals show full title `(<iso>), <date>, <vol>(<iss>), <pages>, ISSN <issn>, eISSN <eissn>`, MeSH major topics render as `(major)`, and affiliations switched to `- [0] <text>` so the 0-indexed `affiliationIndices` values line up with the list they reference.
16
+ - **`pubmed_fetch_fulltext` — author names and reference ids silently dropped** (`fetch-fulltext.tool.ts`): Same `collectiveName` short-circuit problem as `fetch-articles`. Reference lines rendered `label ?? id`, so a reference with both only showed one. Authors now render collective name then individual `givenNames lastName`; reference tags now render both `label` and `id` when present (`[1 gks1195-B1] <citation>`).
17
+
18
+ ## Changed
19
+
20
+ - **Framework bump — `@cyanheads/mcp-ts-core` 0.5.0 → 0.5.3** (patch) (`package.json`, `bun.lock`): 0.5.1 is doc polish and retroactive skill version bumps; 0.5.2 adds the `format-parity` lint rule enforced at startup and via `bun run devcheck`; 0.5.3 rewrites the diagnostic wording around dual-surface parity (some clients forward `structuredContent`, others `content[]`, both must carry the full picture) and ships a new `check-docs-sync.ts` script for newly-scaffolded projects — not auto-adopted here since our `scripts/devcheck.ts` is a standalone copy.
21
+ - **Project skills synced from 0.5.3** (`skills/`, `.agents/skills/`, `.claude/skills/`): `add-tool` v1.4 → v1.6, `api-config` v1.1 → v1.2, `design-mcp-server` v2.3 → v2.4, `field-test` v1.1 → v1.2, `polish-docs-meta` v1.3 → v1.4, `setup` v1.2 → v1.3. Deleted `skills/devcheck/` (removed upstream in 0.5.2 — the skill was a thin restatement of the `devcheck` command already documented in the agent protocol).
22
+
23
+ ## Tests
24
+
25
+ - **Updated eight assertions** in `convert-ids.tool.test.ts`, `spell-check.tool.test.ts`, and `fetch-articles.tool.test.ts` to match the new rendering — author-line format, affiliation list ordering, MeSH `(major)` label, unified convert-ids table, suggestion-branch wording.
26
+ - Full suite: **392 passed** / 4 skipped / 0 regressions. `bun run devcheck` green across all 8 checks including the new `format-parity` rule (0 errors, was 20).
27
+ - **Verified end-to-end against live HTTP server** for all five touched tools (`spell_check`, `convert_ids`, `lookup_citation`, `fetch_articles`, `fetch_fulltext`): both `content[]` and `structuredContent` surfaces carry the same fields for real PubMed data.
28
+
29
+ ## References
30
+
31
+ - Issue [#32](https://github.com/cyanheads/pubmed-mcp-server/issues/32): convert_ids error-column semantics — my fix preserves the column-integrity concern from the original issue but switches from the 2.4.0 split-section layout to a unified table with an explicit `Error` column (the issue's Option B).
32
+ - Upstream: `@cyanheads/mcp-ts-core` 0.5.2 format-parity rule.
@@ -0,0 +1,35 @@
1
+ ---
2
+ summary: "Three feature tracks land together: MCPmed-aligned semantic concept tags on every tool, an HTTP landing page with per-tool view-source links, and a framework bump to `@cyanheads/mcp-ts-core` 0.6.3 that exposes `sourceUrl?` on definitions so the…"
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.5.0 — 2026-04-21
7
+
8
+ Three feature tracks land together: MCPmed-aligned semantic concept tags on every tool, an HTTP landing page with per-tool view-source links, and a framework bump to `@cyanheads/mcp-ts-core` 0.6.3 that exposes `sourceUrl?` on definitions so the auto-derived path convention is overridable without file renames or type casts.
9
+
10
+ ## Added
11
+
12
+ - **Ontology-backed semantic concept tags on every tool** (`_concepts.ts`, all 9 `*.tool.ts` files): Each tool now emits `_meta['io.mcpmed/concepts']` with resolvable Schema.org (`SearchAction`, `ScholarlyArticle`, `CreativeWork`, `DefinedTerm`, `DefinedTermSet`) and EDAM (`operation_2421` Database search, `operation_2422` Data retrieval, `operation_3282` ID mapping, `operation_0335` Data formatting, `topic_0089` Ontology and terminology, `data_1187` PubMed ID, `data_2091` Accession) URIs — implementing the concept-mapping proposal from Flotho et al., *Briefings in Bioinformatics* 2026 (doi:10.1093/bib/bbag076) with real URIs rather than the paper's placeholder strings. Namespace key follows the MCP `_meta` spec (`<reverse-dns>/<name>`); the `conceptMeta()` helper declares the key once so a rename-on-review is a single-file change. Draft MCPmed listing PR at `docs/mcpmed-pr-draft.md`.
13
+ - **HTTP landing page + SEP-1649 Server Card** (`src/index.ts`): `createApp({ landing: { ... } })` wires a server-specific tagline, `repoRoot`, four footer links (PubMed, E-utilities docs, NCBI API key signup, MeSH Browser), and `envExample` surfacing `NCBI_API_KEY` / `NCBI_ADMIN_EMAIL` in the STDIO/Claude CLI connect snippets. The page renders at `/` and the Server Card at `/.well-known/mcp.json` — no new env vars required; Cloudflare pass-through already reaches the container for both paths.
14
+ - **Per-tool `sourceUrl` view-source overrides** (all 9 `*.tool.ts` files): Each tool definition now carries an explicit `sourceUrl` pointing at its actual file path. Without the override, the framework's default `snake_case → kebab-case` filename derivation would produce `pubmed-convert-ids.tool.ts` (prefixed) from the tool name `pubmed_convert_ids`, while our actual file is `convert-ids.tool.ts` (unprefixed, since the directory already namespaces). Landing-page per-tool view-source links now resolve.
15
+
16
+ ## Changed
17
+
18
+ - **Framework bump — `@cyanheads/mcp-ts-core` 0.5.3 → 0.6.3**: 0.5.4 added the `api-linter` skill and a diagnostic breadcrumb on every `LintDiagnostic`; 0.6.0 introduced the landing page, SEP-1649 Server Card, `LandingConfig` export, and directory-based changelog system (opt-in per 0.6.2); 0.6.1 added `envExample` and the tabbed terminal-chrome connect card; 0.6.2 softened the directory-based changelog prescription so runtime-only consumer servers can stay monolithic (closes [cyanheads/mcp-ts-core#41](https://github.com/cyanheads/mcp-ts-core/issues/41)) and clarified `changelog/unreleased.md` as a pristine format reference; 0.6.3 exposed `sourceUrl?: string` on `ToolDefinition` / `ResourceDefinition` / `PromptDefinition` (closes [cyanheads/mcp-ts-core#42](https://github.com/cyanheads/mcp-ts-core/issues/42)).
19
+ - **Test runner — `vitest` 4.1.4 → 4.1.5**: patch bump (bug fixes + experimental istanbul instrumenter option). 392 tests pass on 4.1.5 with no changes required.
20
+ - **`CLAUDE.md` skill table**: added `api-linter` (new in 0.5.4) and `add-app-tool` rows; refreshed `maintenance` description.
21
+ - **Project skills synced**: `add-app-tool` 1.2→1.3, `add-prompt` 1.1→1.2, `add-resource` 1.2→1.3, `add-service` 1.2→1.3, `add-tool` 1.6→1.7, `api-context` 1.0→1.1, `api-services` 1.2→1.3, `api-utils` 2.0→2.1, `design-mcp-server` 2.4→2.5, `maintenance` 1.3→1.4, `polish-docs-meta` 1.4→1.6, `setup` 1.3→1.4. New: `api-linter` v1.0.
22
+
23
+ ## Tests
24
+
25
+ - **Live end-to-end verification** against pubmed-mcp-server running in HTTP mode:
26
+ - Landing page (`GET /`) renders identity, tagline, 4 NCBI links, `envExample` keys in all 3 connect-tab panels (STDIO JSON / Claude CLI `--env` / curl), and per-tool view-source URLs resolving to real repo files.
27
+ - Server Card (`GET /.well-known/mcp.json`) returns correct `server_name`, `server_version`, and all three capability flags `true`.
28
+ - `pubmed_spell_check("alzhimer disese")` → both `content[].text` and `structuredContent` carry the corrected query, `hasSuggestion: true`, and original input.
29
+ - `pubmed_search_articles({query: "CRISPR Cas9", maxResults: 3, summaryCount: 2})` → both surfaces populated with 38,563 hits, PMIDs, per-summary fields (authors/doi/pmid/pubDate/pubmedUrl/source/title), and applied filters.
30
+ - Full suite: **392 passed** / 4 skipped / 0 regressions. `bun run devcheck` green across all 8 checks.
31
+
32
+ ## References
33
+
34
+ - Framework issues closed by upstream: [cyanheads/mcp-ts-core#41](https://github.com/cyanheads/mcp-ts-core/issues/41) (soften directory-based changelog prescription — filed to preserve monolithic `CHANGELOG.md` as a valid choice for runtime-only consumer servers), [cyanheads/mcp-ts-core#42](https://github.com/cyanheads/mcp-ts-core/issues/42) (expose `sourceUrl?` on definitions — unblocks per-tool view-source overrides without file renames or type casts).
35
+ - Upstream: `@cyanheads/mcp-ts-core` 0.6.3 `sourceUrl` export.
@@ -0,0 +1,32 @@
1
+ ---
2
+ summary: "End-to-end cancellation."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.5.1 — 2026-04-22
7
+
8
+ End-to-end cancellation. `ctx.signal` from every tool and resource handler now threads through the NCBI service layer into both `fetch()` and the retry-loop backoff sleep, so client cancellations and the new service-level deadline interrupt the *full* retry chain instead of waiting for the next attempt to complete. Adds a `NCBI_TOTAL_DEADLINE_MS` knob (default `60000`) that bounds worst-case tool latency regardless of `NCBI_MAX_RETRIES × backoff`.
9
+
10
+ ## Added
11
+
12
+ - **Service-level deadline + caller-signal abort propagation** (`src/services/ncbi/ncbi-service.ts`, `src/services/ncbi/api-client.ts`, `src/services/ncbi/types.ts`, `src/config/server-config.ts`, `.env.example`):
13
+ - New `runWithDeadline()` wrapper composes an internal `AbortController` (fires at `totalDeadlineMs`) with the caller's `ctx.signal` via `AbortSignal.any()`. The combined signal is threaded into both `apiClient.makeRequest` (cancels the in-flight `fetch`) and the backoff sleep (cancels pending retries).
14
+ - New `abortableSleep()` replaces the bare `setTimeout` in `withRetry()`'s backoff so retry chains short-circuit on abort instead of finishing the current sleep first.
15
+ - New `NcbiCallOptions { signal? }` exported from `types.ts`. Every public `NcbiService` method (`eSearch`, `eSummary`, `eFetch`, `eLink`, `eSpell`, `eInfo`, `eCitMatch`, `idConvert`) now accepts it as its trailing optional arg.
16
+ - New `NCBI_TOTAL_DEADLINE_MS` env var (range `5000`–`600000`, default `60000`). Surfaced in `.env.example`, `README.md` config table, and the Zod schema in `src/config/server-config.ts`. Deadline expiry throws `McpError(JsonRpcErrorCode.Timeout)` with `{ deadlineMs }` data.
17
+ - `NcbiApiClient.makeExternalRequest` (PMC ID Converter — uses `globalThis.fetch` directly, not `fetchWithTimeout`) now composes the per-request `AbortSignal.timeout` with an optional caller signal via `AbortSignal.any()`. `getRequest` / `postRequest` forward an optional `signal` into `fetchWithTimeout` so service-level cancellation reaches the lower-level fetch.
18
+
19
+ ## Changed
20
+
21
+ - **All 9 tools + database-info resource thread `ctx.signal`** through to the NCBI service (`convert-ids`, `fetch-articles`, `fetch-fulltext`, `find-related`, `format-citations`, `lookup-citation`, `lookup-mesh`, `search-articles`, `database-info.resource`). Single-line wiring per call site — `{ signal: ctx.signal }` is forwarded as the new options arg.
22
+ - **`NCBI_TIMEOUT_MS` description clarified** (`.env.example`, `src/config/server-config.ts`, `README.md`): `Request timeout` → `Per-request HTTP timeout` to disambiguate from the new total-deadline knob.
23
+ - **Framework bump — `@cyanheads/mcp-ts-core` 0.6.3 → 0.6.5** (`package.json`, `bun.lock`): patch bumps with no API surface impact on this server.
24
+
25
+ ## Tests
26
+
27
+ - **`api-client.test.ts` (+97 lines)**: 6 new tests covering `signal` forwarding on GET / POST / no-signal paths, plus three `makeExternalRequest` cases (timeout-only signal, `AbortSignal.any()` composition, pre-aborted external signal short-circuit).
28
+ - **`ncbi-service.test.ts` (+277 lines)**: three new describe blocks:
29
+ - **Retry behavior with signals** (4 tests): forwards `signal` to `apiClient.makeRequest`, throws `Timeout` when deadline fires before first attempt, short-circuits the retry chain when caller signal aborts before invocation or mid-flight.
30
+ - **Real-timer signal wiring during backoff sleep** (4 tests): proves the deadline + caller-signal both cut backoff sleeps short (≤500ms vs the 750–1250ms first-attempt window) for both `eSearch` (`makeRequest` path) and `idConvert` (`makeExternalRequest` path).
31
+ - **Deadline timer cleanup** (3 tests): pins the contract that every request — success, non-retryable error, exhausted retries — clears its deadline timer (`setTimeout` ↔ `clearTimeout` parity).
32
+ - **9 tool-test assertion updates** in `convert-ids`, `fetch-articles`, `fetch-fulltext` (3 sites), `find-related` (3 sites), `search-articles` (4 sites) test files — switch from positional args to `expect.objectContaining({ signal: expect.any(AbortSignal) })` to verify each tool wires `ctx.signal` through to the service.
@@ -0,0 +1,23 @@
1
+ ---
2
+ summary: "Framework patch series bump (`@cyanheads/mcp-ts-core` 0.6.5 → 0.6.8) and documentation refresh."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.5.2 — 2026-04-22
7
+
8
+ Framework patch series bump (`@cyanheads/mcp-ts-core` 0.6.5 → 0.6.8) and documentation refresh. Picks up the new `MCP_PUBLIC_URL` override for TLS-terminating reverse-proxy deployments (0.6.6), template hygiene fixes (0.6.7), and landing-page visual polish plus a new CSS-injection lint rule (0.6.8). No API surface impact on this server.
9
+
10
+ ## Added
11
+
12
+ - **`MCP_PUBLIC_URL` env var documentation** (`.env.example`, `README.md`, `server.json`): Surfaces the 0.6.6 override for deployments behind Cloudflare Tunnel / Caddy / nginx / ALB so the landing page, SEP-1649 Server Card, and RFC 9728 protected-resource metadata emit the public `https://` origin instead of the internal container hostname. Declared on the `streamable-http` package entry in `server.json` so MCP registry clients advertise it alongside `MCP_HTTP_HOST`/`MCP_HTTP_PORT`.
13
+ - **`MCP_HTTP_ENDPOINT_PATH` env var documentation** (`.env.example`, `README.md`): Documents the existing framework knob for the HTTP mount path (default `/mcp`).
14
+
15
+ ## Changed
16
+
17
+ - **Framework bump — `@cyanheads/mcp-ts-core` 0.6.5 → 0.6.8** (`package.json`, `bun.lock`): 0.6.6 adds `MCP_PUBLIC_URL` + `design-mcp-server` skill rework (v2.7 codifies the `{server}_{verb}_{noun}` naming default, documents workflow-safety patterns, and diversifies examples beyond email/notifications); 0.6.7 is template hygiene with no consumer impact; 0.6.8 ships landing-page visual polish (auto-derived `--accent-2` secondary token via `oklch` relative color, animated conic-gradient border beam on the connect card, brighter dark-mode surfaces, accent bar prefix on `h2`s) and a new `landing-theme-accent-format` lint rule that rejects CSS-injection payloads in `landing.theme.accent`. Patch series — no breaking changes.
18
+ - **`.dockerignore` adds `.agents`** (`.dockerignore`): Mirrors the 0.6.7 template fix. Keeps agent scratch directories out of the production image, matching the existing `.claude` exclusion.
19
+ - **Project skills synced from 0.6.6** (`skills/`, `.agents/skills/`, `.claude/skills/`): `add-tool` v1.7 → v1.8, `design-mcp-server` v2.5 → v2.7, `field-test` v1.2 → v1.3, `polish-docs-meta` v1.6 → v1.7.
20
+
21
+ ## Tests
22
+
23
+ - Full suite: **409 passed** / 4 skipped / 0 regressions. `bun run devcheck` green across all 8 checks — the new `landing-theme-accent-format` rule is a no-op here since `src/index.ts` doesn't set `landing.theme.accent`.
@@ -0,0 +1,22 @@
1
+ ---
2
+ summary: "Framework patch bump (`@cyanheads/mcp-ts-core` 0.6.8 → 0.6.10) and agent-protocol polish."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.5.3 — 2026-04-23
7
+
8
+ Framework patch bump (`@cyanheads/mcp-ts-core` 0.6.8 → 0.6.10) and agent-protocol polish. 0.6.9 is an internal landing-page refactor with a new CSP header and per-request render memoization; 0.6.10 renames the `release` skill to `release-and-publish` and expands the `setup` skill to cover everything `init` scaffolds. No library API changes — no code edits required.
9
+
10
+ ## Changed
11
+
12
+ - **Framework bump — `@cyanheads/mcp-ts-core` 0.6.8 → 0.6.10** (`package.json`, `bun.lock`): 0.6.9 splits the 1.7kLOC landing-page monolith into `landing-page/` sub-modules, adds a strict `Content-Security-Policy` header to `GET /`, and memoizes both full and degraded render paths when `transport.publicUrl` is set. 0.6.10 renames the shipping skill to `release-and-publish` (v2.0, now `audience: external`) as a post-wrapup publish workflow that runs the verification gate → push → npm → MCP Registry → GHCR, halting on first failure. `setup` skill bumped 1.4 → 1.5. Patch series — no breaking changes and no API surface impact on this server.
13
+ - **Project skills synced from 0.6.10** (`skills/`, `.agents/skills/`, `.claude/skills/`): new `release-and-publish` v2.0 skill added; `setup` v1.4 → v1.5. Skipped internal-audience skills (`add-export`, `add-provider`). Both agent skill directories refreshed end-to-end.
14
+ - **`CLAUDE.md` agent protocol updates**:
15
+ - Skills table now lists `release-and-publish` so agents can discover it when a release is requested.
16
+ - `## Publishing` section rewritten to direct agents at the `release-and-publish` skill as the primary path; the raw `bun publish` + `docker buildx` commands remain as reference and `mcp-publisher publish` added to cover the MCP Registry leg.
17
+ - Checklist expanded with form-client safety (empty inner values on optional nested objects), `format()` completeness (Claude Code reads `structuredContent`, Claude Desktop reads `content[]` — both must carry the same data), and three NCBI-wrapping items (sparsity review on required/optional fields, uncertainty preservation in normalization/format, sparse-payload test coverage).
18
+ - Zod non-serializable type list in the checklist now enumerates the full set (`z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`, `z.function()`, `z.nan()` in addition to the existing entries).
19
+
20
+ ## Tests
21
+
22
+ - Full suite: **409 passed** / 4 skipped / 0 regressions. `bun run devcheck` green across all 8 checks — MCP definition lint, Biome, TypeScript, depcheck, audit, outdated all clean.
@@ -0,0 +1,52 @@
1
+ ---
2
+ summary: "Framework minor bump (`@cyanheads/mcp-ts-core` 0.6.17 → 0.7.0)."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.5.5 — 2026-04-24
7
+
8
+ Framework minor bump (`@cyanheads/mcp-ts-core` 0.6.17 → 0.7.0). Issue-cleanup release from upstream with no runtime breaking changes. Picks up the devcheck changelog-sync crash fix for single-file `CHANGELOG.md` consumers (this server), the flattened ZodError message shape with structured `data.issues`, locale-aware digit-group separators in the `format-parity` linter rule, and new GitHub issue-management scaffolding. Adopts the framework template updates into `CLAUDE.md` / `AGENTS.md`, syncs five skill version bumps, and scaffolds `.github/ISSUE_TEMPLATE/` for bug reports and feature requests.
9
+
10
+ ## Added
11
+
12
+ - **`.github/ISSUE_TEMPLATE/`** (`bug_report.yml`, `feature_request.yml`, `config.yml`): Copied from `node_modules/@cyanheads/mcp-ts-core/templates/.github/ISSUE_TEMPLATE/` (0.7.0 scaffolding). Structured bug report with runtime / transport / framework-version fields, a feature request form, and disabled blank-issue creation. Both forms reference secondary labels (`regression`, `performance`, `security`, `breaking-change`) documented inline; assignees line left commented. Create missing labels on the repo once with `gh label create <name>` if you want the sidebar hint to resolve.
13
+ - **`security-pass` skill reference in agent protocol** (`CLAUDE.md`, `AGENTS.md`): Added as item #8 in the "What's Next?" list and to the Skills table. The skill file itself was already adopted in 2.5.4; this wires it into the template's orientation surface per 0.7.0.
14
+
15
+ ## Changed
16
+
17
+ - **Framework bump — `@cyanheads/mcp-ts-core` 0.6.17 → 0.7.0** (`package.json`, `bun.lock`): Issue-cleanup release, no runtime breaking changes.
18
+ - **Flattened ZodError message shape** (upstream #55): `getErrorMessage(err)` now detects `ZodError` and returns `<first-issue.message> at <path> (+N more)` instead of the raw serialized issue array. Resource param validation, tool output validation, and user-thrown `ZodError` now populate `error.data.issues` with the full `ZodIssue[]`; for tools, issues surface via `_meta.error.data.issues` alongside explicit `McpError.data`. Verified wire-transparent — no code paths in this server's `src/` or `tests/` parse `error.message` JSON, so downstream behavior is unchanged (cleaner logs aside).
19
+ - **Locale-aware `format-parity` linter** (upstream #54): Numeric sentinel matching now retries against text with common digit-group separators stripped (comma, period, underscore, apostrophe, right single quote, space variants including narrow no-break U+202F, Arabic thousands U+066C). Covers en-US, de-DE, fr-FR, de-CH formatting.
20
+ - **Devcheck changelog-sync crash fix** (upstream #51): Guard now checks only for the `changelog/` directory; the monolithic `CHANGELOG.md` alone is a supported configuration. This server uses single-file `CHANGELOG.md`, so the step now skips cleanly (⚪ SKIPPED) instead of crashing on `readdirSync` `ENOENT`. We filed this upstream during the 2.5.4 cycle; it's now resolved in the framework directly.
21
+ - **Phase C script sync — `scripts/devcheck.ts`**: Resynced from `@cyanheads/mcp-ts-core/scripts/devcheck.ts` (only framework script whose content hash differed). Picks up the guard described above. Other framework scripts (`build.ts`, `build-changelog.ts`, `check-docs-sync.ts`, `check-skills-sync.ts`, `clean.ts`, `lint-mcp.ts`, `tree.ts`) were already in sync.
22
+ - **Phase A skill sync — five skill version bumps** (`skills/`, `.agents/skills/`, `.claude/skills/`): `api-linter` 1.0 → 1.1 (recursion-rules table for `describe-on-fields`, primitive array elements explicitly skipped, softened "mechanical fix" framing); `maintenance` 1.4 → 1.5 (Step 4 template review defaults to direct application of framework-authored updates; Step 6 splits into two tiers — framework changes default adopt, third-party changes default cost/benefit; Step 8 renames "Needs attention" → "Open decisions"); `release-and-publish` 2.0 → 2.1 (transient-failure retry protocol for network steps 3–6 with short backoff, idempotent-success skip signals for npm / MCP Registry, `docker builder prune -f` before retrying `buildx --push`); `report-issue-framework` 1.2 → 1.3 and `report-issue-local` 1.2 → 1.3 (primary + secondary label restructure, `--assignee "@me"` CLI examples, `gh label create` bootstrap block in the local skill).
23
+ - **Phase B agent skill refresh** (`.claude/skills/`, `.agents/skills/`): All 25 project skills copied end-to-end into both agent-discovery paths, including the five version bumps from Phase A.
24
+ - **`CLAUDE.md` / `AGENTS.md` template sync**: Skill-directory callout now references the maintenance skill's Phase B auto-resync instead of instructing a manual re-copy — matches the v1.5 maintenance flow.
25
+
26
+
27
+
28
+ Framework patch series bump (`@cyanheads/mcp-ts-core` 0.6.10 → 0.6.17) and a code-cohesion pass. Picks up the new recursive `describe-on-fields` linter (0.6.16) and an HTTP transport per-request `McpServer` race fix (0.6.17). Adds the new `security-pass` skill, syncs the Phase C build/check scripts from the package, and refactors two heavy output schemas into named sub-schemas for readability — verified wire-format-transparent. No library API changes, no tool behavior changes.
29
+
30
+ ## Added
31
+
32
+ - **`security-pass` skill** (`skills/security-pass/`, `.agents/skills/security-pass/`): New v1.1 skill from framework 0.6.14 — systematic audit pass covering secrets, input validation, rate limiting, error surface, and dependency hygiene. Available as first-class skill for post-change security review.
33
+ - **Phase C build/check scripts** (`scripts/build-changelog.ts`, `scripts/check-docs-sync.ts`, `scripts/check-skills-sync.ts`): Copied from `@cyanheads/mcp-ts-core` 0.6.16 as part of the new package → project script sync path. `build-changelog.ts` is invoked by the `devcheck` Changelog Sync step with an added `existsSync(CHANGELOG_DIR)` early-exit to handle single-file `CHANGELOG.md` projects (this server does not use a directory-based changelog). Filed upstream as [cyanheads/mcp-ts-core#51](https://github.com/cyanheads/mcp-ts-core/issues/51).
34
+
35
+ ## Changed
36
+
37
+ - **Framework bump — `@cyanheads/mcp-ts-core` 0.6.10 → 0.6.17** (`package.json`, `bun.lock`): seven patch releases rolled up.
38
+ - **0.6.11–0.6.13**: Template + skill polish (internal-audience), no consumer impact.
39
+ - **0.6.14**: Ships the `security-pass` skill (adopted above).
40
+ - **0.6.15**: Landing-page hardening.
41
+ - **0.6.16**: Definition-linter upgrade — `describe-on-fields` now walks nested object properties, array element schemas, and union variants recursively. Flagged 19 missing `.describe()` calls on inner schemas across this server's 9 tools + 1 resource; all added in this release.
42
+ - **0.6.17**: HTTP transport fix — per-request `McpServer` instantiation resolves a session race where concurrent requests on the same session could see cross-wired tool registrations.
43
+ - **Schema refactor — `fetch-articles.tool.ts`, `fetch-fulltext.tool.ts`**: Extracted deeply nested inline `z.object({...})` output schemas into named module-scoped schemas (`AuthorSchema`, `JournalInfoSchema`, `MeshTermSchema`, `GrantSchema`, `ArticleDateSchema`, `FetchedArticleSchema`, `SubsectionSchema`, `SectionSchema`, `ReferenceSchema`, `PublicationDateSchema`, `FulltextArticleSchema`, etc.). Code-organization change only — verified byte-identical JSON Schema output via `toJSONSchema` from `zod/v4/core`, so MCP SDK's `tools/list` emission and the LLM's view of the tool are unchanged.
44
+ - **`describe-on-fields` compliance** (`convert-ids.tool.ts`, `find-related.tool.ts`, `search-articles.tool.ts`, `lookup-mesh.tool.ts`, `lookup-citation.tool.ts`, `format-citations.tool.ts`, `database-info.resource.ts`): Added `.describe()` on 19 previously unannotated array element schemas and nested object properties flagged by the 0.6.16 recursive linter. Tools' surface descriptions unchanged — this fills in the missing per-field hints for nested structures.
45
+ - **Project skills synced from 0.6.17** (`skills/`, `.agents/skills/`): `security-pass` v1.1 added; `field-test` 1.3 → 2.0 (HTTP + JSON-RPC helper, universal battery vs situational categories); content refreshes at same version on `add-tool`, `design-mcp-server`, `maintenance`, `polish-docs-meta`, `release-and-publish`, `report-issue-framework`, `report-issue-local`, `setup`. Both agent skill directories refreshed end-to-end.
46
+ - **Project scripts synced from 0.6.16** (`scripts/devcheck.ts`, `scripts/lint-mcp.ts`, `scripts/tree.ts`): picked up the new Docs Sync + Changelog Sync + Skills Sync steps in `devcheck.ts` and linter-rule updates in `lint-mcp.ts`.
47
+ - **`AGENTS.md` re-synced from `CLAUDE.md`**: the two were out of sync (2.5.0 vs 2.5.3); the mirror is now re-established.
48
+ - **Biome patch bump — `@biomejs/biome` 2.4.12 → 2.4.13** (`package.json`, `bun.lock`): internal fixes only.
49
+
50
+ ## Tests
51
+
52
+ - Full suite: **409 passed** / 4 skipped / 0 regressions. `bun run devcheck` green across all 11 checks (Docs Sync, Changelog Sync, Skills Sync, and MCP definition lint included). Field-tested all 9 tools via real HTTP + JSON-RPC transport — happy path, `structuredContent` ↔ `content[]` parity, and input-validation error messages verified.
@@ -0,0 +1,33 @@
1
+ ---
2
+ summary: "Correctness + ergonomics pass on `pubmed_lookup_citation`."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.5.6 — 2026-04-24
7
+
8
+ Correctness + ergonomics pass on `pubmed_lookup_citation`. Closes [#35](https://github.com/cyanheads/pubmed-mcp-server/issues/35) (silent wrong-PMID on author mismatch), [#40](https://github.com/cyanheads/pubmed-mcp-server/issues/40) (same class of bug for year), and [#25](https://github.com/cyanheads/pubmed-mcp-server/issues/25) (AMBIGUOUS candidate PMIDs only available as a CSV buried in `detail`). The root cause for #35/#40 is that NCBI's ECitMatch weights journal+volume+page as the primary key and tolerates author/year disagreement, so a query like `{ authorName: "husain m", journal: "lancet", volume: "394", firstPage: "121", year: "2019" }` (intent: PIONEER-6, Husain M, NEJM 2019, with a deliberately wrong journal) was resolving to PMID `31189511` (REWIND, Gerstein HC, 2019) and surfacing `Status: Matched` with no warning.
9
+
10
+ ## Fixed
11
+
12
+ - **`pubmed_lookup_citation` verifies queried author AND year against the matched article** (`src/mcp-server/tools/definitions/lookup-citation.tool.ts`). After ECitMatch resolves, collects all matched PMIDs and issues a single batched `eSummary` call to enrich with author strings and `pubDate`. Author check: normalize lowercase, compare surname-to-surname (first whitespace-separated token on each side) — avoids `Smith`/`Smithson` false positives. Year check: compare queried `year` against the first four chars of the standardized `pubDate`. On either mismatch, keeps the PMID in results ("flag, don't drop") and attaches a structured `warnings: [{ code: 'author_mismatch' | 'year_mismatch', message }]` entry so downstream code can key on `code` without parsing `format()` text. Both warnings stack on the same result when both disagree.
13
+
14
+ ## Added
15
+
16
+ - **`matchedFirstAuthor` in `pubmed_lookup_citation` output** (`src/mcp-server/tools/definitions/lookup-citation.tool.ts`). Populated for matched results when ESummary returns an author list. Provides an eyeball signal even for clean matches (the prior `format()` dropped author info entirely). Rendered as `**First Author:** Lastname FI`.
17
+ - **`candidatePmids: string[]` for AMBIGUOUS matches** ([#25](https://github.com/cyanheads/pubmed-mcp-server/issues/25), `src/services/ncbi/ncbi-service.ts`, `src/services/ncbi/types.ts`). Parsed in the service layer from the raw `AMBIGUOUS <csv>` string that ECitMatch returns in `detail`; `detail` is preserved for human readability. Exposes candidates directly on `ECitMatchResult` so programmatic consumers no longer have to regex-extract from the detail field. `format()` renders them as `**Candidate PMIDs:** 33057196, 32076266, 32025019` with a next-step hint to fetch via `pubmed_fetch_articles` to disambiguate manually.
18
+ - **`warnings` array on per-result output and `totalWarnings` on top-level** (`src/mcp-server/tools/definitions/lookup-citation.tool.ts`). Structured `{ code: 'author_mismatch' | 'year_mismatch', message }[]` flagged at the result level; `totalWarnings` rolls up how many matches carry at least one warning. `format()` renders a `**Warnings:** N` header and a `- [code] …` bullet per finding, and swaps the matched-status next-step from "PMID is ready" to "`<code1> + <code2>` detected — confirm this PMID is the intended article before citing" when present.
19
+
20
+ ## Tests
21
+
22
+ - Ten new tests across `tests/mcp-server/tools/definitions/lookup-citation.tool.test.ts` and `tests/services/ncbi/ncbi-service.test.ts`:
23
+ - Clean match surfaces first author, no warning.
24
+ - The PIONEER-6 / REWIND reproducer from #35 is flagged but not dropped.
25
+ - `Smith` vs `Smithson` does not false-positive (surname vs substring).
26
+ - Missing `authorName` skips verification while still surfacing `matchedFirstAuthor`.
27
+ - `eSummary` is skipped when no matches land.
28
+ - Multiple matched PMIDs batch into a single `eSummary` call.
29
+ - Year mismatch is flagged; year match is not.
30
+ - Author + year mismatches stack on the same result.
31
+ - AMBIGUOUS with CSV PMIDs produces `candidatePmids: string[]`; `eSummary` is not called for ambiguous-only batches.
32
+ - `format()` renders candidate PMIDs and the fetch-articles hint; combined mismatch codes appear in the next-step.
33
+ - Suite: **422 passed** / 4 skipped / 0 regressions. `bun run devcheck` green across all 11 checks.
@@ -0,0 +1,32 @@
1
+ ---
2
+ summary: "Closes [#34](https://github.com/cyanheads/pubmed-mcp-server/issues/34) — non-PMC full-text fallback via Unpaywall."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.6.0 — 2026-04-24
7
+
8
+ Closes [#34](https://github.com/cyanheads/pubmed-mcp-server/issues/34) — non-PMC full-text fallback via Unpaywall. `pubmed_fetch_fulltext` previously returned `{ articles: [], unavailable: [...] }` for every article that wasn't in PubMed Central, even when a legal open-access copy existed at the publisher or in an institutional repository. New behavior: when `UNPAYWALL_EMAIL` is configured, articles without a PMCID are routed through [Unpaywall](https://unpaywall.org/) to resolve the DOI to an OA location, the content is fetched (PDF preferred, HTML fallback), and extracted to the LLM-friendly surface (Markdown for HTML via [Defuddle](https://github.com/kepano/defuddle); text for PDF via [unpdf](https://github.com/unjs/unpdf)). Absent `UNPAYWALL_EMAIL`, the tool's legacy behavior is preserved — unavailable results now carry a structured `reason` code so callers can distinguish "fallback disabled" from "no OA copy exists" from "fetch/parse failure."
9
+
10
+ ## Added
11
+
12
+ - **Unpaywall service** (`src/services/unpaywall/`, new module). `UnpaywallService.resolve(doi)` calls Unpaywall's `/v2/{doi}` endpoint, normalizes `doi:`- and `doi.org/`-prefixed inputs, and returns a discriminated resolution `{ kind: 'found', location } | { kind: 'no-oa', reason }`. 404 and 422 from upstream are treated as structured non-OA (not errors). `UnpaywallService.fetchContent(location)` prefers `url_for_pdf` when present and falls back to `url`, detecting content type from response headers to branch into HTML or PDF extraction. Init/accessor pattern (`initUnpaywallService`, `getUnpaywallService`) — no-op when `UNPAYWALL_EMAIL` is unset, so the fallback is opt-in and downstream code can guard on `getUnpaywallService() !== undefined`.
13
+ - **`source: "unpaywall"` output variant on `pubmed_fetch_fulltext`** (`src/mcp-server/tools/definitions/fetch-fulltext.tool.ts`). Output is now a Zod discriminated union on `source`: the existing PMC variant (structured `sections`, `references`, `journal`, etc.) remains unchanged; the new Unpaywall variant returns `{ pmid, doi, title, body, contentFormat, source, sourceUrl, license, version, hostType }` where `contentFormat` is `'html-markdown' | 'pdf-text'` so the LLM can reason about granularity without guessing from body content. `license`, `version` (submitted/accepted/published), and `hostType` (publisher/repository) pass through unaltered from Unpaywall for provenance.
14
+ - **Structured `unavailable.reason` enum** (`src/mcp-server/tools/definitions/fetch-fulltext.tool.ts`). `unavailable[]` entries now carry `reason: 'no-pmc-fallback-disabled' | 'no-doi' | 'no-oa' | 'fetch-failed' | 'parse-failed' | 'service-error'` alongside optional `detail`. Lets downstream agents decide whether to retry (service-error), suggest configuration (no-pmc-fallback-disabled), or surface to the user (no-oa).
15
+ - **Config — `UNPAYWALL_EMAIL` and `UNPAYWALL_TIMEOUT_MS`** (`src/config/server-config.ts`, `.env.example`, `server.json`). Zod-validated; email uses `z.email()` with empty-string-as-undefined preprocessing so blank `.env` entries don't fail validation. Timeout defaults to 20_000 ms, min 1_000, max 120_000. `server.json` declares `UNPAYWALL_EMAIL` as an optional environment variable for both stdio and HTTP packages so registry consumers see it in client config UIs.
16
+ - **New dependencies** (`package.json`): `defuddle@^0.18.1` + `linkedom@^0.18.12` for HTML → Markdown extraction; `unpdf@^1.6.0` for PDF → text. All three are tree-shakable and Worker-compatible.
17
+
18
+ ## Fixed
19
+
20
+ - **`pubmed_fetch_fulltext` now sources DOI from PubMed metadata when the PMC ID Converter omits it** (`src/mcp-server/tools/definitions/fetch-fulltext.tool.ts`). Root cause: NCBI's PMC ID Converter API only returns DOI for articles already indexed in PMC — non-PMC PMIDs come back as `{ pmid, errmsg: "Identifier not found in PMC" }` with no DOI field. Discovered via live field testing: PMID `30298337` has DOI `10.1200/JCO.2018.79.0691` per `pubmed_fetch_articles`, but `pubmed_fetch_fulltext` returned `no-doi` because idConvert said so. Fix: after idConvert, the handler now batch-fetches missing DOIs from `db=pubmed` via `eFetch`, reusing existing `extractDoi`/`extractPmid` helpers over the `PubmedArticle` XML (ELocationID and ArticleIdList scanned for `EIdType='doi'`). Without this, the Unpaywall fallback would have been effectively unreachable for the exact case it's meant to serve.
21
+
22
+ ## Changed
23
+
24
+ - **Issue templates auto-assign to repo owner** (`.github/ISSUE_TEMPLATE/bug_report.yml`, `.github/ISSUE_TEMPLATE/feature_request.yml`). Switched the `assignees:` key from commented placeholder to `["cyanheads"]` so new bug reports and feature requests route to notifications automatically.
25
+
26
+ ## Tests
27
+
28
+ - **`tests/services/unpaywall/unpaywall-service.test.ts`** (new file, 18 tests). Covers `resolve` (found with `best_oa_location`, found via `oa_locations[0]` fallback, `is_oa: false` → no-oa, 404 → no-oa, 422 → no-oa, 500 → service-unavailable, network failure → service-unavailable, invalid DOI normalization), `fetchContent` (PDF preference, HTML fallback after PDF 500, content-type-driven branching, Uint8Array body for PDF vs string body for HTML, redirect-followed `fetchedUrl`), and the init/accessor pattern (`initUnpaywallService` no-op when env unset, constructs with email/timeout when set).
29
+ - **Rewrote Unpaywall tests in `tests/mcp-server/tools/definitions/fetch-fulltext.tool.test.ts`** with a db-aware `mockEFetchBy({ pmc?, pubmedDois? })` helper that dispatches by `params.db` (`pmc` → full JATS body, `pubmed` → PubmedArticleSet with DOI in ArticleIdList). Previous tests stubbed `idConvert` to return DOI directly, which masked the real-world gap where non-PMC PMIDs come back without DOIs. Includes a named regression test (`"sources the DOI from PubMed metadata when ID Converter omits it (regression for field-test)"`) that reproduces the production bug with an idConvert response lacking the DOI field.
30
+ - **Config test** — `UNPAYWALL_EMAIL` and `UNPAYWALL_TIMEOUT_MS` pickup (`tests/config/server-config.test.ts`).
31
+ - **Index test** — `initUnpaywallService` mocked and asserted called in `setup()` (`tests/index.test.ts`).
32
+ - Suite: **445 passed** / 4 skipped / 0 regressions (up from 422). `bun run devcheck` green across all 11 checks.
@@ -0,0 +1,26 @@
1
+ ---
2
+ summary: "Field-test correctness + DX pass."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.6.1 — 2026-04-24
7
+
8
+ Field-test correctness + DX pass. Closes [#41](https://github.com/cyanheads/pubmed-mcp-server/issues/41) (data-correctness bug — `<sup>`/`<sub>`/`<inf>` content silently dropped from EFetch abstracts), [#42](https://github.com/cyanheads/pubmed-mcp-server/issues/42), [#43](https://github.com/cyanheads/pubmed-mcp-server/issues/43), and [#44](https://github.com/cyanheads/pubmed-mcp-server/issues/44) (DX polish across `find_related`, `convert_ids`, `search_articles`).
9
+
10
+ ## Fixed
11
+
12
+ - **`pubmed_fetch_articles` and `pubmed_format_citations` preserve superscript/subscript content in abstracts** ([#41](https://github.com/cyanheads/pubmed-mcp-server/issues/41), `src/services/ncbi/response-handler.ts`). Root cause: the non-ordered `XMLParser` used for EFetch responses doesn't preserve mixed content — `1.73 m<sup>2</sup>` parses to `{ '#text': '1.73 m', sup: 2 }`, and `extractAbstractText` only reads `#text`, so the superscript digit was silently dropped from titles and abstracts. Discovered via live field testing: PMID 38785209 abstract showed `1.73 mof body-surface area` instead of `1.73 m² of body-surface area`; PMID 38740993 showed `≥40 kg m` instead of `≥40 kg m⁻²`. Fix: pre-flatten `<sup>`/`<sub>`/`<inf>` to Unicode (or `^X`/`_X` ASCII fallback) before fast-xml-parser runs, and strip emphasis tags (`<i>`, `<b>`, `<u>`, `<sc>`) keeping their content. Scoped to the regular parser path; the PMC JATS path already preserves inline markup via `preserveOrder: true` and is left untouched.
13
+
14
+ ## Changed
15
+
16
+ - **`pubmed_find_related` returns a relationship-aware notice when `references` yields zero results for a non-PMC source** ([#42](https://github.com/cyanheads/pubmed-mcp-server/issues/42), `src/mcp-server/tools/definitions/find-related.tool.ts`). NCBI's `pubmed_pubmed_refs` ELink only resolves reference lists for PMC-indexed sources, so a perfectly valid PMID without a PMCID returned an empty payload indistinguishable from "this article cites nothing." On a zero-result `references` query, the handler now does a single ESummary lookup on the source PMID and emits a `notice` field — either "Reference lists require the source article to be indexed in PMC. PMID X has no PMCID …" with recovery steps, or "No reference list found in PMC for PMID X (PMCID Y)." when the source IS in PMC. Added `notice?: string` to the output schema; `format()` renders it as a blockquote in place of the generic "No related articles found." line.
17
+ - **`pubmed_convert_ids` rewrites NCBI's "Identifier not found in PMC" errmsg** ([#43](https://github.com/cyanheads/pubmed-mcp-server/issues/43), `src/mcp-server/tools/definitions/convert-ids.tool.ts`). Upstream wording reads as "this PMID doesn't exist," but the article may still be retrievable via `pubmed_fetch_articles`. The handler now matches `/^identifier not found in pmc$/i` and substitutes `"Not in PMC ID Converter. Article may still exist in PubMed — try pubmed_fetch_articles (PMID → DOI) or pubmed_search_articles."`, preserving the original at debug log level. Other NCBI error messages pass through unchanged.
18
+ - **`pubmed_search_articles` `format()` explains the summary/PMID count split** ([#44](https://github.com/cyanheads/pubmed-mcp-server/issues/44), `src/mcp-server/tools/definitions/search-articles.tool.ts`). When `summaryCount < maxResults` the response correctly carries more PMIDs than summaries, but the rendered output didn't say so. Added a one-line blockquote above the Summaries section: `"Summaries shown for top N of M PMIDs. Increase summaryCount (max 50) to fetch more."` Suppressed when summaries == pmids or when summaries are empty.
19
+
20
+ ## Tests
21
+
22
+ - **Inline markup flattening** (`tests/services/ncbi/response-handler.test.ts`) — 8 new assertions covering `<sup>` digits → Unicode, leading-minus → `⁻²`, `<sub>`/`<inf>` → Unicode, alphabetic fallback to `^X`/`_X`, emphasis-tag stripping, end-to-end AbstractText regression for PMID 38785209, and confirmation that the ordered/PMC parser path is left untouched.
23
+ - **`find_related` references notice** (`tests/mcp-server/tools/definitions/find-related.tool.test.ts`) — 5 new tests covering non-PMC source hint, PMC-source-with-no-refs hint, no-notice for `similar`/`cited_by` paths, graceful degradation when ESummary fails, and `format()` blockquote rendering.
24
+ - **`convert_ids` PMC-not-found rewrite** (`tests/mcp-server/tools/definitions/convert-ids.tool.test.ts`) — 2 new tests confirming the rewrite fires on the matching errmsg and leaves other NCBI errors untouched.
25
+ - **`search_articles` count-split note** (`tests/mcp-server/tools/definitions/search-articles.tool.test.ts`) — 3 new tests covering the partial-summary case, summaries==pmids parity, and empty summaries.
26
+ - Suite: **466 passed** / 4 skipped (up from 449).
@@ -0,0 +1,16 @@
1
+ ---
2
+ summary: "Reclassify NCBI prolog-only XML responses as transient `ServiceUnavailable` so the retry chain recovers, fixing intermittent `pubmed_find_related` (and any eLink) failures caused by upstream TXCLIENT EOFs."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 2.6.10 — 2026-05-10
8
+
9
+ ## Fixed
10
+
11
+ - **`response-handler.ts`** — NCBI occasionally drops the root element when its upstream backend connection fails mid-response, returning just `<?xml ... ?>` + DOCTYPE with no body (the same TXCLIENT EOF surfaced as an `ERROR` field in JSON retmode). The XMLValidator was rejecting these as malformed and the framework was classifying them as `SerializationError` — a non-retryable code — so the retry chain short-circuited on what is in fact a transient outage. Empty bodies (after stripping prolog + DOCTYPE) now throw `serviceUnavailable({ reason: 'ncbi_unreachable' })` and route through the existing retry/backoff path.
12
+ - **`pubmed_find_related`** — drops the unreachable `if (firstResult?.ERROR)` branch and the corresponding `ERROR?: string` field on `ELinkResultItem`. `response-handler` already throws on `<ERROR>` payloads before the parsed body reaches the tool, so the branch was dead. The empty-LinkSet ESummary disambiguation path (invalid PMID vs. valid-but-no-relations) is unchanged.
13
+
14
+ ## Tests
15
+
16
+ - 539 passed / 4 skipped. `bun run devcheck` clean.
@@ -0,0 +1,24 @@
1
+ ---
2
+ summary: "Adopts `@cyanheads/mcp-ts-core` 0.9.0 (Workers under `nodejs_compat`, `instructions` field, portability lint family). Pins `fast-xml-parser` to `~5.7.3` to avoid the 5.8.0 `XMLValidator` deprecation in favor of an unproven sibling package. Server now ships an `instructions` string."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 2.6.11 — 2026-05-13
8
+
9
+ ## Added
10
+
11
+ - **`instructions` on `createApp()`** ([mcp-ts-core#91](https://github.com/cyanheads/mcp-ts-core/issues/91)) — server-level orientation surfaced on every `initialize` response. Echoes the same typical-flow guidance carried in the MCP server instructions block (`pubmed_search_articles` → `pubmed_fetch_articles` → `pubmed_fetch_fulltext`, prefer `pubmed_lookup_citation` for known references, `pubmed_lookup_mesh`/`pubmed_spell_check` for query refinement). Spec-compliant clients forward this to the model as session-level system context.
12
+
13
+ ## Changed
14
+
15
+ - **`@cyanheads/mcp-ts-core` ^0.8.20 → ^0.9.0** ([0.9.0 changelog](https://github.com/cyanheads/mcp-ts-core/blob/main/changelog/0.9.x/0.9.0.md)) — Workers boot under `nodejs_compat`, cross-vendor portability lint family (5 rules), definition linting moves to build-time only, RFC 8414 §3 well-known path-suffix mount, SSRF DNS validation in Workers, tenant-id boundary check in `FileSystemProvider`. All schemas already conformed to the new `schema-format-portability` allowlist — no migration needed.
16
+ - **`fast-xml-parser` ^5.7.3 → ~5.7.3 (pinned)** — 5.8.0 deprecates `XMLValidator` in favor of a brand-new sibling package `fast-xml-validator` (0 dependents, published days before the deprecation by the same author). No security fix or feature we need, so we stay on 5.7.x. Documented in `src/services/ncbi/response-handler.ts` and added to `devcheck.config.json` `outdated.allowlist` so the gate stops flagging it. Re-evaluate during a future `maintenance` pass.
17
+ - **Changelog tooling summary cap raised 250 → 350 chars** (mirrors [mcp-ts-core#129](https://github.com/cyanheads/mcp-ts-core/issues/129)) — `SUMMARY_MAX_LENGTH` in `scripts/build-changelog.ts` and `changelog/template.md`.
18
+ - **`.gitignore`** — ignore `.agents/` (the agent-skill-directory mirror populated by the `maintenance` skill).
19
+ - **Skill bumps mirrored from upstream.** `api-linter` 1.2 → 1.3 (adds portability/landing/handler-body/error-contract rule docs), `polish-docs-meta` 1.7 → 1.8, `design-mcp-server` 2.10 → 2.11, `add-tool` 2.8 → 2.9, `tool-defs-analysis` 1.1 → 1.2 (10 → 12 audit categories), `field-test` 2.3 → 2.4, `api-errors` 1.5 → 1.6.
20
+ - **Dev deps.** `@types/node` ^25.6.2 → ^25.7.0, `@vitest/coverage-istanbul` ^4.1.5 → ^4.1.6, `fast-check` ^4.7.0 → ^4.8.0, `vitest` ^4.1.5 → ^4.1.6.
21
+
22
+ ## Tests
23
+
24
+ - 539 passed / 4 skipped. `bun run devcheck` clean.
@@ -0,0 +1,29 @@
1
+ ---
2
+ summary: "NCBI queue wait now counts toward `totalDeadlineMs`, so a slow upstream can't backlog tools into 10+ min latencies ([#50](https://github.com/cyanheads/pubmed-mcp-server/issues/50)). `pubmed_find_related` similar switches to `cmd=neighbor` to dodge NCBI's unstable scoring backend ([#53](https://github.com/cyanheads/pubmed-mcp-server/issues/53))."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 2.6.12 — 2026-05-16
8
+
9
+ ## Added
10
+
11
+ - **`NCBI_MAX_CONCURRENT` env var** — caps in-flight NCBI requests (default `8`, range 1–16). Decouples concurrency from `NCBI_REQUEST_DELAY_MS` so a slow upstream doesn't block new dispatches behind a single worker. Documented in `.env.example` and the README config table.
12
+
13
+ ## Changed
14
+
15
+ - **`@cyanheads/mcp-ts-core` ^0.9.0 → ^0.9.1.**
16
+ - **`NcbiRequestQueue` rewritten as a token-bucket scheduler** (`src/services/ncbi/request-queue.ts`). Two independent ceilings: `minStartGapMs` enforces NCBI's per-second start-rate cap, `maxConcurrent` caps in-flight requests. `enqueue(task, endpoint, params, signal?)` now accepts an `AbortSignal` and rejects a still-waiting waiter immediately when its signal fires, splicing it out of the queue without dispatching.
17
+ - **`NCBI_REQUEST_DELAY_MS` description** refined in `.env.example` and README — names what it actually does ("minimum gap between request starts") instead of the older "delay between requests".
18
+ - **Dev deps.** `sanitize-html` ^2.17.3 → ^2.17.4, `@types/node` ^25.7.0 → ^25.8.0.
19
+ - **`skills/api-workers/SKILL.md`** 1.3 → 1.4 — documents the `instructions` resolver form `(env: CloudflareBindings) => string` so env-derived orientation text reaches the model inside `initializeApp(env)`, after `injectEnvVars()`.
20
+
21
+ ## Fixed
22
+
23
+ - **Queue wait now counts toward `totalDeadlineMs`** ([#50](https://github.com/cyanheads/pubmed-mcp-server/issues/50)). `NcbiService.performRequest` and `idConvert` previously constructed `runWithDeadline` *inside* the enqueued task, so during NCBI eutils incidents a queued request could sit for minutes before its deadline started ticking — observed `pubmed_fetch_fulltext` and `pubmed_find_related` durations climbing in lockstep to 10+ minutes. `runWithDeadline` now wraps the enqueue, threading the combined deadline+caller signal into queue wait, retry backoff sleeps, and the HTTP fetch.
24
+ - **`pubmed_find_related` similar wedge on high-traffic PMIDs** ([#53](https://github.com/cyanheads/pubmed-mcp-server/issues/53)). NCBI's `cmd=neighbor_score` backend was returning a 200 OK with a `TXCLIENT::readAll EOF` C++ exception trace for popular source PMIDs (1, 12345, 33567185, 37952131), so the retry chain ran for ~60s before surfacing `-32003 ServiceUnavailable`. Switched to `cmd=neighbor` with explicit `linkname=pubmed_pubmed`, which returns the same neighbor list in NCBI's natural relevance order. Output schema drops the `score` field — it was an arbitrary-scale integer, so ordering is preserved without it.
25
+ - **`devcheck` outdated-packages parser** — `bun outdated` emits markdown-style rows (`| col1 | ... |`), so package names live at `split('|')[1]`, not `[0]`. The previous parser was matching the empty leading cell as the package name, so the allowlist always missed and every outdated dep surfaced as unexpected. Also strips the trailing `(dev|peer|prod|optional)` workspace-type marker so allowlist entries match the bare name.
26
+
27
+ ## Tests
28
+
29
+ - 565 passed / 4 skipped. `bun run devcheck` clean. Regression coverage added for the deadline-during-queue-wait path (`tests/services/ncbi/ncbi-service.test.ts`) and the new scheduler's abort-during-wait, concurrency, FIFO, and gap-enforcement behavior (`tests/services/ncbi/request-queue.test.ts`).
@@ -0,0 +1,23 @@
1
+ ---
2
+ summary: Maintenance — `@cyanheads/mcp-ts-core` 0.7.0 → 0.7.5, `fast-xml-parser` 5.7.1 → 5.7.2. No runtime API changes for this server.
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.6.2 — 2026-04-26
7
+
8
+ Drop-in dependency bumps. Field-tested against the live NCBI surface to confirm the v2.6.1 inline-markup flatten path still survives the parser update (PMID 38785209 abstract continues to render `1.73 m²`).
9
+
10
+ ## Changed
11
+
12
+ - **`@cyanheads/mcp-ts-core` 0.7.0 → 0.7.5** — picks up HTTP Origin guard fail-closed default, landing-page bearer validation, raw-payload removal from default request-completion logs, `format-parity` numeric normalization, and a `describe-on-fields` linter exemption for `z.literal` union variants. None of the affected runtime surfaces are used here, so adoption is transparent. MCP SDK pinned at `^1.29.0`.
13
+ - **`fast-xml-parser` 5.7.1 → 5.7.2** — stack-overflow fix on very long tag expressions ([NaturalIntelligence/fast-xml-parser#817](https://github.com/NaturalIntelligence/fast-xml-parser/issues/817)). Marginal hardening for deeply nested PMC full-text articles.
14
+
15
+ ## Synced
16
+
17
+ - **Skills** from the framework: `api-linter` 1.1 → 1.2 (canonical form-client safety pattern), `maintenance` 1.5 → 1.6 (Step 4 surfaces new/changed skills explicitly), content refreshes for `field-test` and `design-mcp-server` (description leak audits). Mirrored into `.agents/skills/` and `.claude/skills/`.
18
+ - **Scripts** from the framework: `scripts/devcheck.ts` (now wires `Framework Antipatterns` as a check step) and the new `scripts/check-framework-antipatterns.ts` (no consumer-side findings).
19
+ - **`CLAUDE.md` / `AGENTS.md`** — form-client safety bullet now names the union+literal alternative for cases where regex/length constraints matter enough to surface in JSON Schema.
20
+
21
+ ## Tests
22
+
23
+ - **466 passed** / 4 skipped. No suite changes — the dependency bumps are drop-in.
@@ -0,0 +1,17 @@
1
+ ---
2
+ summary: Cross-field input violations on `pubmed_fetch_fulltext` and `pubmed_lookup_citation` now classify as `-32602` instead of `-32603` (closes [#46](https://github.com/cyanheads/pubmed-mcp-server/issues/46)).
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.6.3 — 2026-04-26
7
+
8
+ Two tools were enforcing cross-field input constraints with imperative `throw new Error(...)` after schema parsing, so violations leaked as JSON-RPC `-32603 Internal error` (with a level-50 server log) instead of `-32602 Invalid params`. Production telemetry on `fetch_fulltext` showed 13 sessions hitting this in 30 minutes — real LLM callers were inferring incorrect call shapes from the schema.
9
+
10
+ ## Fixed
11
+
12
+ - **`pubmed_fetch_fulltext` — XOR `pmcids` / `pmids` constraint moved into the Zod schema** ([#46](https://github.com/cyanheads/pubmed-mcp-server/issues/46)). The "exactly one of" check now lives in `.refine()` on the input object. Violations surface as `-32602 Input validation error` with the message ``Provide exactly one of `pmcids` or `pmids` (not both, not neither).`` Caveat: Zod `.refine()` doesn't serialize into JSON Schema, so the published `inputSchema` still shows both fields as independently optional — the constraint is communicated through the description prose and the runtime error.
13
+ - **`pubmed_lookup_citation` — same fix.** The "each citation must include at least one bibliographic field (journal, year, volume, firstPage, or authorName)" check moved from a per-item handler loop into a per-item `.refine()`. Bonus: Zod attaches `path: ["citations", N]` so callers learn which array index failed.
14
+
15
+ ## Tests
16
+
17
+ - **462 passed** / 4 skipped. Two tests rewritten to assert on `input.safeParse()` failure rather than handler rejection, matching where validation now runs.
@@ -0,0 +1,21 @@
1
+ ---
2
+ summary: Description audit follow-up to v2.6.3 — cross-field constraints surface in tool descriptions, and `pubmed_format_citations` no longer throws on zero matches.
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.6.4 — 2026-04-26
7
+
8
+ Follow-up to v2.6.3's classification fix. Tool descriptions now declare their cross-field input contracts inline so callers don't learn them from a rejection, and one more imperative throw was lifted into a structured empty result.
9
+
10
+ ## Fixed
11
+
12
+ - **`pubmed_format_citations` no longer throws when EFetch returns no articles** ([#46](https://github.com/cyanheads/pubmed-mcp-server/issues/46) class). Returns `{ citations: [], totalFormatted: 0, totalSubmitted: N, unavailablePmids }` instead of raising `-32603`. `format()` appends a one-line pointer to `pubmed_search_articles` / `pubmed_spell_check` when the result is empty.
13
+
14
+ ## Changed
15
+
16
+ - **Top-level descriptions on `pubmed_fetch_fulltext` and `pubmed_lookup_citation` now state the cross-field contract.** The runtime `.refine()` from v2.6.3 catches violations either way; this surfaces the rule upfront. `fetch_fulltext` declares the `pmcids` / `pmids` XOR; `lookup_citation` requires at least one bibliographic field per citation.
17
+ - **Field-level `.describe()` audit across 5 tools.** Removes implementation leaks (`pubmed_lookup_citation.candidatePmids` no longer mentions "raw `AMBIGUOUS` CSV"), drops meta-coaching ("downstream code should key on…"), and documents under-specified semantics: `pubmed_convert_ids` outputs (`pmid` / `pmcid` / `doi` / `errmsg`) call out the absent-on-failure / presence-as-signal contract; `pubmed_search_articles.dateRange` documents the form-client empty-string-disables pattern; `pubmed_fetch_fulltext.unavailablePmcIds` corrected to cover both direct-request and PMID-resolved failures.
18
+
19
+ ## Tests
20
+
21
+ - **462 passed** / 4 skipped. `format-citations.tool.test.ts`: `throws when no articles found` → `returns structured empty result when no articles match`.