@cyanheads/pubmed-mcp-server 2.10.15 → 2.10.17

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 (77) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +26 -15
  4. package/changelog/2.10.x/2.10.16.md +26 -0
  5. package/changelog/2.10.x/2.10.17.md +19 -0
  6. package/dist/mcp-server/tools/definitions/_schemas.d.ts +11 -1
  7. package/dist/mcp-server/tools/definitions/_schemas.d.ts.map +1 -1
  8. package/dist/mcp-server/tools/definitions/_schemas.js +13 -1
  9. package/dist/mcp-server/tools/definitions/_schemas.js.map +1 -1
  10. package/dist/mcp-server/tools/definitions/_text.d.ts +20 -3
  11. package/dist/mcp-server/tools/definitions/_text.d.ts.map +1 -1
  12. package/dist/mcp-server/tools/definitions/_text.js +35 -3
  13. package/dist/mcp-server/tools/definitions/_text.js.map +1 -1
  14. package/dist/mcp-server/tools/definitions/convert-ids.tool.d.ts +3 -0
  15. package/dist/mcp-server/tools/definitions/convert-ids.tool.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/definitions/convert-ids.tool.js +41 -9
  17. package/dist/mcp-server/tools/definitions/convert-ids.tool.js.map +1 -1
  18. package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts +3 -1
  19. package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/definitions/fetch-articles.tool.js +12 -4
  21. package/dist/mcp-server/tools/definitions/fetch-articles.tool.js.map +1 -1
  22. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts +25 -31
  23. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js +373 -131
  25. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/definitions/find-related.tool.d.ts +7 -3
  27. package/dist/mcp-server/tools/definitions/find-related.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/find-related.tool.js +38 -21
  29. package/dist/mcp-server/tools/definitions/find-related.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/format-citations.tool.d.ts +3 -1
  31. package/dist/mcp-server/tools/definitions/format-citations.tool.d.ts.map +1 -1
  32. package/dist/mcp-server/tools/definitions/format-citations.tool.js +12 -4
  33. package/dist/mcp-server/tools/definitions/format-citations.tool.js.map +1 -1
  34. package/dist/mcp-server/tools/definitions/lookup-citation.tool.d.ts +11 -2
  35. package/dist/mcp-server/tools/definitions/lookup-citation.tool.d.ts.map +1 -1
  36. package/dist/mcp-server/tools/definitions/lookup-citation.tool.js +62 -41
  37. package/dist/mcp-server/tools/definitions/lookup-citation.tool.js.map +1 -1
  38. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.d.ts +3 -3
  39. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.js +3 -1
  41. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.js.map +1 -1
  42. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts +5 -2
  43. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts.map +1 -1
  44. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js +19 -10
  45. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js.map +1 -1
  46. package/dist/mcp-server/tools/definitions/search-articles.tool.d.ts +9 -3
  47. package/dist/mcp-server/tools/definitions/search-articles.tool.d.ts.map +1 -1
  48. package/dist/mcp-server/tools/definitions/search-articles.tool.js +71 -11
  49. package/dist/mcp-server/tools/definitions/search-articles.tool.js.map +1 -1
  50. package/dist/mcp-server/tools/definitions/spell-check.tool.d.ts +5 -4
  51. package/dist/mcp-server/tools/definitions/spell-check.tool.d.ts.map +1 -1
  52. package/dist/mcp-server/tools/definitions/spell-check.tool.js +4 -3
  53. package/dist/mcp-server/tools/definitions/spell-check.tool.js.map +1 -1
  54. package/dist/services/error-contracts.d.ts +7 -5
  55. package/dist/services/error-contracts.d.ts.map +1 -1
  56. package/dist/services/error-contracts.js +7 -5
  57. package/dist/services/error-contracts.js.map +1 -1
  58. package/dist/services/europe-pmc/api-client.d.ts +14 -11
  59. package/dist/services/europe-pmc/api-client.d.ts.map +1 -1
  60. package/dist/services/europe-pmc/api-client.js +21 -18
  61. package/dist/services/europe-pmc/api-client.js.map +1 -1
  62. package/dist/services/europe-pmc/europe-pmc-service.d.ts +2 -0
  63. package/dist/services/europe-pmc/europe-pmc-service.d.ts.map +1 -1
  64. package/dist/services/europe-pmc/europe-pmc-service.js +6 -2
  65. package/dist/services/europe-pmc/europe-pmc-service.js.map +1 -1
  66. package/dist/services/ncbi/parsing/pmc-article-parser.d.ts +8 -0
  67. package/dist/services/ncbi/parsing/pmc-article-parser.d.ts.map +1 -1
  68. package/dist/services/ncbi/parsing/pmc-article-parser.js +159 -16
  69. package/dist/services/ncbi/parsing/pmc-article-parser.js.map +1 -1
  70. package/dist/services/unpaywall/types.d.ts +16 -2
  71. package/dist/services/unpaywall/types.d.ts.map +1 -1
  72. package/dist/services/unpaywall/unpaywall-service.d.ts +4 -3
  73. package/dist/services/unpaywall/unpaywall-service.d.ts.map +1 -1
  74. package/dist/services/unpaywall/unpaywall-service.js +15 -4
  75. package/dist/services/unpaywall/unpaywall-service.js.map +1 -1
  76. package/package.json +1 -1
  77. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/pubmed-mcp-server
4
- **Version:** 2.10.15
4
+ **Version:** 2.10.17
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
 
package/CLAUDE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/pubmed-mcp-server
4
- **Version:** 2.10.15
4
+ **Version:** 2.10.17
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
 
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
 
11
11
 
12
- [![Version](https://img.shields.io/badge/Version-2.10.15-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.17-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
 
@@ -44,9 +44,9 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
44
44
  | `pubmed_fetch_fulltext` | Fetch full-text articles via a chain: NCBI PMC EFetch → Europe PMC `fullTextXML` → Unpaywall. Accepts PMIDs, PMCIDs, or DOIs. |
45
45
  | `pubmed_format_citations` | Generate formatted citations in APA 7th, MLA 9th, BibTeX, RIS, or Vancouver (ICMJE/NLM) |
46
46
  | `pubmed_find_related` | Find similar articles, citing articles, or references for a given PMID |
47
- | `pubmed_spell_check` | Spell-check a biomedical query via NCBI ESpell — returns the corrected query and whether a suggestion was found |
47
+ | `pubmed_spell_check` | Spell-check a PubMed query via NCBI ESpell — every misspelled token corrected in one call; the recovery step after a zero-hit or thin search |
48
48
  | `pubmed_lookup_mesh` | Search MeSH by heading — tree numbers, scope notes, entry terms — for building controlled-vocabulary queries |
49
- | `pubmed_lookup_citation` | Resolve partial bibliographic references to PubMed IDs via ECitMatch |
49
+ | `pubmed_lookup_citation` | Resolve partial bibliographic references — one citation or a batch of up to 25 — to PubMed IDs via ECitMatch |
50
50
  | `pubmed_convert_ids` | Convert between DOI, PMID, and PMCID using the PMC ID Converter API |
51
51
 
52
52
  ### Resources
@@ -68,14 +68,17 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
68
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
69
  - Date ranges by publication, modification, or Entrez date; sort by relevance, date, author, or journal; offset pagination
70
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
71
+ - NCBI Bookshelf hits carry `bookTitle`, `publisherName`, `docType`, and `editors` in place of the empty `source`; the rendered summary shows the doc type only for these, not for ordinary journal articles (`citation`)
72
+ - Reports `totalCount`, stated in the header beside the page (`Returned: 3 of 2924`); echoes the original query, the fully applied PubMed query, and normalized filter metadata
73
+ - A query with no search term — blank, markup only, a bare field tag like `[pdat]`, or empty `()` — is rejected as `blank_query` rather than sent upstream
74
+ - `limit` is accepted for `maxResults`
73
75
 
74
76
  ---
75
77
 
76
78
  ### `pubmed_fetch_articles` <sub>tool</sub>
77
79
 
78
- - Up to 200 PMIDs per call (POST for batches of 100 or more)
80
+ - Up to 200 PMIDs per call (POST for batches of 100 or more); `ids` is accepted for `pmids`
81
+ - A zero-padded PMID (`00000001`) resolves as the PMID it spells; `unavailablePmids` lists misses as you sent them
79
82
  - Title, abstract, authors with deduplicated affiliations, journal info, DOI, PubMed/PMC links; optional MeSH terms, grants, and publication types
80
83
  - Tolerant of PubMed's inconsistent XML — structured abstracts, missing fields, varying date formats
81
84
  - 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
@@ -86,20 +89,23 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
86
89
 
87
90
  ### `pubmed_fetch_fulltext` <sub>tool</sub>
88
91
 
89
- - Exactly one of `pmcids`, `pmids`, or `dois` (one id per element), up to 10 per request
92
+ - Exactly one of `pmcids`, `pmids`, or `dois` (one id per element), up to 10 per request; a zero-padded PMID or PMC ID resolves as the ID it spells, DOIs match case-insensitively, and PMC IDs match with or without the `PMC` prefix in any case. Each record is fetched once however many spellings name it; `unavailable[].id` keeps your PMID or DOI spelling, and reports a PMC ID as `PMC<digits>`
90
93
  - 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
94
  - 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`)
95
+ - `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, and the file pointer read through `<alternatives>` when a figure offers several formats); `source: "unpaywall"` returns a best-effort body with `contentFormat` (`html-markdown` / `pdf-text`), a `title` from Unpaywall's record (else the Europe PMC record, else the HTML page), and `journalName` / `year` when Unpaywall has them
96
+ - A titled list, definition list, or boxed text that sits in the body outside any section, or is the only content of an untitled section, such as an abbreviations list, becomes a section under its own title, and a figure or table inside it names that section; a section with no title is headed `untitled section`, the same name the `truncation` ledger gives it, and section titles in headings and ledger lines are Markdown-escaped
93
97
  - 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
98
+ - 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, per section and subsection
99
+ - A budget cut ends at a word boundary; `truncate` drops every section and subsection past the cut, while `outline` keeps each heading and marks one the budget left empty
95
100
 
96
101
  ---
97
102
 
98
103
  ### `pubmed_europepmc_search` <sub>tool</sub>
99
104
 
100
105
  - 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`
106
+ - Cursor pagination via `cursorMark` — `*` for the first page, then `nextCursorMark`; `pageSize` up to 100, with `max_results` and `limit` accepted for it
102
107
  - Hits carry `source` plus `pmid` / `pmcId` / `doi` when known; `abstractSnippet` is capped at 400 characters, with `abstractTruncated` flagging the cut
108
+ - `totalCount` reports the full hit count, stated in the header beside the page; `searchUrl` opens the same source-filtered query on europepmc.org
103
109
  - Not registered when `EUROPEPMC_ENABLED=false`
104
110
 
105
111
  ---
@@ -117,7 +123,7 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
117
123
  - APA 7th, MLA 9th, BibTeX, RIS, Vancouver (ICMJE/NLM); several styles per article in one call, up to 50 articles
118
124
  - 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
125
  - Hand-rolled formatters — zero dependencies, Workers-compatible
120
- - Reports formatted counts and unavailable PMIDs
126
+ - Reports formatted counts and unavailable PMIDs; `ids` is accepted for `pmids`, and a zero-padded PMID resolves as the PMID it spells
121
127
 
122
128
  ---
123
129
 
@@ -125,12 +131,15 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
125
131
 
126
132
  - `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
133
  - 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
134
+ - `maxResults` up to 50 (`limit` also accepted) with offset pagination; `totalCount` reports the full match count, stated in the header beside the page
135
+ - A zero-padded source PMID resolves as the PMID it spells and is never listed among its own related articles
128
136
 
129
137
  ---
130
138
 
131
139
  ### `pubmed_spell_check` <sub>tool</sub>
132
140
 
133
- - Runs a query through NCBI ESpell and returns `original`, `corrected`, and `hasSuggestion`
141
+ - Runs a PubMed query through NCBI ESpell and returns `original`, `corrected`, and `hasSuggestion`; every misspelled token is corrected in one call (`alzhiemer diseese treatmnt outcomse` → `alzheimer disease treatment outcomes`)
142
+ - Reach for it after a zero-hit or thin `pubmed_search_articles` result, or when a drug, gene, disease, or author name may be misspelled, then re-run the search with `corrected`
134
143
  - A blank or whitespace-only query is rejected rather than sent upstream
135
144
 
136
145
  ---
@@ -139,13 +148,14 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
139
148
 
140
149
  - Looks up MeSH descriptors by name or free-text term, pinning the exact-heading match to the top of the first page
141
150
  - 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
151
+ - `maxResults` up to 50 (`limit` also accepted) with offset pagination via `nextOffset`; `totalCount` reports the upstream match count
143
152
 
144
153
  ---
145
154
 
146
155
  ### `pubmed_lookup_citation` <sub>tool</sub>
147
156
 
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
157
+ - Match on journal, year, volume, first page, and/or author — journal or year required, more fields for better precision
158
+ - `citations` takes an array of up to 25 or a single citation object; `citation` is accepted for it
149
159
  - Pipes and line breaks are rejected at the schema (ECitMatch's wire format is pipe-delimited); the free-form `key` label is exempt
150
160
  - Explicit `matched`, `not_found`, and `ambiguous` statuses with recovery detail
151
161
 
@@ -155,7 +165,8 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
155
165
 
156
166
  - Up to 50 DOIs, PMIDs, or PMCIDs per call, all one type; only PMC-indexed articles resolve
157
167
  - 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
168
+ - One success/error row per submitted element, in order, with `requestedId` exactly as sent — repeats, a bare-digit PMCID, and a DOI's casing included; a partial batch never fails as a whole
169
+ - A zero-padded PMID resolves as the PMID it spells
159
170
 
160
171
  ---
161
172
 
@@ -0,0 +1,26 @@
1
+ ---
2
+ summary: "Normalizes a zero-padded PMID to the PMID it spells across every ID-diffing tool, and adds snake_case/synonym parameter aliases (ids, limit, max_results, citation) across the tool surface."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 2.10.16 — 2026-09-22
8
+
9
+ ## Added
10
+
11
+ - **`inputAliases` accept snake_case and near-synonym parameter names**, rewritten to the canonical key before the schema parses and never advertised in `tools/list`: `ids` for `pmids` (`pubmed_fetch_articles`, `pubmed_format_citations`), `limit` for `maxResults`/`pageSize` (`pubmed_search_articles`, `pubmed_find_related`, `pubmed_lookup_mesh`, `pubmed_europepmc_search`), `max_results` for `pageSize` (`pubmed_europepmc_search`), and `citation` for `citations` (`pubmed_lookup_citation`). [#156](https://github.com/cyanheads/pubmed-mcp-server/issues/156)
12
+
13
+ ## Changed
14
+
15
+ - **`pubmed_search_articles`, `pubmed_find_related`, and `pubmed_europepmc_search` state `totalCount` in the header** (`Returned: 3 of 2924`) instead of the enrichment trailer. It moves from `enrichment` into each tool's `output`, so `structuredContent.totalCount` keeps its value and type. [#147](https://github.com/cyanheads/pubmed-mcp-server/issues/147)
16
+ - **`docType` is rendered only when it marks something other than an ordinary journal article (`citation`)** in `pubmed_search_articles` and `pubmed_find_related` summaries. [#146](https://github.com/cyanheads/pubmed-mcp-server/issues/146)
17
+ - **`pubmed_lookup_citation`'s `citations` accepts a single citation object or an array of up to 25**, with a named error for a value that fits neither shape. [#156](https://github.com/cyanheads/pubmed-mcp-server/issues/156)
18
+ - **`pubmed_europepmc_search`'s query description documents that identifier tokens may be quoted inside its source-filter wrapper**, and that a PubMed-indexed article resolves under `SRC:MED`, not `SRC:PMC`. [#149](https://github.com/cyanheads/pubmed-mcp-server/issues/149)
19
+ - **`pubmed_spell_check`'s description names PubMed and ESpell and states its role as the recovery step after a zero-hit or thin search**, with a worked correction example. [#151](https://github.com/cyanheads/pubmed-mcp-server/issues/151)
20
+
21
+ ## Fixed
22
+
23
+ - **A zero-padded PMID (`00000001`) is no longer both returned and reported unavailable in the same response.** NCBI reads it as the PMID it spells; `pubmed_fetch_articles`, `pubmed_fetch_fulltext`, `pubmed_format_citations`, `pubmed_find_related`, and `pubmed_convert_ids` now send and compare the canonical form while reporting the caller's own spelling. [#161](https://github.com/cyanheads/pubmed-mcp-server/issues/161)
24
+ - **`pubmed_convert_ids` gives every submitted element its own record, in submission order, under `requestedId` exactly as sent** — a repeated ID, including one DOI in two casings, gets a record per element instead of one shared record; a bare-digit PMCID is reported as sent rather than as `PMC<digits>`; and an element the converter returns nothing for gets an error row instead of disappearing. [#165](https://github.com/cyanheads/pubmed-mcp-server/issues/165)
25
+ - **`pubmed_search_articles`'s blank-query guard now catches a bare field tag (`[pdat]`) or empty parentheses**, which previously passed the check: `[pdat]` reached NCBI as a zero-hit search pointing at `pubmed_spell_check`, and `()` drew NCBI's blank-term error, misread as a retryable outage. Only brackets holding a PubMed field tag are stripped for the check, so `[18F]` and `benzo[a]pyrene` are still searched as text. [#145](https://github.com/cyanheads/pubmed-mcp-server/issues/145)
26
+ - **`pubmed_europepmc_search`'s `searchUrl` now carries the same source filter the search actually ran**, instead of opening an unfiltered query with a different hit count. The reported `query` falls back to that same source-filtered query when Europe PMC echoes none. [#150](https://github.com/cyanheads/pubmed-mcp-server/issues/150)
@@ -0,0 +1,19 @@
1
+ ---
2
+ summary: "Fixes pubmed_fetch_fulltext body parsing (alternatives figures, titled body-level blocks), word-boundary truncation with per-subsection accounting, Unpaywall title/journal/year, and case-insensitive DOI and zero-padded PMC ID matching."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 2.10.17 — 2026-09-23
8
+
9
+ ## Fixed
10
+
11
+ - **`pubmed_fetch_fulltext` reads a figure's or supplementary file's pointer from `<alternatives>`** when it has no direct `<graphic>`/`<media>`, preferring the rendering whose `specific-use` names the web, else the first. Its `href`, and a label or caption hung on that pointer, are no longer lost. [#142](https://github.com/cyanheads/pubmed-mcp-server/issues/142)
12
+ - **Character-budget cuts end at the last word boundary inside the allowance**, in sections and Unpaywall bodies alike; a single token longer than the allowance is still cut at it. In `truncate` mode every subsection past the cut is dropped and counted in `omittedSections`, where only whole top-level sections were before; `outline` mode keeps those headings and marks them in `content[]`. The `truncation` ledger lists a shortened section's `subsections` with their own accounting, and each entry carries its `label`. [#143](https://github.com/cyanheads/pubmed-mcp-server/issues/143)
13
+ - **Unpaywall articles carry a `title`**, from Unpaywall's own DOI record, else the Europe PMC record the chain searched, else — for HTML content only — the page's own title, **plus `journalName` and `year`** when Unpaywall's record has them. [#144](https://github.com/cyanheads/pubmed-mcp-server/issues/144)
14
+ - **A titled list, definition list, or boxed text sitting outside any `<sec>`, or alone inside an untitled one, becomes a section under its own title** instead of an untitled block with no heading to separate it from its neighbors. A figure or table inside a titled box names that section as its `sectionTitle`, so a `sections` filter on the box keeps them. [#148](https://github.com/cyanheads/pubmed-mcp-server/issues/148), [#169](https://github.com/cyanheads/pubmed-mcp-server/issues/169)
15
+ - **Every section gets a heading in `content[]`**: an untitled one is headed `untitled section` and a label-only one by its label, the same name its truncation-ledger line uses, and section titles are Markdown-escaped in both places. [#148](https://github.com/cyanheads/pubmed-mcp-server/issues/148), [#169](https://github.com/cyanheads/pubmed-mcp-server/issues/169)
16
+ - **A `<boxed-text>` caption's title and paragraph render as separate lines** instead of running together. [#169](https://github.com/cyanheads/pubmed-mcp-server/issues/169)
17
+ - **DOI input matches case-insensitively**: every casing of one DOI runs the chain once, and `unavailable[]` reports each casing as submitted. Two different DOIs the ID Converter places on one PMC record also share one chain, and that record is fetched once. [#166](https://github.com/cyanheads/pubmed-mcp-server/issues/166)
18
+ - **`errors[]` no longer declares Unpaywall or Europe PMC failure reasons the handler never throws** — both are folded into each id's `triedTiers` instead of propagated. [#168](https://github.com/cyanheads/pubmed-mcp-server/issues/168)
19
+ - **A zero-padded PMC ID (`PMC0123`) resolves as the record it spells** instead of being returned and reported unavailable in the same response. Every spelling of one PMC ID — with or without the `PMC` prefix, in any case, zero-padded or not — is fetched once and reported in `PMC<digits>` form. [#170](https://github.com/cyanheads/pubmed-mcp-server/issues/170)
@@ -1,5 +1,6 @@
1
1
  /**
2
- * @fileoverview Shared Zod schemas reused across tool definitions.
2
+ * @fileoverview Shared Zod schemas reused across tool definitions, plus the
3
+ * normalizer for the PMID form {@link pmidStringSchema} accepts.
3
4
  * @module src/mcp-server/tools/definitions/_schemas
4
5
  */
5
6
  import { z } from '@cyanheads/mcp-ts-core';
@@ -11,6 +12,15 @@ import { z } from '@cyanheads/mcp-ts-core';
11
12
  * stray prefixes like "PMID:").
12
13
  */
13
14
  export declare const pmidStringSchema: z.ZodString;
15
+ /**
16
+ * Canonical form of a PMID {@link pmidStringSchema} accepted: leading zeros
17
+ * stripped, `0` kept for an all-zero input. NCBI reads `00000001` as PMID 1 and
18
+ * answers with `<PMID>1</PMID>`, so a handler sends this form upstream and
19
+ * compares upstream PMIDs against it — the schema itself stays as advertised,
20
+ * since tool schemas carry no transforms. `0` is still no UID, so an all-zero
21
+ * list keeps NCBI's empty-list answer. (#161)
22
+ */
23
+ export declare function normalizePmid(pmid: string): string;
14
24
  /**
15
25
  * Zod string schema for a single PMC ID. Digits, with the "PMC" prefix optional
16
26
  * and case-insensitive — both forms are accepted upstream.
@@ -1 +1 @@
1
- {"version":3,"file":"_schemas.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_schemas.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,aAK1B,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,iBAAiB,aAK3B,CAAC;AAEJ;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,aAKzB,CAAC"}
1
+ {"version":3,"file":"_schemas.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_schemas.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,aAK1B,CAAC;AAEJ;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,aAK3B,CAAC;AAEJ;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,aAKzB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  /**
2
- * @fileoverview Shared Zod schemas reused across tool definitions.
2
+ * @fileoverview Shared Zod schemas reused across tool definitions, plus the
3
+ * normalizer for the PMID form {@link pmidStringSchema} accepts.
3
4
  * @module src/mcp-server/tools/definitions/_schemas
4
5
  */
5
6
  import { z } from '@cyanheads/mcp-ts-core';
@@ -13,6 +14,17 @@ import { z } from '@cyanheads/mcp-ts-core';
13
14
  export const pmidStringSchema = z
14
15
  .string()
15
16
  .regex(/^\d+$/, 'PMID must be a numeric identifier (e.g. "13054692"). Remove any whitespace, commas, or non-digit characters — provide each PMID separately.');
17
+ /**
18
+ * Canonical form of a PMID {@link pmidStringSchema} accepted: leading zeros
19
+ * stripped, `0` kept for an all-zero input. NCBI reads `00000001` as PMID 1 and
20
+ * answers with `<PMID>1</PMID>`, so a handler sends this form upstream and
21
+ * compares upstream PMIDs against it — the schema itself stays as advertised,
22
+ * since tool schemas carry no transforms. `0` is still no UID, so an all-zero
23
+ * list keeps NCBI's empty-list answer. (#161)
24
+ */
25
+ export function normalizePmid(pmid) {
26
+ return pmid.replace(/^0+(?=\d)/, '');
27
+ }
16
28
  /**
17
29
  * Zod string schema for a single PMC ID. Digits, with the "PMC" prefix optional
18
30
  * and case-insensitive — both forms are accepted upstream.
@@ -1 +1 @@
1
- {"version":3,"file":"_schemas.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_schemas.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC;KAC9B,MAAM,EAAE;KACR,KAAK,CACJ,OAAO,EACP,6IAA6I,CAC9I,CAAC;AAEJ;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,EAAE;KACR,KAAK,CACJ,gBAAgB,EAChB,2JAA2J,CAC5J,CAAC;AAEJ;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,KAAK,CACJ,wBAAwB,EACxB,0IAA0I,CAC3I,CAAC"}
1
+ {"version":3,"file":"_schemas.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_schemas.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC;KAC9B,MAAM,EAAE;KACR,KAAK,CACJ,OAAO,EACP,6IAA6I,CAC9I,CAAC;AAEJ;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,EAAE;KACR,KAAK,CACJ,gBAAgB,EAChB,2JAA2J,CAC5J,CAAC;AAEJ;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,KAAK,CACJ,wBAAwB,EACxB,0IAA0I,CAC3I,CAAC"}
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * @fileoverview Text helpers shared by the tool definitions: the surrogate-safe
3
- * character cut used to bound returned text to a budget, and the render-time
4
- * Markdown escapes applied to upstream strings interpolated into `format()` —
5
- * one for an inline position, one for a table cell.
3
+ * character cut and the word-boundary cut built on it, which bound returned text
4
+ * to a budget, and the render-time Markdown escapes applied to upstream strings
5
+ * interpolated into `format()` — one for an inline position, one for a table
6
+ * cell.
6
7
  * @module src/mcp-server/tools/definitions/_text
7
8
  */
8
9
  /**
@@ -21,6 +22,22 @@
21
22
  * than assume the allowance was spent exactly. (#93)
22
23
  */
23
24
  export declare function sliceCodeUnits(text: string, limit: number): string;
25
+ /**
26
+ * Take at most `limit` UTF-16 code units from `text`, ending at the last word
27
+ * boundary inside the allowance rather than partway through a word.
28
+ *
29
+ * The cut is {@link sliceCodeUnits}' cut, backed off past the partial word it
30
+ * would end on — unless the character after it is whitespace, in which case
31
+ * the last word is already whole — with the whitespace before that word
32
+ * trimmed. A cut is therefore never mid-word and never ends in whitespace.
33
+ * `limit` stays a ceiling: the result can come back several units short of it,
34
+ * so callers reporting character counts measure the returned string. (#143)
35
+ *
36
+ * An allowance holding no boundary at all — one token longer than the
37
+ * allowance, such as a sequence or a URL — is cut at the allowance instead:
38
+ * backing off would return nothing, dropping a field that has text to give.
39
+ */
40
+ export declare function sliceAtWordBoundary(text: string, limit: number): string;
24
41
  /**
25
42
  * Escape the Markdown-significant characters in an upstream string that is
26
43
  * about to be interpolated *inline* into a `format()` line — a `### `/`#### `
@@ -1 +1 @@
1
- {"version":3,"file":"_text.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_text.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlE;AA2CD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAazD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D"}
1
+ {"version":3,"file":"_text.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_text.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlE;AAQD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAUvE;AA2CD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAazD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * @fileoverview Text helpers shared by the tool definitions: the surrogate-safe
3
- * character cut used to bound returned text to a budget, and the render-time
4
- * Markdown escapes applied to upstream strings interpolated into `format()` —
5
- * one for an inline position, one for a table cell.
3
+ * character cut and the word-boundary cut built on it, which bound returned text
4
+ * to a budget, and the render-time Markdown escapes applied to upstream strings
5
+ * interpolated into `format()` — one for an inline position, one for a table
6
+ * cell.
6
7
  * @module src/mcp-server/tools/definitions/_text
7
8
  */
8
9
  const HIGH_SURROGATE_FIRST = 0xd800;
@@ -31,6 +32,37 @@ export function sliceCodeUnits(text, limit) {
31
32
  const splitsPair = last >= HIGH_SURROGATE_FIRST && last <= HIGH_SURROGATE_LAST;
32
33
  return text.slice(0, splitsPair ? limit - 1 : limit);
33
34
  }
35
+ /** A whitespace character — the boundary a word-boundary cut backs off to. */
36
+ const WHITESPACE_RE = /\s/;
37
+ const isWhitespace = (text, index) => WHITESPACE_RE.test(text.charAt(index));
38
+ /**
39
+ * Take at most `limit` UTF-16 code units from `text`, ending at the last word
40
+ * boundary inside the allowance rather than partway through a word.
41
+ *
42
+ * The cut is {@link sliceCodeUnits}' cut, backed off past the partial word it
43
+ * would end on — unless the character after it is whitespace, in which case
44
+ * the last word is already whole — with the whitespace before that word
45
+ * trimmed. A cut is therefore never mid-word and never ends in whitespace.
46
+ * `limit` stays a ceiling: the result can come back several units short of it,
47
+ * so callers reporting character counts measure the returned string. (#143)
48
+ *
49
+ * An allowance holding no boundary at all — one token longer than the
50
+ * allowance, such as a sequence or a URL — is cut at the allowance instead:
51
+ * backing off would return nothing, dropping a field that has text to give.
52
+ */
53
+ export function sliceAtWordBoundary(text, limit) {
54
+ const cut = sliceCodeUnits(text, limit);
55
+ if (cut.length === text.length)
56
+ return cut;
57
+ let end = cut.length;
58
+ if (!isWhitespace(text, end)) {
59
+ while (end > 0 && !isWhitespace(cut, end - 1))
60
+ end -= 1;
61
+ }
62
+ while (end > 0 && isWhitespace(cut, end - 1))
63
+ end -= 1;
64
+ return end > 0 ? cut.slice(0, end) : cut.trimEnd();
65
+ }
34
66
  /** Any line break, including the Unicode line and paragraph separators. */
35
67
  const LINE_BREAK_RE = /[\r\n\u2028\u2029]+/g;
36
68
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"_text.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_text.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"_text.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_text.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * @fileoverview Article ID conversion tool. Converts between DOI, PMID, and PMCID
3
3
  * using the NCBI PMC ID Converter API for deterministic, batch-friendly resolution.
4
+ * Every submitted element gets its own record, in submission order, with the
5
+ * caller's own spelling as `requestedId`; a zero-padded PMID is converted as the
6
+ * PMID it spells.
4
7
  * @module src/mcp-server/tools/definitions/convert-ids.tool
5
8
  */
6
9
  import { z } from '@cyanheads/mcp-ts-core';
@@ -1 +1 @@
1
- {"version":3,"file":"convert-ids.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/convert-ids.tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAiCjD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAyHzB,CAAC"}
1
+ {"version":3,"file":"convert-ids.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/convert-ids.tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAsDjD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwIzB,CAAC"}
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * @fileoverview Article ID conversion tool. Converts between DOI, PMID, and PMCID
3
3
  * using the NCBI PMC ID Converter API for deterministic, batch-friendly resolution.
4
+ * Every submitted element gets its own record, in submission order, with the
5
+ * caller's own spelling as `requestedId`; a zero-padded PMID is converted as the
6
+ * PMID it spells.
4
7
  * @module src/mcp-server/tools/definitions/convert-ids.tool
5
8
  */
6
9
  import { tool, z } from '@cyanheads/mcp-ts-core';
7
10
  import { NCBI_ID_INPUT_ERRORS, NCBI_SERVICE_ERRORS } from '../../../services/error-contracts.js';
8
11
  import { getNcbiService } from '../../../services/ncbi/ncbi-service.js';
9
12
  import { conceptMeta, EDAM_ACCESSION, EDAM_ID_MAPPING } from './_concepts.js';
10
- import { doiStringSchema, pmcidStringSchema, pmidStringSchema } from './_schemas.js';
13
+ import { doiStringSchema, normalizePmid, pmcidStringSchema, pmidStringSchema } from './_schemas.js';
11
14
  /**
12
15
  * NCBI's PMC ID Converter returns this exact wording for any non-PMC ID — even
13
16
  * articles that exist in PubMed and have a recoverable DOI. Rewrite to point
@@ -31,6 +34,24 @@ const ID_ELEMENT_SCHEMAS = {
31
34
  };
32
35
  /** Cap the offending value echoed back so an oversized element can't bloat the error. */
33
36
  const MAX_ECHOED_ID_LENGTH = 120;
37
+ /** A submitted element the converter's answer carries no record for. */
38
+ const NO_RECORD_ERRMSG = 'The PMC ID Converter returned no record for this ID. Article may still exist in PubMed — try pubmed_search_articles.';
39
+ /**
40
+ * The form the PMC ID Converter echoes an identifier in, so each element can be
41
+ * matched to its answer: a PMID's canonical digits, a PMCID PMC-prefixed and
42
+ * upper-cased, a DOI lower-cased — DOIs are case-insensitive, and the converter
43
+ * echoes one casing for DOIs that differ only in case.
44
+ */
45
+ function matchKey(id, idType) {
46
+ switch (idType) {
47
+ case 'pmid':
48
+ return normalizePmid(id);
49
+ case 'pmcid':
50
+ return (/^\d+$/.test(id) ? `PMC${id}` : id).toUpperCase();
51
+ case 'doi':
52
+ return id.toLowerCase();
53
+ }
54
+ }
34
55
  export const convertIdsTool = tool('pubmed_convert_ids', {
35
56
  description: `Convert between article identifiers (DOI, PMID, PMCID). Accepts up to 50 IDs of a single type per request. Only resolves articles indexed in PubMed Central — for articles not in PMC, use pubmed_search_articles instead.`,
36
57
  annotations: { readOnlyHint: true, openWorldHint: true },
@@ -84,29 +105,40 @@ export const convertIdsTool = tool('pubmed_convert_ids', {
84
105
  const shown = id.length > MAX_ECHOED_ID_LENGTH ? `${id.slice(0, MAX_ECHOED_ID_LENGTH)}…` : id;
85
106
  throw ctx.fail('malformed_id', `Invalid ${input.idType} element "${shown}". ${parsed.error.issues[0]?.message}`, { ...ctx.recoveryFor('malformed_id') });
86
107
  }
87
- const raw = await getNcbiService().idConvert(input.ids, input.idType, { signal: ctx.signal });
108
+ // The converter parses `0023193287` as PMID 23193287 yet answers "not found
109
+ // in PMC" for it, so a PMID is sent in its canonical form. (#161)
110
+ const ids = input.idType === 'pmid' ? [...new Set(input.ids.map(normalizePmid))] : input.ids;
111
+ const raw = await getNcbiService().idConvert(ids, input.idType, { signal: ctx.signal });
88
112
  // NCBI returns pmid as a number in JSON — coerce all ID fields to strings
89
- const records = raw.map((r) => {
90
- const requestedId = String(r['requested-id']);
113
+ const conversions = new Map();
114
+ for (const r of raw) {
115
+ const requested = String(r['requested-id']);
91
116
  let errmsg;
92
117
  if (r.errmsg !== undefined) {
93
118
  const original = String(r.errmsg);
94
119
  if (PMC_NOT_FOUND_RE.test(original)) {
95
- ctx.log.debug('Rewriting PMC-not-found errmsg', { requestedId, original });
120
+ ctx.log.debug('Rewriting PMC-not-found errmsg', { requestedId: requested, original });
96
121
  errmsg = PMC_NOT_FOUND_REWRITE;
97
122
  }
98
123
  else {
99
124
  errmsg = original;
100
125
  }
101
126
  }
102
- return {
103
- requestedId,
127
+ conversions.set(matchKey(requested, input.idType), {
104
128
  ...(r.pmid !== undefined && { pmid: String(r.pmid) }),
105
129
  ...(r.pmcid !== undefined && { pmcid: String(r.pmcid) }),
106
130
  ...(r.doi !== undefined && { doi: String(r.doi) }),
107
131
  ...(errmsg !== undefined && { errmsg }),
108
- };
109
- });
132
+ });
133
+ }
134
+ // The converter answers a repeated identifier once and echoes the form it
135
+ // was sent in — PMC-prefixed, upper-cased, or with another element's DOI
136
+ // casing — so each element is matched to its answer by that key and
137
+ // reported under its own spelling. (#165)
138
+ const records = input.ids.map((requestedId) => ({
139
+ requestedId,
140
+ ...(conversions.get(matchKey(requestedId, input.idType)) ?? { errmsg: NO_RECORD_ERRMSG }),
141
+ }));
110
142
  const totalConverted = records.filter((r) => !r.errmsg).length;
111
143
  ctx.log.info('pubmed_convert_ids completed', {
112
144
  totalConverted,
@@ -1 +1 @@
1
- {"version":3,"file":"convert-ids.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/convert-ids.tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAErF;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,gCAAgC,CAAC;AAC1D,MAAM,qBAAqB,GACzB,gIAAgI,CAAC;AAEnI;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GAAG;IACzB,GAAG,EAAE,eAAe;IACpB,KAAK,EAAE,iBAAiB;IACxB,IAAI,EAAE,gBAAgB;CACd,CAAC;AAEX,yFAAyF;AACzF,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAEjC,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,oBAAoB,EAAE;IACvD,WAAW,EAAE,4NAA4N;IACzO,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE;IACxD,KAAK,EAAE,WAAW,CAAC,CAAC,eAAe,EAAE,cAAc,CAAC,CAAC;IACrD,SAAS,EACP,+GAA+G;IAEjH,MAAM,EAAE,CAAC,GAAG,mBAAmB,EAAE,GAAG,oBAAoB,CAAU;IAElE,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,GAAG,EAAE,CAAC;aACH,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;aACxB,GAAG,CAAC,CAAC,CAAC;aACN,GAAG,CAAC,EAAE,CAAC;aACP,QAAQ,CACP,icAAic,CAClc;QACH,MAAM,EAAE,CAAC;aACN,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;aAC9B,QAAQ,CACP,sFAAsF,CACvF;KACJ,CAAC;IAEF,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,OAAO,EAAE,CAAC;aACP,KAAK,CACJ,CAAC;aACE,MAAM,CAAC;YACN,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,2BAA2B,CAAC;YAC7D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2CAA2C,CAAC;YACjF,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,0DAA0D,CAAC;YACvE,GAAG,EAAE,CAAC;iBACH,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CACP,6QAA6Q,CAC9Q;YACH,MAAM,EAAE,CAAC;iBACN,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CACP,yHAAyH,CAC1H;SACJ,CAAC;aACD,QAAQ,CAAC,0BAA0B,CAAC,CACxC;aACA,QAAQ,CAAC,sCAAsC,CAAC;QACnD,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sCAAsC,CAAC;QAC3E,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;KAC/D,CAAC;IAEF,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG;QACtB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,8BAA8B,EAAE;YAC3C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM;YACvB,MAAM,EAAE,KAAK,CAAC,MAAM;SACrB,CAAC,CAAC;QAEH,MAAM,aAAa,GAAG,kBAAkB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACvD,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YAC3C,IAAI,MAAM,CAAC,OAAO;gBAAE,SAAS;YAC7B,MAAM,KAAK,GAAG,EAAE,CAAC,MAAM,GAAG,oBAAoB,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9F,MAAM,GAAG,CAAC,IAAI,CACZ,cAAc,EACd,WAAW,KAAK,CAAC,MAAM,aAAa,KAAK,MAAM,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAChF,EAAE,GAAG,GAAG,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,CACvC,CAAC;QACJ,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,cAAc,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAE9F,0EAA0E;QAC1E,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YAC5B,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC;YAC9C,IAAI,MAA0B,CAAC;YAC/B,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;gBAClC,IAAI,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACpC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,gCAAgC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC;oBAC3E,MAAM,GAAG,qBAAqB,CAAC;gBACjC,CAAC;qBAAM,CAAC;oBACN,MAAM,GAAG,QAAQ,CAAC;gBACpB,CAAC;YACH,CAAC;YACD,OAAO;gBACL,WAAW;gBACX,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrD,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;gBACxD,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClD,GAAG,CAAC,MAAM,KAAK,SAAS,IAAI,EAAE,MAAM,EAAE,CAAC;aACxC,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;QAC/D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,8BAA8B,EAAE;YAC3C,cAAc;YACd,cAAc,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM;SACjC,CAAC,CAAC;QAEH,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IACvE,CAAC;IAED,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;QACjB,MAAM,KAAK,GAAG;YACZ,0BAA0B;YAC1B,kBAAkB,MAAM,CAAC,cAAc,IAAI,MAAM,CAAC,cAAc,EAAE;YAClE,EAAE;YACF,+CAA+C;YAC/C,4BAA4B;SAC7B,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC/B,KAAK,CAAC,IAAI,CACR,KAAK,CAAC,CAAC,WAAW,MAAM,CAAC,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,CAAC,MAAM,IAAI,GAAG,IAAI,CACrG,CAAC;QACJ,CAAC;QACD,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACpD,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"convert-ids.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/convert-ids.tool.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * @fileoverview PubMed fetch tool. Fetches full article metadata by PubMed IDs,
3
- * including abstracts, authors, journal info, and MeSH terms.
3
+ * including abstracts, authors, journal info, and MeSH terms. A zero-padded
4
+ * PMID is fetched and matched as the PMID it spells; `ids` is accepted as an
5
+ * alias for `pmids`.
4
6
  * @module src/mcp-server/tools/definitions/fetch-articles.tool
5
7
  */
6
8
  import { z } from '@cyanheads/mcp-ts-core';
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-articles.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fetch-articles.tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAwRjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAWhB,yBAAyB;;mBAE3B,sEAAsE;uBAE1E,qFAAqF;;;;EAkR3F,CAAC"}
1
+ {"version":3,"file":"fetch-articles.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fetch-articles.tool.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAwRjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAWhB,yBAAyB;;mBAE3B,sEAAsE;uBAE1E,qFAAqF;;;;EA0R3F,CAAC"}
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * @fileoverview PubMed fetch tool. Fetches full article metadata by PubMed IDs,
3
- * including abstracts, authors, journal info, and MeSH terms.
3
+ * including abstracts, authors, journal info, and MeSH terms. A zero-padded
4
+ * PMID is fetched and matched as the PMID it spells; `ids` is accepted as an
5
+ * alias for `pmids`.
4
6
  * @module src/mcp-server/tools/definitions/fetch-articles.tool
5
7
  */
6
8
  import { tool, z } from '@cyanheads/mcp-ts-core';
@@ -10,7 +12,7 @@ import { getNcbiService } from '../../../services/ncbi/ncbi-service.js';
10
12
  import { parseArticleSet } from '../../../services/ncbi/parsing/article-parser.js';
11
13
  import { fitWholeItems } from './_budget.js';
12
14
  import { conceptMeta, EDAM_DATA_RETRIEVAL, EDAM_PUBMED_ID, SCHEMA_SCHOLARLY_ARTICLE, } from './_concepts.js';
13
- import { pmidStringSchema } from './_schemas.js';
15
+ import { normalizePmid, pmidStringSchema } from './_schemas.js';
14
16
  import { escapeMarkdownInline } from './_text.js';
15
17
  const AuthorSchema = z
16
18
  .object({
@@ -225,6 +227,8 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
225
227
  recovery: 'Retry once; if it persists, NCBI returned malformed data — try fewer PMIDs at once.',
226
228
  },
227
229
  ],
230
+ // Never advertised; rewritten to the canonical key before the schema parses. (#156)
231
+ inputAliases: { ids: 'pmids' },
228
232
  input: z.object({
229
233
  pmids: z.array(pmidStringSchema).min(1).max(200).describe('PubMed IDs to fetch'),
230
234
  includeMesh: z.boolean().default(true).describe('Include MeSH terms'),
@@ -263,7 +267,11 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
263
267
  },
264
268
  async handler(input, ctx) {
265
269
  ctx.log.info('Executing pubmed_fetch', { pmidCount: input.pmids.length });
266
- const xmlData = await getNcbiService().eFetch({ db: 'pubmed', id: input.pmids.join(','), retmode: 'xml' }, { retmode: 'xml', usePost: input.pmids.length >= 100, signal: ctx.signal });
270
+ // NCBI reads `00000001` as PMID 1 and answers with `<PMID>1</PMID>`, so the
271
+ // request and the unavailability diff below both use the canonical form;
272
+ // `unavailablePmids` still reports the caller's own spelling. (#161)
273
+ const requested = [...new Set(input.pmids.map(normalizePmid))];
274
+ const xmlData = await getNcbiService().eFetch({ db: 'pubmed', id: requested.join(','), retmode: 'xml' }, { retmode: 'xml', usePost: requested.length >= 100, signal: ctx.signal });
267
275
  if (!xmlData || !('PubmedArticleSet' in xmlData)) {
268
276
  throw ctx.fail('invalid_efetch_response', 'Invalid EFetch response from NCBI: missing PubmedArticleSet', { requestedPmids: input.pmids.length, ...ctx.recoveryFor('invalid_efetch_response') });
269
277
  }
@@ -281,7 +289,7 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
281
289
  }),
282
290
  }));
283
291
  const returnedPmids = new Set(articles.map((a) => a.pmid).filter(Boolean));
284
- const unavailable = input.pmids.filter((id) => !returnedPmids.has(id));
292
+ const unavailable = input.pmids.filter((id) => !returnedPmids.has(normalizePmid(id)));
285
293
  // Whole-response budget: fill with complete records in response order and
286
294
  // hand the remainder back as PMIDs the caller can re-submit. Without
287
295
  // `maxResponseCharacters` nothing is measured and the response is exactly