@cyanheads/pubmed-mcp-server 2.10.12 → 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 (118) hide show
  1. package/AGENTS.md +39 -20
  2. package/CLAUDE.md +39 -20
  3. package/README.md +89 -121
  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/config/server-config.d.ts +4 -4
  94. package/dist/config/server-config.d.ts.map +1 -1
  95. package/dist/config/server-config.js +9 -24
  96. package/dist/config/server-config.js.map +1 -1
  97. package/dist/index.js +1 -0
  98. package/dist/index.js.map +1 -1
  99. package/dist/services/europe-pmc/europe-pmc-service.d.ts.map +1 -1
  100. package/dist/services/europe-pmc/europe-pmc-service.js +3 -10
  101. package/dist/services/europe-pmc/europe-pmc-service.js.map +1 -1
  102. package/dist/services/ncbi/ncbi-service.d.ts +0 -2
  103. package/dist/services/ncbi/ncbi-service.d.ts.map +1 -1
  104. package/dist/services/ncbi/ncbi-service.js +9 -13
  105. package/dist/services/ncbi/ncbi-service.js.map +1 -1
  106. package/dist/services/ncbi/response-handler.d.ts +9 -0
  107. package/dist/services/ncbi/response-handler.d.ts.map +1 -1
  108. package/dist/services/ncbi/response-handler.js +33 -1
  109. package/dist/services/ncbi/response-handler.js.map +1 -1
  110. package/dist/services/openalex/openalex-service.d.ts.map +1 -1
  111. package/dist/services/openalex/openalex-service.js +3 -10
  112. package/dist/services/openalex/openalex-service.js.map +1 -1
  113. package/package.json +23 -12
  114. package/server.json +3 -3
  115. package/dist/services/retry-policy.d.ts +0 -18
  116. package/dist/services/retry-policy.d.ts.map +0 -1
  117. package/dist/services/retry-policy.js +0 -21
  118. package/dist/services/retry-policy.js.map +0 -1
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
 
11
11
 
12
- [![Version](https://img.shields.io/badge/Version-2.10.12-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/pubmed-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/pubmed-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/pubmed-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
12
+ [![Version](https://img.shields.io/badge/Version-2.10.14-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/pubmed-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/pubmed-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/pubmed-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
13
13
 
14
14
  </div>
15
15
 
@@ -29,9 +29,11 @@
29
29
 
30
30
  ---
31
31
 
32
- ## Tools
32
+ ## Overview
33
33
 
34
- 11 tools for working with PubMed, PubMed Central, and Europe PMC data:
34
+ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC. Search it, fetch metadata and full text, resolve identifiers and partial citations, format references, and ground queries in MeSH vocabulary. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
35
+
36
+ ### Tools
35
37
 
36
38
  | Tool | Description |
37
39
  |:---|:---|
@@ -42,173 +44,139 @@
42
44
  | `pubmed_fetch_fulltext` | Fetch full-text articles via a chain: NCBI PMC EFetch → Europe PMC `fullTextXML` → Unpaywall. Accepts PMIDs, PMCIDs, or DOIs. |
43
45
  | `pubmed_format_citations` | Generate formatted citations in APA 7th, MLA 9th, BibTeX, RIS, or Vancouver (ICMJE/NLM) |
44
46
  | `pubmed_find_related` | Find similar articles, citing articles, or references for a given PMID |
45
- | `pubmed_spell_check` | Spell-check biomedical queries using NCBI's ESpell service |
46
- | `pubmed_lookup_mesh` | Search and explore MeSH vocabulary — tree numbers, scope notes, entry terms |
47
+ | `pubmed_spell_check` | Spell-check a biomedical query via NCBI ESpell — returns the corrected query and whether a suggestion was found |
48
+ | `pubmed_lookup_mesh` | Search MeSH by heading — tree numbers, scope notes, entry terms — for building controlled-vocabulary queries |
47
49
  | `pubmed_lookup_citation` | Resolve partial bibliographic references to PubMed IDs via ECitMatch |
48
50
  | `pubmed_convert_ids` | Convert between DOI, PMID, and PMCID using the PMC ID Converter API |
49
51
 
50
- ### `pubmed_search_articles`
52
+ ### Resources
51
53
 
52
- Search PubMed with full NCBI query syntax and filters.
54
+ | Resource | Description |
55
+ |:---|:---|
56
+ | `pubmed://database/info` | PubMed database metadata via EInfo (field list, record count, last update) |
53
57
 
54
- - Free-text queries with PubMed's full boolean and field-tag syntax
55
- - Field-specific filters: author, journal, MeSH terms, language, species
56
- - Common filters: has abstract, free full text
57
- - Date range filtering by publication, modification, or Entrez date
58
- - Publication type filtering (Review, Clinical Trial, Meta-Analysis, etc.)
59
- - Sort by relevance, publication date, author, or journal
60
- - Pagination via offset for paging through large result sets
61
- - Optional brief summaries for top N results via ESummary
62
- - NCBI Bookshelf results carry their own venue — `bookTitle`, `publisherName`, and `docType` (`chapter`, `book`, or `citation`) — because PubMed leaves `source` empty on them; the book's editors are reported in `editors`, apart from the chapter's own authors
63
- - Returns the original query plus the fully applied PubMed query and normalized filter metadata
58
+ ### Prompts
64
59
 
65
- ---
60
+ | Prompt | Description |
61
+ |:---|:---|
62
+ | `research_plan` | Generate a structured 4-phase biomedical research plan outline |
66
63
 
67
- ### `pubmed_fetch_articles`
64
+ ## Capability reference
68
65
 
69
- Fetch full article metadata by PubMed IDs.
66
+ ### `pubmed_search_articles` <sub>tool</sub>
70
67
 
71
- - Batch fetch up to 200 articles at once (auto-switches to POST for batches >= 100)
72
- - Returns structured data: title, abstract, authors with deduplicated affiliations, journal info, DOI
73
- - Direct links to PubMed and PubMed Central (when available)
74
- - Optional MeSH terms, grant information, and publication types
75
- - Handles PubMed's inconsistent XML (structured abstracts, missing fields, varying date formats)
76
- - NCBI Bookshelf chapters and whole books are returned as first-class records, not reported unavailable: `recordType` (`journal-article`, `book-chapter`, `book`) tells them apart, and a `book` object carries the book title, publisher, place, dates, medium, edition, series, ISBNs, book DOI, editors, and Bookshelf accession. `journalInfo` is absent on those records — a book title is never reported as a journal
77
- - Journals that assign article numbers instead of page ranges often carry no pagination at all; the number is reported as `journalInfo.elocationId` with its `journalInfo.elocationIdType` (`pii`), never merged into `journalInfo.pages` and never confused with the DOI
78
- - Opt-in whole-response ceiling: `maxResponseCharacters` keeps complete article records in response order until the next one would cross it, then defers the rest whole and lists their PMIDs in `deferred.ids`. Re-call with those PMIDs to resume exactly where the response stopped — no article is split, skipped, or duplicated. Each article is measured as the JSON record it is returned as, so a ceiling under the first article returns zero articles, the full deferred list, and the size to clear
68
+ - Full PubMed boolean and field-tag syntax, plus structured filters: author, journal, MeSH terms, language, species, publication type, has-abstract, free-full-text
69
+ - Date ranges by publication, modification, or Entrez date; sort by relevance, date, author, or journal; offset pagination
70
+ - Optional brief summaries for the top N results via ESummary
71
+ - NCBI Bookshelf hits carry `bookTitle`, `publisherName`, `docType`, and `editors` in place of the empty `source`
72
+ - Echoes the original query, the fully applied PubMed query, and normalized filter metadata
79
73
 
80
74
  ---
81
75
 
82
- ### `pubmed_fetch_fulltext`
83
-
84
- Fetch full-text articles via a three-stage chain: NCBI PMC EFetch → Europe PMC `fullTextXML` → Unpaywall.
85
-
86
- - Accepts exactly one of `pmcids` (direct PMC IDs), `pmids` (PubMed IDs, auto-resolved), or `dois` (auto-resolved to PMC via the ID Converter; preprints and EPMC-only OA fall through to Europe PMC / Unpaywall). One identifier per element in every branch — a DOI carrying a comma or whitespace is rejected at the schema
87
- - NCBI PMC and Europe PMC both return structured JATS; output records origin via `viaSource: "pmc" | "europepmc" | "unpaywall"`
88
- - Europe PMC layer (enabled by default; disable with `EUROPEPMC_ENABLED=false`) recovers PMC-counterpart records that NCBI PMC EFetch missed, and resolves DOI input to PMC counterparts when one exists. EPMC's `fullTextXML` is PMC-keyed, so preprints (PPR), patents (PAT), and Agricola (AGR) are reachable via `pubmed_europepmc_search` for metadata but have no full text via this chain.
89
- - Unpaywall layer (enabled by setting `UNPAYWALL_EMAIL`) resolves DOIs to legal OA copies; extracts HTML landing pages to Markdown via Defuddle or PDFs to text via unpdf
90
- - Discriminated output contract — `source: "pmc"` (structured sections, regardless of whether it came from PMC or EPMC) or `source: "unpaywall"` (best-effort body + `contentFormat`: `html-markdown` or `pdf-text`)
91
- - Structured unavailable reasons (`not-found`, `no-pmc-fallback-disabled`, `no-epmc-fulltext`, `no-body`, `no-doi`, `doi-lookup-failed`, `no-oa`, `fetch-failed`, `parse-failed`, `service-error`) so callers can retry or explain to users without parsing text. `no-doi` and `doi-lookup-failed` are the settled and unsettled halves of the same gap: the first means the DOI lookup ran and the record has none, the second that the lookup itself errored, so a DOI may well exist and the request is worth retrying
92
- - An `unavailable` entry also carries `unqueriedTiers` when the chain skipped a tier this deployment has not configured and that tier could have served the id — the search was incomplete, and a deployment with those tiers configured may still resolve it
93
- - Each `unavailable` entry carries `idType` (`pmid` / `pmcid` / `doi`) and `triedTiers` — per-tier outcomes (`not-attempted`, `miss`, `no-fulltext`, `service-error`, …) in execution order, so callers can see which stage failed and why
94
- - Section filtering by title (case-insensitive substring match at any nesting depth, e.g. `["methods", "results"]`) and configurable max sections apply to PMC output. A section that matches directly is returned whole; one kept only because a nested subsection matched keeps its heading as a breadcrumb with its own text cleared
95
- - Tables are returned as structured cells (`tables[]` on each PMC article — rows, caption, label, footnotes, and the enclosing section, named for back-matter and appendix tables as well as body ones), covering `<floats-group>`, `<back>` and appendix deposits alongside body tables. `colspan` and `rowspan` are expanded to one entry per grid column, so a value stays under the header it belongs to on both output surfaces; a cell spanning several columns or rows repeats across the cells it covers. A deposit with no readable markup comes back labelled with an `unextractableReason` rather than silently missing. Turn them off with `includeTables: false`
96
- - Figures and supplementary material come back as structured entries (`assets[]` on each PMC article — `assetType`, label, caption, the enclosing section, and the `<graphic>`/`<media>` pointer exactly as deposited, which is a name inside the PMC deposit rather than a fetchable URL), covering `<floats-group>`, `<back>` and appendix placements alongside body ones. Each one lifted out of the body leaves a `[Figure: <label>]` / `[Supplementary: <label>]` marker at its position, so reading order survives the lift. Turn them off with `includeAssets: false`, which removes the markers with them. Prose-shaped blocks — lists, definition lists, block quotes, boxed text, preformatted blocks, displayed formulae — render into the section text at their document position instead, and no block is ever concatenated into a neighbouring sentence
97
- - Character budgets keep context size predictable: `maxCharacters` caps body text per article (PMC sections and subsections, inline blocks included, plus table content — cell, caption, label and footnote text, not the Markdown grid rendered around it — and asset label, caption and pointer text; or the Unpaywall body), `maxCharactersPerSection` caps a single PMC section, and `overflowMode` picks between `truncate` (fill sections in document order) and `outline` (split the budget evenly so every heading survives with an excerpt). Sections are served first, then tables, then assets, each spending what is left in document order until one does not fit; that entry and the rest are dropped whole rather than cut mid-row or returned with a shortened caption, and named in `truncation.articles[].omittedTableNames` / `omittedAssetNames`. Budgets run after the semantic filters, and a `truncation` object reports per-article and per-section character counts whenever anything was shortened
98
- - `maxResponseCharacters` bounds the whole response instead of each body: every field of a returned record counts (abstract, references, metadata, body), one ledger across PMC-, Europe PMC-, and Unpaywall-served articles. Articles past the ceiling are deferred whole, with their ids — in the branch they were requested under — in `deferred.ids` for a follow-up call
99
- - Up to 10 articles per request
76
+ ### `pubmed_fetch_articles` <sub>tool</sub>
100
77
 
101
- ---
78
+ - Up to 200 PMIDs per call (POST for batches of 100 or more)
79
+ - Title, abstract, authors with deduplicated affiliations, journal info, DOI, PubMed/PMC links; optional MeSH terms, grants, and publication types
80
+ - Tolerant of PubMed's inconsistent XML — structured abstracts, missing fields, varying date formats
81
+ - Bookshelf chapters and books are first-class: `recordType` (`journal-article` / `book-chapter` / `book`) plus a `book` object (title, publisher, editors, ISBNs, Bookshelf accession); `journalInfo` is absent on them
82
+ - Article-number journals report `journalInfo.elocationId` + `elocationIdType` rather than a page range
83
+ - Opt-in `maxResponseCharacters` keeps whole records in order until the ceiling, then defers the rest to `deferred.ids` for a follow-up call
102
84
 
103
- ### `pubmed_europepmc_search`
85
+ ---
104
86
 
105
- Search Europe PMC (EBI/EMBL-EBI), a broader open-access biomedical corpus than PubMed alone.
87
+ ### `pubmed_fetch_fulltext` <sub>tool</sub>
106
88
 
107
- - Surfaces records PubMed search can't reach — preprints (`source: PPR`), patents (`source: PAT`), Agricola (`source: AGR`), plus everything in PubMed (`MED`) and PMC (`PMC`). On recent queries this can mean dozens of relevant hits with zero PubMed overlap.
108
- - Default sources `["MED", "PMC", "PPR"]`; pass `sources` to include `PAT` / `AGR`
109
- - Cursor-based pagination via `cursorMark` (unlike `pubmed_search_articles`, which uses offset) — `*` for the first page, return `nextCursorMark` for the next
110
- - Output discriminator on `source` plus optional `pmid` / `pmcId` / `doi` cross-walking
111
- - `abstractSnippet` is capped at 400 characters to keep a page bounded; `abstractTruncated` says whether it was cut, and `pubmed_europepmc_fetch` returns the whole abstract for the records worth reading in full
112
- - Disabled when `EUROPEPMC_ENABLED=false`; tool is not registered in that case
89
+ - Exactly one of `pmcids`, `pmids`, or `dois` (one id per element), up to 10 per request
90
+ - Three-tier chain: NCBI PMC EFetch → Europe PMC `fullTextXML` (`EUROPEPMC_ENABLED`, default on) → Unpaywall (needs `UNPAYWALL_EMAIL`); `viaSource` names which tier served each article
91
+ - Preprints, patents, and Agricola records have metadata via `pubmed_europepmc_search` but no full text through this chain — Europe PMC's `fullTextXML` is PMC-keyed
92
+ - `source: "pmc"` returns structured sections plus `tables[]` (cells, caption, label, footnotes) and `assets[]` (figures and supplementary material, with `[Figure: <label>]` markers left in the body); `source: "unpaywall"` returns a best-effort body with `contentFormat` (`html-markdown` / `pdf-text`)
93
+ - Unavailable entries carry a typed `reason` (`not-found`, `no-doi`, `doi-lookup-failed`, `no-oa`, `service-error`, …), `idType`, `triedTiers` (per-tier outcome in execution order), and `unqueriedTiers` when an unconfigured tier could have served the id
94
+ - Filters and budgets: `sections` (case-insensitive title match), `maxSections`, `includeTables`, `includeAssets`, `maxCharacters`, `maxCharactersPerSection`, `overflowMode` (`truncate` / `outline`), and `maxResponseCharacters`, which defers whole articles past the ceiling to `deferred.ids`; a `truncation` object reports what was shortened or omitted
113
95
 
114
96
  ---
115
97
 
116
- ### `pubmed_europepmc_fetch`
117
-
118
- Fetch complete Europe PMC records by `source` + `epmcId`, the detail counterpart to `pubmed_europepmc_search`.
98
+ ### `pubmed_europepmc_search` <sub>tool</sub>
119
99
 
120
- - Returns the full, untruncated abstract as display-ready plain text — markup stripped, HTML entities decoded
121
- - Addressed by the `source` and `epmcId` of a search hit, the only identifier preprint (`PPR`), patent (`PAT`), and Agricola (`AGR`) records reliably carry — `pubmed_fetch_articles` needs a PMID and `pubmed_fetch_fulltext` needs a PMCID, PMID, or DOI
122
- - Up to 25 records per call, resolved in a single Europe PMC request
123
- - Pairs unresolved requests back to the caller in `notFound` instead of failing the batch
124
- - Disabled when `EUROPEPMC_ENABLED=false`; tool is not registered in that case
100
+ - Reaches records PubMed can't: preprints (`PPR`), patents (`PAT`), Agricola (`AGR`), alongside `MED` and `PMC`; default `sources` is `["MED", "PMC", "PPR"]`
101
+ - Cursor pagination via `cursorMark` — `*` for the first page, then `nextCursorMark`
102
+ - Hits carry `source` plus `pmid` / `pmcId` / `doi` when known; `abstractSnippet` is capped at 400 characters, with `abstractTruncated` flagging the cut
103
+ - Not registered when `EUROPEPMC_ENABLED=false`
125
104
 
126
105
  ---
127
106
 
128
- ### `pubmed_format_citations`
129
-
130
- Generate formatted citations for articles.
107
+ ### `pubmed_europepmc_fetch` <sub>tool</sub>
131
108
 
132
- - Five citation styles: APA 7th, MLA 9th, BibTeX, RIS, Vancouver (ICMJE/NLM)
133
- - NCBI Bookshelf chapters and whole books cite in their own form in every style — Vancouver's `In: … editors` contribution pattern, APA's chapter-in-edited-book, MLA's `edited by`, BibTeX `@incollection` / `@book`, RIS `CHAP` / `BOOK` — carrying the book title, editors, publisher, place, ISBNs and Bookshelf URL
134
- - An article with no page range cites by its electronic article locator in each style's own convention — Vancouver's trailing `pii:` note, APA's `Article <n>`, MLA's `art. <n>`, biblatex `eid`, RIS `C7` — rather than dropping it or writing it into a page field
135
- - Request multiple styles per article in a single call
136
- - Hand-rolled formatters — zero external dependencies, fully Workers-compatible
137
- - Up to 50 articles per request
138
- - Reports formatted counts and unavailable PMIDs for partial-result handling
109
+ - Full records with the untruncated plain-text abstract, addressed by `source` + `epmcId` — the only identifier preprint, patent, and Agricola records reliably carry
110
+ - Up to 25 per call in one Europe PMC request; unresolved ids come back in `notFound` rather than failing the batch
111
+ - Not registered when `EUROPEPMC_ENABLED=false`
139
112
 
140
113
  ---
141
114
 
142
- ### `pubmed_find_related`
115
+ ### `pubmed_format_citations` <sub>tool</sub>
143
116
 
144
- Find articles related to a source article via ELink.
145
-
146
- - Three relationship types: `similar` (content similarity), `cited_by`, `references`
147
- - Results enriched with title, authors, publication date, and source via ESummary — or, for an NCBI Bookshelf record, its book title, publisher, and doc type in place of the empty source
148
- - Results returned in NCBI's relevance order
149
- - Falls back to Europe PMC, then OpenAlex, when NCBI cannot answer; the response names which provider served it. A request no provider can answer fails with a typed `all_providers_failed` error instead of an empty result
117
+ - APA 7th, MLA 9th, BibTeX, RIS, Vancouver (ICMJE/NLM); several styles per article in one call, up to 50 articles
118
+ - Bookshelf chapters and books cite in each style's edited-book form; articles without a page range cite by electronic locator in each style's convention
119
+ - Hand-rolled formatters — zero dependencies, Workers-compatible
120
+ - Reports formatted counts and unavailable PMIDs
150
121
 
151
122
  ---
152
123
 
153
- ### `pubmed_spell_check`
154
-
155
- Spell-check a biomedical query using NCBI's ESpell.
124
+ ### `pubmed_find_related` <sub>tool</sub>
156
125
 
157
- - Returns the original query, corrected query, and whether a suggestion was found
158
- - Useful for query refinement before searching
126
+ - `similar`, `cited_by`, or `references` for a PMID, in NCBI relevance order, enriched with title, authors, date, and source (or Bookshelf book title and publisher)
127
+ - Falls back to Europe PMC, then OpenAlex, when NCBI can't answer; the response names the provider. Fails with a typed `all_providers_failed` error rather than an empty result
159
128
 
160
129
  ---
161
130
 
162
- ### `pubmed_lookup_mesh`
131
+ ### `pubmed_spell_check` <sub>tool</sub>
163
132
 
164
- Search and explore the MeSH (Medical Subject Headings) vocabulary.
133
+ - Runs a query through NCBI ESpell and returns `original`, `corrected`, and `hasSuggestion`
134
+ - A blank or whitespace-only query is rejected rather than sent upstream
165
135
 
166
- - Search MeSH terms by name with exact-heading matching
167
- - Detailed records with tree numbers, scope notes, and entry terms by default
168
- - Useful for building precise PubMed queries with controlled vocabulary
136
+ ---
137
+
138
+ ### `pubmed_lookup_mesh` <sub>tool</sub>
139
+
140
+ - Looks up MeSH descriptors by name or free-text term, pinning the exact-heading match to the top of the first page
141
+ - Records carry `meshId` (DescriptorUI), `entrezUid`, and, with `includeDetails` (default on), tree numbers, scope notes, and entry terms
142
+ - `maxResults` up to 50 with offset pagination via `nextOffset`; `totalCount` reports the upstream match count
169
143
 
170
144
  ---
171
145
 
172
- ### `pubmed_lookup_citation`
146
+ ### `pubmed_lookup_citation` <sub>tool</sub>
173
147
 
174
- Resolve partial bibliographic references to PubMed IDs via NCBI ECitMatch.
148
+ - Match on journal, year, volume, first page, and/or author — at least one field, more fields for better precision; up to 25 per call
149
+ - Pipes and line breaks are rejected at the schema (ECitMatch's wire format is pipe-delimited); the free-form `key` label is exempt
150
+ - Explicit `matched`, `not_found`, and `ambiguous` statuses with recovery detail
151
+
152
+ ---
175
153
 
176
- - Match citations by journal, year, volume, first page, and/or author name
177
- - More fields = better match accuracy; at least one field required
178
- - Bibliographic fields cannot contain a pipe (`|`) or a line break — ECitMatch's wire format is pipe-delimited, so those characters are rejected at the schema; the free-form `key` label is exempt
179
- - Batch up to 25 citations per request
180
- - Deterministic matching — more reliable than free-text search for known references
181
- - Returns explicit `matched`, `not_found`, and `ambiguous` statuses with recovery detail
154
+ ### `pubmed_convert_ids` <sub>tool</sub>
155
+
156
+ - Up to 50 DOIs, PMIDs, or PMCIDs per call, all one type; only PMC-indexed articles resolve
157
+ - One id per element — a packed `"23193287,37952131"` is rejected rather than expanded
158
+ - Per-id success/error rows; a partial batch never fails as a whole
182
159
 
183
160
  ---
184
161
 
185
- ### `pubmed_convert_ids`
162
+ ### `pubmed://database/info` <sub>resource</sub>
186
163
 
187
- Convert between article identifiers (DOI, PMID, PMCID) using the PMC ID Converter API.
164
+ - Live EInfo call for the `pubmed` database, returned as `application/json`
165
+ - `dbName`, `description`, `count`, `lastUpdate`, and `fields[]` — each field's short `name` (the tag usable in `pubmed_search_articles` queries), `fullName`, and `description`
166
+ - No parameters
188
167
 
189
- - Batch up to 50 IDs per request
190
- - Accepts DOIs, PMIDs, or PMCIDs (all IDs must be the same type)
191
- - One identifier per array element, checked against `idType` before the request — a packed value like `"23193287,37952131"` is rejected rather than expanded into extra records, since a comma is the converter's list delimiter in any encoding
192
- - Only resolves articles indexed in PubMed Central
193
- - Per-ID success/error reporting — partial batches return resolved mappings alongside structured errors for unresolvable IDs, not a batch-level failure
168
+ ---
194
169
 
195
- ## Resource and prompt
170
+ ### `research_plan` <sub>prompt</sub>
196
171
 
197
- | Type | Name | Description |
198
- |:---|:---|:---|
199
- | Resource | `pubmed://database/info` | PubMed database metadata via EInfo (field list, record count, last update) |
200
- | Prompt | `research_plan` | Generate a structured 4-phase biomedical research plan outline |
172
+ - Arguments: `title`, `goal`, `keywords` (comma-separated) required; `organism` and `includeAgentPrompts` (`"true"` / `"false"`) optional
173
+ - Returns two messages: an assistant framing message (biomedical research planning assistant, grounds recommendations in the PubMed tools when available) and a user message carrying the plan
174
+ - The plan walks four phases — Conception & Planning, Data Collection & Processing, Analysis & Interpretation, Dissemination — with sub-steps under each
175
+ - `includeAgentPrompts: "true"` adds an agent-guidance block under each sub-step, several of which point at `pubmed_search_articles` and `pubmed_lookup_mesh`
201
176
 
202
177
  ## Features
203
178
 
204
- Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core):
205
-
206
- - Declarative tool definitions — single file per tool, framework handles registration and validation
207
- - Unified error handling across all tools
208
- - Pluggable auth (`none`, `jwt`, `oauth`)
209
- - Swappable storage backends: `in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`
210
- - Structured logging with optional OpenTelemetry tracing
211
- - Runs locally (stdio/HTTP) or on Cloudflare Workers from the same codebase
179
+ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
212
180
 
213
181
  PubMed-specific:
214
182
 
@@ -303,7 +271,7 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
303
271
 
304
272
  ### Prerequisites
305
273
 
306
- - [Bun v1.3.2](https://bun.sh/) or higher.
274
+ - [Bun v1.4.0](https://bun.sh/) or higher.
307
275
  - Optional: [NCBI API key](https://www.ncbi.nlm.nih.gov/account/settings/) for higher rate limits (10 req/s vs 3 req/s).
308
276
 
309
277
  ### Installation
@@ -328,7 +296,7 @@ bun install
328
296
 
329
297
  ## Configuration
330
298
 
331
- All configuration is validated at startup via Zod schemas in `src/config/server-config.ts`. Key environment variables:
299
+ Key environment variables:
332
300
 
333
301
  | Variable | Description | Default |
334
302
  |:---|:---|:---|
@@ -403,7 +371,7 @@ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rule
403
371
 
404
372
  ## Contributing
405
373
 
406
- Issues and pull requests are welcome. Run checks and tests before submitting:
374
+ Issues are welcome. Run checks and tests before submitting:
407
375
 
408
376
  ```sh
409
377
  bun run devcheck
@@ -0,0 +1,32 @@
1
+ ---
2
+ summary: "Initial release — 7 PubMed tools, NCBI E-utilities service layer (eSearch/eSummary/eFetch/eLink/eSpell/eInfo with rate-limit + retry), `research_plan` prompt, and `pubmed://database/info` resource."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.0.0 — 2026-03-04
7
+
8
+ ## Added
9
+
10
+ - **NCBI Service Layer**: Complete E-utilities integration (`eSearch`, `eSummary`, `eFetch`, `eLink`, `eSpell`, `eInfo`) with request queuing, rate limiting, retry with exponential backoff, and XML parsing.
11
+ - **7 PubMed Tools**:
12
+ - `pubmed_search` — Search PubMed with filters, date ranges, and optional summaries
13
+ - `pubmed_fetch` — Fetch full article metadata by PMIDs (abstract, authors, journal, MeSH)
14
+ - `pubmed_cite` — Generate formatted citations (APA 7th, MLA 9th, BibTeX, RIS)
15
+ - `pubmed_related` — Find related/cited-by/references via ELink
16
+ - `pubmed_spell` — Spell-check biomedical queries via ESpell
17
+ - `pubmed_trending` — Date-filtered search for recent publications
18
+ - `pubmed_mesh_lookup` — MeSH vocabulary search and exploration
19
+ - **Research Plan Prompt**: `research_plan` — structured 4-phase biomedical research plan generation
20
+ - **Database Info Resource**: `pubmed://database/info` — PubMed database metadata via EInfo
21
+ - **Citation Formatters**: Hand-rolled, zero-dependency, Workers-compatible formatters for APA, MLA, BibTeX, and RIS
22
+ - **NCBI Configuration**: `NCBI_API_KEY`, `NCBI_ADMIN_EMAIL`, `NCBI_REQUEST_DELAY_MS`, `NCBI_MAX_RETRIES`, `NCBI_TIMEOUT_MS`
23
+
24
+ ## Changed
25
+
26
+ - **Rebranded** from `mcp-ts-template` to `@cyanheads/pubmed-mcp-server` (package.json, server.json, smithery.yaml, wrangler.toml)
27
+ - **Architecture**: Built on mcp-ts-template 3.0 with DI container, typed tokens, Zod-validated config, OpenTelemetry, and multi-transport support (stdio, HTTP, Cloudflare Workers)
28
+
29
+ ## Removed
30
+
31
+ - All template example tools, resources, prompts, and services (graph, LLM, speech)
32
+ - `openai`, `@modelcontextprotocol/ext-apps` dependencies
@@ -0,0 +1,32 @@
1
+ ---
2
+ summary: "`pubmed_search` gains field filters, offset pagination, and PMC URLs; `pubmed_mesh_lookup` exact-heading sort; `pubmed_trending` removed; six tool and config defaults revised."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.0.1 — 2026-03-04
7
+
8
+ ## Added
9
+
10
+ - **Search filters**: `pubmed_search` gained field-specific filters (`author`, `journal`, `meshTerms`, `language`, `hasAbstract`, `freeFullText`, `species`) and pagination via `offset`
11
+ - **PMC links**: `pubmed_fetch` and `pubmed_search` summaries now include `pmcId`, `pubmedUrl`, and `pmcUrl` for direct article access
12
+ - **Affiliation deduplication**: article parser collects affiliations into a single array with per-author index references, reducing payload size for multi-center papers
13
+ - **Exact MeSH heading search**: `pubmed_mesh_lookup` runs a parallel `[MH]` exact-heading search and stable-sorts exact matches to the top
14
+
15
+ ## Changed
16
+
17
+ - **`pubmed_search`**: renamed `includeSummaries` to `summaryCount`; date range format changed from `YYYY/MM/DD` to `YYYY-MM-DD` (auto-converted internally)
18
+ - **`pubmed_cite`**: max PMIDs raised from 20 to 50
19
+ - **`pubmed_related`**: simplified to use `cmd=neighbor` for all relationship types instead of `neighbor_history` + WebEnv for cited_by/references
20
+ - **`pubmed_mesh_lookup`**: `includeDetails` now defaults to `true`; switched from eFetch to eSummary for detail retrieval (MeSH eFetch returns plain text, not XML)
21
+ - **NCBI response handler**: demoted `eSearchResult.ErrorList` (PhraseNotFound, FieldNotFound) from errors to warnings — NCBI populates these on valid zero-result queries; enabled `processEntities` and `htmlEntities` in XML parser
22
+ - **Config defaults**: HTTP port 3010 → 3017, transport default `http` → `stdio`, storage default `filesystem` → `in-memory`
23
+
24
+ ## Fixed
25
+
26
+ - **Auth factory tests**: JWT strategy tests now provide `mcpAuthSecretKey` and restore it on teardown
27
+ - **Response handler tests**: updated assertions to match ErrorList demotion (PhraseNotFound is a warning, not a thrown error)
28
+ - **Conformance tests**: removed `pubmed_trending` from expected tools list
29
+
30
+ ## Removed
31
+
32
+ - **`pubmed_trending` tool**: removed — its functionality is fully covered by `pubmed_search` with date range and `pub_date` sort
@@ -0,0 +1,17 @@
1
+ ---
2
+ summary: "Adds `pubmed_pmc_fetch` tool — fetch full-text articles from PubMed Central via NCBI EFetch, with a JATS XML parser returning structured body sections, metadata, and references."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.1.0 — 2026-03-04
7
+
8
+ ## Added
9
+
10
+ - **`pubmed_pmc_fetch` tool**: Fetch full-text articles from PubMed Central (PMC) via NCBI EFetch with `db=pmc`. Accepts PMC IDs directly or PubMed IDs (auto-resolved to PMCIDs via ELink). Returns structured body sections, subsections, metadata, and optional references parsed from JATS XML.
11
+ - **PMC article parser**: JATS XML parser (`pmc-article-parser.ts`) extracts metadata (authors, affiliations, journal, keywords, publication date, abstract), recursive body sections, and back-matter references from PMC EFetch responses.
12
+ - **PMC types**: JATS XML element types and parsed PMC result types (`XmlJatsArticle`, `ParsedPmcArticle`, etc.) in `src/services/ncbi/types.ts`.
13
+
14
+ ## Changed
15
+
16
+ - **NCBI response handler**: Added PMC JATS-specific jpaths (`pmc-articleset.article`, `contrib-group.contrib`, `body.sec`, `ref-list.ref`, etc.) to the `isArray` set for consistent XML parsing.
17
+ - **README**: Added `pubmed_pmc_fetch` tool documentation, updated server description to mention full-text fetch.
@@ -0,0 +1,29 @@
1
+ ---
2
+ summary: "Bug fixes across response handler, PMC/article parsers, and citation formatter — plus comprehensive test coverage for NCBI service and parser edge cases."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.1.1 — 2026-03-04
7
+
8
+ ## Fixed
9
+
10
+ - **Response handler**: `extractTextValues` now handles numeric and boolean primitives emitted by fast-xml-parser when `parseTagValue` is enabled
11
+ - **Response handler**: Error detection uses shared `ERROR_PATHS` constant to stay in sync with error message extraction
12
+ - **PMC article parser**: Empty PMCID no longer produces a bare "PMC" prefix — returns empty string instead
13
+ - **Article parser**: Eliminated redundant `getText()` calls for month, day, and medlineDate in `extractJournalInfo`
14
+ - **Citation formatter**: `formatAuthorApa` no longer produces "undefined." when firstName contains consecutive spaces
15
+ - **Citation formatter**: Reordered `formatAuthorApa` logic so authors with only initials (no lastName) return formatted initials instead of empty string
16
+
17
+ ## Changed
18
+
19
+ - **Citation formatter**: `escapeBibtex` refactored from chained `.replace()` calls to a single regex with switch — fixes ordering bug where backslash-then-brace sequences were double-escaped
20
+ - **Citation formatter**: `splitPages` simplified with destructuring
21
+
22
+ ## Added
23
+
24
+ - Comprehensive test coverage for NCBI service edge cases: eSearch non-numeric fields, eSpell fallbacks, eSummary retmode logic, eFetch POST behavior
25
+ - Response handler tests: `CannotRetrievePMID` error path, numeric error values, DOCTYPE stripping, `returnRawXml` error passthrough
26
+ - Citation formatter tests: BibTeX special character escaping, APA author formatting edge cases, author-count boundaries (1/3/20/21), page splitting with en-dash/em-dash, minimal article formatting
27
+ - Article parser tests: PMC ID extraction from `ArticleIdList`, ORCID extraction, ISSN type classification, MedlineDate without year, empty AffiliationInfo handling
28
+ - ESummary parser tests: nested Author objects, string authors, PMC ID from ArticleIds, FullJournalName fallback
29
+ - PMC article parser tests: `pmc-uid` fallback, empty PMCID, affiliations, page ranges, pub-date priority (epub > ppub > pub)
@@ -0,0 +1,18 @@
1
+ ---
2
+ summary: "`pmc_fetch` renamed to `pubmed_pmc_fetch`; log directory path resolution switched to `node:path` for cross-platform correctness ([#9](https://github.com/cyanheads/pubmed-mcp-server/pull/9))."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.1.2 — 2026-03-04
7
+
8
+ ## Changed
9
+
10
+ - **Tool rename**: `pmc_fetch` renamed to `pubmed_pmc_fetch` for consistency with the `pubmed_*` naming convention across all tools
11
+
12
+ ## Fixed
13
+
14
+ - **Config**: Path resolution for logs directory now uses `node:path` utilities (`dirname`, `join`, `isAbsolute`) instead of URL-based arithmetic for cross-platform correctness ([#9](https://github.com/cyanheads/pubmed-mcp-server/pull/9))
15
+
16
+ ## Updated
17
+
18
+ - `@cloudflare/workers-types` to `4.20260305.1`
@@ -0,0 +1,10 @@
1
+ ---
2
+ summary: "Fix: OpenTelemetry NodeSDK now initializes on Bun — `isBun` guard removed, manual spans, custom metrics, and OTLP export all work correctly."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.1.3 — 2026-03-04
7
+
8
+ ## Fixed
9
+
10
+ - **Telemetry**: Enable OpenTelemetry NodeSDK on Bun — the `isBun` guard was unnecessarily blocking initialization when manual spans, custom metrics, and OTLP export all work correctly
@@ -0,0 +1,12 @@
1
+ ---
2
+ summary: "`pubmed_fetch` gains `affiliations` and `articleDates` fields; public hosted endpoint added to README; new output-schema coverage tests prevent strict-client rejections."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.1.4 — 2026-03-04
7
+
8
+ ## Added
9
+
10
+ - **pubmed_fetch**: `affiliations` (deduplicated author affiliations) and `articleDates` (electronic publication, received, accepted dates) now included in article output
11
+ - **Public hosted instance**: Added public Streamable HTTP endpoint (`https://pubmed.caseyjhand.com/mcp`) to README — no installation required
12
+ - **Output schema coverage tests**: New test suite validates that tool output schemas cover every field returned by parsers at runtime, preventing strict-client rejections from `additionalProperties: false`
@@ -0,0 +1,18 @@
1
+ ---
2
+ summary: "NCBI config now logged at startup (API key status, delay, retries, timeout). Dep bumps: `@biomejs/biome` 2.4.6, `jose` 6.2.0, `@types/node` 25.3.5."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.1.5 — 2026-03-06
7
+
8
+ ## Added
9
+
10
+ - **Startup logging**: NCBI configuration (API key status, email, request delay, max retries, timeout) now logged at initialization for easier debugging
11
+
12
+ ## Updated
13
+
14
+ - `@biomejs/biome` to 2.4.6
15
+ - `@cloudflare/workers-types` to 4.20260307.1
16
+ - `@types/node` to 25.3.5
17
+ - `@types/sanitize-html` to 2.16.1
18
+ - `jose` to 6.2.0
@@ -0,0 +1,15 @@
1
+ ---
2
+ summary: "Fix: `structuredContent` removed from error responses (valid for success only); `fast-check` → 4.6.0, `jose` → 6.2.1."
3
+ breaking: false
4
+ ---
5
+
6
+ # 2.1.6 — 2026-03-09
7
+
8
+ ## Fixed
9
+
10
+ - **Error responses**: Removed `structuredContent` from error responses in tool handler factory — `structuredContent` is only valid for successful results, not error payloads
11
+
12
+ ## Updated
13
+
14
+ - `fast-check` to 4.6.0
15
+ - `jose` to 6.2.1
@@ -0,0 +1,17 @@
1
+ ---
2
+ summary: "New pubmed_europepmc_fetch tool (11th tool) resolves full Europe PMC records by source + epmcId; pubmed_fetch_fulltext gains maxCharacters/maxCharactersPerSection/overflowMode budget controls; a shared surrogate-pair-safe slice helper backs both."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 2.10.0 — 2026-07-26
8
+
9
+ ## Added
10
+
11
+ - **`pubmed_europepmc_fetch` tool** — new 11th tool. Resolves specific Europe PMC records by `source` + `epmcId` and returns each one's complete, untruncated abstract, up to 25 records per call resolved in a single Europe PMC request; unresolved pairs come back in `notFound` instead of failing the batch. This is the retrieval path for preprint (`PPR`), patent (`PAT`), and Agricola (`AGR`) records, which frequently carry no PMID and no DOI. Backed by a new `EuropePmcService.fetchRecords()` method that OR-joins `(EXT_ID:<id> AND SRC:<source>)` clauses into one search query. ([#83](https://github.com/cyanheads/pubmed-mcp-server/issues/83))
12
+ - **`pubmed_fetch_fulltext` character budgets** — new `maxCharacters`, `maxCharactersPerSection`, and `overflowMode` (`truncate` | `outline`) inputs cap body text per article. `truncate` fills sections in document order so early sections stay whole; `outline` spreads the budget evenly across sections so every heading survives with an excerpt. Applies to `source=pmc` section/subsection text and the `source=unpaywall` body; titles, abstracts, identifiers, and references are never counted or cut. A new `truncation` output object reports per-article and per-section character counts whenever a budget shortened the response, and a matching `ctx.enrich.notice()` names what was spent. ([#81](https://github.com/cyanheads/pubmed-mcp-server/issues/81))
13
+ - **`pubmed_europepmc_search` `abstractTruncated`** — new boolean alongside `abstractSnippet` (capped at 400 characters) marking whether the snippet was cut; pass the hit's `source` and `epmcId` to `pubmed_europepmc_fetch` for the full text. ([#83](https://github.com/cyanheads/pubmed-mcp-server/issues/83))
14
+
15
+ ## Fixed
16
+
17
+ - **Character cuts could split a UTF-16 surrogate pair** — `pubmed_fetch_fulltext`'s new budget cuts and `pubmed_europepmc_search`'s `abstractSnippet` cut now back off one code unit when the boundary lands mid-pair, via a new shared `sliceCodeUnits` helper (`src/mcp-server/tools/definitions/_text.ts`). Budgets remain a code-unit ceiling; reported character counts are measured off the text actually returned, never assumed from the requested allowance. ([#93](https://github.com/cyanheads/pubmed-mcp-server/issues/93))
@@ -0,0 +1,14 @@
1
+ ---
2
+ summary: "eSearch upstream failures now throw instead of masking as zero-hit searches; pubmed_search_articles surfaces ignored field tags, unmatched phrases, and dropped partial dateRange filters via notice; the summaryCount cap message points at pubmed_fetch_articles when maxed; research_plan's includeAgentPrompts is now correctly advertised as optional."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 2.10.1 — 2026-07-26
8
+
9
+ ## Fixed
10
+
11
+ - **`eSearchResult.ERROR` was missing from `ERROR_PATHS`** — an upstream eSearch failure parsed as a zero-hit success instead of throwing. `pubmed_search_articles`'s `offset` is now bounded at 9998 (`OFFSET_MAX`), matching PubMed's `retstart` ceiling, and the ceiling is stated in its `.describe()`. `pubmed_lookup_mesh`'s `offset` is left unbounded — `db=mesh` has no equivalent `retstart` ceiling. ([#95](https://github.com/cyanheads/pubmed-mcp-server/issues/95))
12
+ - **eSearch `ErrorList`/`WarningList` never reached the caller** — `pubmed_search_articles` now reads both off the eSearch result and surfaces them through the `notice` enrichment: an unrecognized field tag fires independently of hit count, and an unmatched phrase names the exact clause instead of the generic empty-result guidance. `NcbiService.eSearch` normalizes every list member to `string[]` — NCBI collapses a single-entry member to a scalar, which the declared type didn't account for. ([#96](https://github.com/cyanheads/pubmed-mcp-server/issues/96))
13
+ - **Partial `dateRange` and the `summaryCount` cap were both undisclosed** — a `dateRange` with exactly one bound filled now fires a notice naming the supplied bound and a sentinel for an open-ended range, instead of silently dropping the filter. The `summaryCount` cap message now points at `pubmed_fetch_articles` with the remaining PMIDs when already at its maximum, instead of advising a raise that isn't possible. `buildNotice()` now collects every applicable signal and joins them, rather than returning the first of two mutually exclusive branches. ([#97](https://github.com/cyanheads/pubmed-mcp-server/issues/97))
14
+ - **`research_plan`'s `includeAgentPrompts` was advertised as a required prompt argument** — the SDK derives `prompts/list`'s `required` flag from schema optionality, and a `ZodDefault` doesn't read as optional. Changed from `.default('false')` to `.optional()`; `buildPlan`'s `=== 'true'` check behaves identically for an omitted value. ([#98](https://github.com/cyanheads/pubmed-mcp-server/issues/98))
@@ -0,0 +1,22 @@
1
+ ---
2
+ summary: "pubmed_fetch_fulltext renders JATS block content at its own position and returns figures and supplementary material as a structured assets[] field, pubmed_find_related's OpenAlex fallback pages to the full requested window, and the query tools reject a blank query instead of forwarding it to NCBI."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 2.10.10 — 2026-09-10
8
+
9
+ ## Added
10
+
11
+ - **`pubmed_fetch_fulltext` `assets[]`** — every `<fig>` and `<supplementary-material>` a PMC article carries, with `assetType`, `label`, `caption`, `id`, `sectionTitle`, and the deposit-relative `href` pointer; each lifted asset leaves a `[Figure: <label>]` / `[Supplementary: <label>]` marker at its position. New `includeAssets` input (default `true`) mirrors `includeTables`. ([#130](https://github.com/cyanheads/pubmed-mcp-server/issues/130))
12
+ - **`pubmed_find_related` `coverageFailures`** — when the Europe PMC or OpenAlex reference-coverage fallback throws, an entry naming `provider`, `reason`, and `retryable` is added instead of folding the failure into a false "no references" answer. ([#118](https://github.com/cyanheads/pubmed-mcp-server/issues/118))
13
+
14
+ ## Fixed
15
+
16
+ - **JATS block content (lists, definition lists, quotes, boxed text, preformatted text, formulae) dropped or fused into surrounding prose** — each now renders inline at its position instead of vanishing or gluing onto a paragraph's sentence, and an article whose entire body is one such block (e.g. a legacy OCR `<preformat>` deposit) is no longer reported as having no body. A caption's own title no longer runs into the sentence that follows it — on tables as well as on figures and supplementary material — and a caption or label hung directly on a `<media>`/`<graphic>` pointer is now read. ([#130](https://github.com/cyanheads/pubmed-mcp-server/issues/130))
17
+ - **`pubmed_fetch_fulltext` returned the first serialized `<abstract>` regardless of `@abstract-type`** — the untyped abstract is now preferred over a typed one (e.g. `graphical`, `executive-summary`), and content within the selected abstract renders through the same block-aware walk as body text, so an embedded figure caption reaches `assets[]` instead of the prose. ([#134](https://github.com/cyanheads/pubmed-mcp-server/issues/134))
18
+ - **`pubmed_find_related`'s OpenAlex fallback stalled after its first upstream page** — `cited_by`, `references`, and `similar` now page (or batch-resolve, for the latter two) until the requested window is filled, upstream is exhausted, or a 10-page/batch cap is reached, with `totalCount` reporting the exact PubMed-addressable count once exhausted. This also fixes an HTTP 400 the `references` path threw once `offset + maxResults` reached 34, from a candidate filter of up to 200 values against OpenAlex's 100-value OR-clause ceiling. ([#117](https://github.com/cyanheads/pubmed-mcp-server/issues/117))
19
+ - **A failed Europe PMC/OpenAlex reference-coverage fallback was reported as a genuine empty answer** — a thrown fallback now surfaces via `coverageFailures` and a distinct notice instead of being folded into NCBI's "no reference list" wording, and a provider disabled by configuration is marked non-retryable with no retry guidance offered. ([#118](https://github.com/cyanheads/pubmed-mcp-server/issues/118))
20
+ - **A failed DOI backfill lookup was misreported as `no-doi`** — a new `doi-lookup-failed` reason (added to `UnavailableReasonSchema` and `TierOutcomeSchema`) now distinguishes an errored lookup from a record that genuinely has none. A DOI Europe PMC already returned is also now carried forward to Unpaywall instead of being discarded, skipping a redundant PubMed round-trip. ([#119](https://github.com/cyanheads/pubmed-mcp-server/issues/119))
21
+ - **`pubmed_search_articles` accepted a blank query and forwarded it to NCBI as a blank term** — a whitespace-only or sanitized-to-empty query was previously retried as a misclassified transient outage across seven attempts and roughly a minute of backoff; it now fails immediately with a declared `blank_query` reason (`ValidationError`, non-retryable). ([#122](https://github.com/cyanheads/pubmed-mcp-server/issues/122))
22
+ - **`pubmed_spell_check` and `pubmed_lookup_mesh` accepted blank queries and returned a false empty success** — both now reject the same `blank_query` reason before calling NCBI, instead of reporting a checked-but-empty result for a query that was never searched. ([#133](https://github.com/cyanheads/pubmed-mcp-server/issues/133))