@cyanheads/pubmed-mcp-server 2.10.14 → 2.10.16

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 (78) hide show
  1. package/AGENTS.md +4 -4
  2. package/CLAUDE.md +4 -4
  3. package/README.md +24 -15
  4. package/changelog/2.10.x/2.10.14.md +1 -0
  5. package/changelog/2.10.x/2.10.15.md +28 -0
  6. package/changelog/2.10.x/2.10.16.md +26 -0
  7. package/dist/mcp-server/tools/definitions/_schemas.d.ts +11 -1
  8. package/dist/mcp-server/tools/definitions/_schemas.d.ts.map +1 -1
  9. package/dist/mcp-server/tools/definitions/_schemas.js +13 -1
  10. package/dist/mcp-server/tools/definitions/_schemas.js.map +1 -1
  11. package/dist/mcp-server/tools/definitions/convert-ids.tool.d.ts +10 -2
  12. package/dist/mcp-server/tools/definitions/convert-ids.tool.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/convert-ids.tool.js +41 -9
  14. package/dist/mcp-server/tools/definitions/convert-ids.tool.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts +10 -3
  16. package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/fetch-articles.tool.js +12 -4
  18. package/dist/mcp-server/tools/definitions/fetch-articles.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts +16 -5
  20. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js +35 -12
  22. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/find-related.tool.d.ts +7 -63
  24. package/dist/mcp-server/tools/definitions/find-related.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/find-related.tool.js +42 -25
  26. package/dist/mcp-server/tools/definitions/find-related.tool.js.map +1 -1
  27. package/dist/mcp-server/tools/definitions/format-citations.tool.d.ts +10 -3
  28. package/dist/mcp-server/tools/definitions/format-citations.tool.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/definitions/format-citations.tool.js +12 -4
  30. package/dist/mcp-server/tools/definitions/format-citations.tool.js.map +1 -1
  31. package/dist/mcp-server/tools/definitions/lookup-citation.tool.d.ts +18 -4
  32. package/dist/mcp-server/tools/definitions/lookup-citation.tool.d.ts.map +1 -1
  33. package/dist/mcp-server/tools/definitions/lookup-citation.tool.js +62 -41
  34. package/dist/mcp-server/tools/definitions/lookup-citation.tool.js.map +1 -1
  35. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.d.ts +10 -5
  36. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.d.ts.map +1 -1
  37. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.js +3 -1
  38. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.js.map +1 -1
  39. package/dist/mcp-server/tools/definitions/pubmed-europepmc-fetch.tool.d.ts +6 -3
  40. package/dist/mcp-server/tools/definitions/pubmed-europepmc-fetch.tool.d.ts.map +1 -1
  41. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts +11 -5
  42. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts.map +1 -1
  43. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js +20 -11
  44. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js.map +1 -1
  45. package/dist/mcp-server/tools/definitions/search-articles.tool.d.ts +16 -5
  46. package/dist/mcp-server/tools/definitions/search-articles.tool.d.ts.map +1 -1
  47. package/dist/mcp-server/tools/definitions/search-articles.tool.js +71 -11
  48. package/dist/mcp-server/tools/definitions/search-articles.tool.js.map +1 -1
  49. package/dist/mcp-server/tools/definitions/spell-check.tool.d.ts +12 -6
  50. package/dist/mcp-server/tools/definitions/spell-check.tool.d.ts.map +1 -1
  51. package/dist/mcp-server/tools/definitions/spell-check.tool.js +4 -3
  52. package/dist/mcp-server/tools/definitions/spell-check.tool.js.map +1 -1
  53. package/dist/services/error-contracts.d.ts +32 -15
  54. package/dist/services/error-contracts.d.ts.map +1 -1
  55. package/dist/services/error-contracts.js +32 -15
  56. package/dist/services/error-contracts.js.map +1 -1
  57. package/dist/services/europe-pmc/api-client.d.ts +24 -9
  58. package/dist/services/europe-pmc/api-client.d.ts.map +1 -1
  59. package/dist/services/europe-pmc/api-client.js +47 -18
  60. package/dist/services/europe-pmc/api-client.js.map +1 -1
  61. package/dist/services/europe-pmc/europe-pmc-service.d.ts +22 -1
  62. package/dist/services/europe-pmc/europe-pmc-service.d.ts.map +1 -1
  63. package/dist/services/europe-pmc/europe-pmc-service.js +102 -43
  64. package/dist/services/europe-pmc/europe-pmc-service.js.map +1 -1
  65. package/dist/services/ncbi/ncbi-service.d.ts +18 -24
  66. package/dist/services/ncbi/ncbi-service.d.ts.map +1 -1
  67. package/dist/services/ncbi/ncbi-service.js +111 -120
  68. package/dist/services/ncbi/ncbi-service.js.map +1 -1
  69. package/dist/services/ncbi/request-queue.d.ts +22 -30
  70. package/dist/services/ncbi/request-queue.d.ts.map +1 -1
  71. package/dist/services/ncbi/request-queue.js +29 -128
  72. package/dist/services/ncbi/request-queue.js.map +1 -1
  73. package/dist/services/ncbi/response-handler.d.ts +14 -1
  74. package/dist/services/ncbi/response-handler.d.ts.map +1 -1
  75. package/dist/services/ncbi/response-handler.js +65 -9
  76. package/dist/services/ncbi/response-handler.js.map +1 -1
  77. package/package.json +6 -6
  78. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/pubmed-mcp-server
4
- **Version:** 2.10.14
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.4`
4
+ **Version:** 2.10.16
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
 
8
8
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
@@ -198,7 +198,7 @@ Handlers receive a unified `ctx` object. Key properties:
198
198
 
199
199
  Handlers throw — the framework catches, classifies, and formats.
200
200
 
201
- **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required (≥ 5 words, lint-validated) — it's the single source of truth for the recovery hint. Spread `ctx.recoveryFor('reason')` into `data` to mirror the contract recovery onto the wire (the framework mirrors `data.recovery.hint` into `content[]` text); pass an explicit `recovery: { hint: '...' }` when runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
201
+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required (≥ 5 words, lint-validated) — it's the single source of truth for the recovery hint. Spread `ctx.recoveryFor('reason')` into `data` to mirror the contract recovery onto the wire (the framework mirrors `data.recovery.hint` into `content[]` text unless the message already contains it verbatim, then closes the text with `(reason <reason> · not retryable)`); pass an explicit `recovery: { hint: '...' }` when runtime context matters. Forwarding is lint-enforced per throw site (`error-contract-recovery-unforwarded`). A declared reason the handler never names warns as `error-contract-unthrown` — entries the service layer throws carry `thrownBy: 'service'` (lint-only metadata; every entry in `src/services/error-contracts.ts`'s service arrays has it), and a tool whose handler catches a service's failures doesn't spread that service's array at all. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
202
202
 
203
203
  ```ts
204
204
  errors: [
@@ -257,7 +257,7 @@ src/
257
257
  ncbi/
258
258
  ncbi-service.ts # NCBI E-utilities service (init/accessor)
259
259
  api-client.ts # HTTP client for NCBI API
260
- request-queue.ts # Rate-limited request queue
260
+ request-queue.ts # Request queue (framework pacer, 429 cooldown)
261
261
  response-handler.ts # XML response parsing
262
262
  types.ts # NCBI/PubMed domain types
263
263
  parsing/ # XML parsers (article, esummary, PMC)
package/CLAUDE.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/pubmed-mcp-server
4
- **Version:** 2.10.14
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.4`
4
+ **Version:** 2.10.16
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
 
8
8
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
@@ -198,7 +198,7 @@ Handlers receive a unified `ctx` object. Key properties:
198
198
 
199
199
  Handlers throw — the framework catches, classifies, and formats.
200
200
 
201
- **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required (≥ 5 words, lint-validated) — it's the single source of truth for the recovery hint. Spread `ctx.recoveryFor('reason')` into `data` to mirror the contract recovery onto the wire (the framework mirrors `data.recovery.hint` into `content[]` text); pass an explicit `recovery: { hint: '...' }` when runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
201
+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required (≥ 5 words, lint-validated) — it's the single source of truth for the recovery hint. Spread `ctx.recoveryFor('reason')` into `data` to mirror the contract recovery onto the wire (the framework mirrors `data.recovery.hint` into `content[]` text unless the message already contains it verbatim, then closes the text with `(reason <reason> · not retryable)`); pass an explicit `recovery: { hint: '...' }` when runtime context matters. Forwarding is lint-enforced per throw site (`error-contract-recovery-unforwarded`). A declared reason the handler never names warns as `error-contract-unthrown` — entries the service layer throws carry `thrownBy: 'service'` (lint-only metadata; every entry in `src/services/error-contracts.ts`'s service arrays has it), and a tool whose handler catches a service's failures doesn't spread that service's array at all. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
202
202
 
203
203
  ```ts
204
204
  errors: [
@@ -257,7 +257,7 @@ src/
257
257
  ncbi/
258
258
  ncbi-service.ts # NCBI E-utilities service (init/accessor)
259
259
  api-client.ts # HTTP client for NCBI API
260
- request-queue.ts # Rate-limited request queue
260
+ request-queue.ts # Request queue (framework pacer, 429 cooldown)
261
261
  response-handler.ts # XML response parsing
262
262
  types.ts # NCBI/PubMed domain types
263
263
  parsing/ # XML parsers (article, esummary, PMC)
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
 
11
11
 
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/)
12
+ [![Version](https://img.shields.io/badge/Version-2.10.16-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,7 +89,7 @@ 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 resolves as the PMID it spells, and `unavailable[].id` keeps your spelling
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
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); `source: "unpaywall"` returns a best-effort body with `contentFormat` (`html-markdown` / `pdf-text`)
@@ -98,8 +101,9 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
98
101
  ### `pubmed_europepmc_search` <sub>tool</sub>
99
102
 
100
103
  - 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`
104
+ - Cursor pagination via `cursorMark` — `*` for the first page, then `nextCursorMark`; `pageSize` up to 100, with `max_results` and `limit` accepted for it
102
105
  - Hits carry `source` plus `pmid` / `pmcId` / `doi` when known; `abstractSnippet` is capped at 400 characters, with `abstractTruncated` flagging the cut
106
+ - `totalCount` reports the full hit count, stated in the header beside the page; `searchUrl` opens the same source-filtered query on europepmc.org
103
107
  - Not registered when `EUROPEPMC_ENABLED=false`
104
108
 
105
109
  ---
@@ -117,7 +121,7 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
117
121
  - APA 7th, MLA 9th, BibTeX, RIS, Vancouver (ICMJE/NLM); several styles per article in one call, up to 50 articles
118
122
  - 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
123
  - Hand-rolled formatters — zero dependencies, Workers-compatible
120
- - Reports formatted counts and unavailable PMIDs
124
+ - Reports formatted counts and unavailable PMIDs; `ids` is accepted for `pmids`, and a zero-padded PMID resolves as the PMID it spells
121
125
 
122
126
  ---
123
127
 
@@ -125,12 +129,15 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
125
129
 
126
130
  - `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
131
  - 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
132
+ - `maxResults` up to 50 (`limit` also accepted) with offset pagination; `totalCount` reports the full match count, stated in the header beside the page
133
+ - A zero-padded source PMID resolves as the PMID it spells and is never listed among its own related articles
128
134
 
129
135
  ---
130
136
 
131
137
  ### `pubmed_spell_check` <sub>tool</sub>
132
138
 
133
- - Runs a query through NCBI ESpell and returns `original`, `corrected`, and `hasSuggestion`
139
+ - 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`)
140
+ - 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
141
  - A blank or whitespace-only query is rejected rather than sent upstream
135
142
 
136
143
  ---
@@ -139,13 +146,14 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
139
146
 
140
147
  - Looks up MeSH descriptors by name or free-text term, pinning the exact-heading match to the top of the first page
141
148
  - 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
149
+ - `maxResults` up to 50 (`limit` also accepted) with offset pagination via `nextOffset`; `totalCount` reports the upstream match count
143
150
 
144
151
  ---
145
152
 
146
153
  ### `pubmed_lookup_citation` <sub>tool</sub>
147
154
 
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
155
+ - Match on journal, year, volume, first page, and/or author — journal or year required, more fields for better precision
156
+ - `citations` takes an array of up to 25 or a single citation object; `citation` is accepted for it
149
157
  - Pipes and line breaks are rejected at the schema (ECitMatch's wire format is pipe-delimited); the free-form `key` label is exempt
150
158
  - Explicit `matched`, `not_found`, and `ambiguous` statuses with recovery detail
151
159
 
@@ -155,7 +163,8 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
155
163
 
156
164
  - Up to 50 DOIs, PMIDs, or PMCIDs per call, all one type; only PMC-indexed articles resolve
157
165
  - 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
166
+ - 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
167
+ - A zero-padded PMID resolves as the PMID it spells
159
168
 
160
169
  ---
161
170
 
@@ -181,7 +190,7 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
181
190
  PubMed-specific:
182
191
 
183
192
  - Complete NCBI E-utilities integration (ESearch, EFetch, ESummary, ELink, ESpell, EInfo, ECitMatch) plus PMC ID Converter
184
- - Sequential request queue with configurable delay for NCBI rate limit compliance
193
+ - Shared NCBI request queue — paced request starts, capped concurrency, a cooldown that holds every caller after an NCBI 429, and one deadline covering queue wait and retries
185
194
  - NCBI-specific XML parser with `isArray` hints for PubMed's inconsistent XML structure
186
195
  - Hand-rolled citation formatters (APA, MLA, BibTeX, RIS, Vancouver) — zero deps, Workers-compatible
187
196
 
@@ -316,7 +325,7 @@ Key environment variables:
316
325
  | `NCBI_MAX_CONCURRENT` | Max concurrent in-flight NCBI requests | `8` |
317
326
  | `NCBI_MAX_RETRIES` | Retry attempts for failed NCBI requests | 6 |
318
327
  | `NCBI_TIMEOUT_MS` | Per-request HTTP timeout in ms | `30000` |
319
- | `NCBI_TOTAL_DEADLINE_MS` | Total deadline across all retry attempts for one NCBI call, in ms | `60000` |
328
+ | `NCBI_TOTAL_DEADLINE_MS` | Total deadline for one NCBI call — queue wait, retry attempts, and backoff — in ms. A call the queue cannot start before it is rejected at once | `60000` |
320
329
  | `UNPAYWALL_EMAIL` | Contact email for Unpaywall. When set, `pubmed_fetch_fulltext` falls back to Unpaywall open-access copies for non-PMC DOIs | none |
321
330
  | `UNPAYWALL_TIMEOUT_MS` | Per-request HTTP timeout for Unpaywall lookups and content fetches, in ms | `20000` |
322
331
  | `EUROPEPMC_ENABLED` | Enable Europe PMC search tool and the `pubmed_fetch_fulltext` JATS fallback chain. Set `false` to disable all EPMC calls and skip tool registration. | `true` |
@@ -23,6 +23,7 @@ security: true
23
23
 
24
24
  - `@cyanheads/mcp-ts-core` ^0.13.0 → ^0.13.4
25
25
  - `zod` ^4.6.1 → ^4.6.5
26
+ - `vitest` ^5.0.0 → ^5.0.1
26
27
  - `@vitest/coverage-istanbul` ^5.0.0 → ^5.0.1
27
28
  - `fast-check` ^4.9.0 → ^4.10.0
28
29
  - `tsc-alias` ^1.9.4 → ^1.9.5
@@ -0,0 +1,28 @@
1
+ ---
2
+ summary: "Retries NCBI's proxy_stream() backend relays and Europe PMC's empty-envelope and outage responses instead of surfacing them as caller errors, and closes the NCBI request queue on a 429 for every caller at once."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 2.10.15 — 2026-09-22
8
+
9
+ ## Changed
10
+
11
+ - **`pubmed_find_related` declares only `all_providers_failed` in its error contract.** Its handler catches every NCBI, Europe PMC, and OpenAlex failure and reports it inside `data.attempted` or the `coverageFailures` enrichment, so the service reasons it used to spread never reached the caller as `data.reason`.
12
+ - **Tooling and docs:** every service-layer entry in `src/services/error-contracts.ts` carries `thrownBy: 'service'` for mcp-ts-core 0.13.6's `error-contract-unthrown` lint rule; `scripts/lint-mcp.ts` reads its rule options from the `lint` block of `devcheck.config.json`; the README and `.env.example` describe the new queue and deadline, and `.env.example` shows the actual `NCBI_MAX_RETRIES` default (6); `framework-skills/` synced to mcp-ts-core 0.13.6.
13
+
14
+ ## Fixed
15
+
16
+ - **EFetch's `proxy_stream()` backend relays now retry as `ncbi_unreachable`** instead of a bare HTTP 400 — the same envelope NCBI's backend proxy prefixes onto a relayed 502 page or a "Failed to connect to PubOne service" message. [#155](https://github.com/cyanheads/pubmed-mcp-server/issues/155)
17
+ - **An all-invalid EFetch ID list (`id=00000000`, `pubmed`/`pmc` only) now resolves as the empty set NCBI returns for a well-formed but unknown ID**, instead of an `ID list is empty!` error. [#155](https://github.com/cyanheads/pubmed-mcp-server/issues/155)
18
+ - **The NCBI request queue is rebuilt on the framework pacer**, closing one shared cooldown gate for every queued caller on an NCBI 429 instead of each caller retrying independently into the same throttle. The gate holds one second, doubles per consecutive 429 up to 15 seconds, and resets on the first success; a `Retry-After` is honored within the same ceiling. Each retry attempt now re-queues and re-paces under one deadline covering queue wait, attempts, and backoff; a call that cannot start in time sheds as `queue_full` with `retryAfter`. [#157](https://github.com/cyanheads/pubmed-mcp-server/issues/157)
19
+ - **A Europe PMC `/search` 404 now retries and ends as `ServiceUnavailable` with `europepmc_unreachable`** instead of a bare `NotFound` — a failed search is an outage, not "no match"; a genuine zero-hit query stays HTTP 200. A 500, 502, or 503 ends the same way. Only a `ServiceUnavailable` now carries that reason: a Europe PMC call whose retries run out on a `Timeout` (a 504) or a `RateLimited` (a 429) keeps its code with no reason, and a 429 keeps its `retryAfter`. [#152](https://github.com/cyanheads/pubmed-mcp-server/issues/152)
20
+ - **Europe PMC's intermittent empty `{version}` envelope now retries as transient upstream noise** instead of failing fast as invalid input; one that persists through every attempt ends as retryable `europepmc_unreachable`. A `sort` with a key outside the four documented fields or without an `asc`/`desc` direction — each key of a comma-separated sort checked on its own — still fails fast as `europepmc_invalid_input` naming the sort. A non-default `cursorMark` whose envelope persists through the full retry budget ends as `europepmc_invalid_input` naming the cursor. The `pubmed_europepmc_search` `sort` description now covers multi-key sorts and undocumented fields. [#159](https://github.com/cyanheads/pubmed-mcp-server/issues/159)
21
+
22
+ ## Dependencies
23
+
24
+ - `@cyanheads/mcp-ts-core` ^0.13.4 → ^0.13.6
25
+ - `defuddle` ^0.19.3 → ^0.19.4
26
+ - `@biomejs/biome` ^2.5.13 → ^2.5.14
27
+ - `@types/node` ^26.5.1 → ^26.6.2
28
+ - `fast-check` ^4.10.0 → ^4.10.2
@@ -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)
@@ -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,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';
@@ -24,33 +27,38 @@ export declare const convertIdsTool: import("@cyanheads/mcp-ts-core").ToolDefini
24
27
  }, z.core.$strip>, readonly [{
25
28
  readonly reason: 'queue_full';
26
29
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.RateLimited;
27
- readonly when: 'Local NCBI request queue is at capacity.';
28
- readonly recovery: 'Retry after 1-2 seconds; the request queue hit the NCBI rate limit.';
30
+ readonly when: 'The local NCBI request queue shed the call — the queue is full, or the call cannot start before its total deadline (for example behind the cooldown that follows an NCBI 429).';
31
+ readonly recovery: 'Wait the number of seconds in `retryAfter`, then retry; the NCBI request queue is saturated or cooling down after a rate limit.';
29
32
  readonly retryable: true;
33
+ readonly thrownBy: 'service';
30
34
  }, {
31
35
  readonly reason: 'ncbi_unreachable';
32
36
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ServiceUnavailable;
33
37
  readonly when: 'NCBI E-utilities is unreachable after all retry attempts.';
34
38
  readonly recovery: 'Retry after a brief delay; NCBI was unreachable across all retry attempts.';
35
39
  readonly retryable: true;
40
+ readonly thrownBy: 'service';
36
41
  }, {
37
42
  readonly reason: 'ncbi_deadline_exceeded';
38
43
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.Timeout;
39
44
  readonly when: 'Total request deadline expired before NCBI returned a response.';
40
45
  readonly recovery: 'Reduce batch size or retry; NCBI may be under temporary load.';
41
46
  readonly retryable: true;
47
+ readonly thrownBy: 'service';
42
48
  }, {
43
49
  readonly reason: 'ncbi_invalid_response';
44
50
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.SerializationError;
45
51
  readonly when: 'NCBI returned a body that could not be parsed (invalid XML/JSON).';
46
52
  readonly recovery: 'Retry the request; NCBI returned a malformed response that could not be parsed.';
47
53
  readonly retryable: true;
54
+ readonly thrownBy: 'service';
48
55
  }, {
49
56
  readonly reason: 'ncbi_resource_not_found';
50
57
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
51
58
  readonly when: 'NCBI returned a structured "not found" error for the requested ID(s).';
52
59
  readonly recovery: 'Verify the ID exists in PubMed; the resource was not found in NCBI and retrying will not help.';
53
60
  readonly retryable: false;
61
+ readonly thrownBy: 'service';
54
62
  }, {
55
63
  readonly reason: 'malformed_id';
56
64
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
@@ -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';
@@ -107,33 +109,38 @@ export declare const fetchArticlesTool: import("@cyanheads/mcp-ts-core").ToolDef
107
109
  }, z.core.$strip>, readonly [{
108
110
  readonly reason: 'queue_full';
109
111
  readonly code: JsonRpcErrorCode.RateLimited;
110
- readonly when: 'Local NCBI request queue is at capacity.';
111
- readonly recovery: 'Retry after 1-2 seconds; the request queue hit the NCBI rate limit.';
112
+ readonly when: 'The local NCBI request queue shed the call — the queue is full, or the call cannot start before its total deadline (for example behind the cooldown that follows an NCBI 429).';
113
+ readonly recovery: 'Wait the number of seconds in `retryAfter`, then retry; the NCBI request queue is saturated or cooling down after a rate limit.';
112
114
  readonly retryable: true;
115
+ readonly thrownBy: 'service';
113
116
  }, {
114
117
  readonly reason: 'ncbi_unreachable';
115
118
  readonly code: JsonRpcErrorCode.ServiceUnavailable;
116
119
  readonly when: 'NCBI E-utilities is unreachable after all retry attempts.';
117
120
  readonly recovery: 'Retry after a brief delay; NCBI was unreachable across all retry attempts.';
118
121
  readonly retryable: true;
122
+ readonly thrownBy: 'service';
119
123
  }, {
120
124
  readonly reason: 'ncbi_deadline_exceeded';
121
125
  readonly code: JsonRpcErrorCode.Timeout;
122
126
  readonly when: 'Total request deadline expired before NCBI returned a response.';
123
127
  readonly recovery: 'Reduce batch size or retry; NCBI may be under temporary load.';
124
128
  readonly retryable: true;
129
+ readonly thrownBy: 'service';
125
130
  }, {
126
131
  readonly reason: 'ncbi_invalid_response';
127
132
  readonly code: JsonRpcErrorCode.SerializationError;
128
133
  readonly when: 'NCBI returned a body that could not be parsed (invalid XML/JSON).';
129
134
  readonly recovery: 'Retry the request; NCBI returned a malformed response that could not be parsed.';
130
135
  readonly retryable: true;
136
+ readonly thrownBy: 'service';
131
137
  }, {
132
138
  readonly reason: 'ncbi_resource_not_found';
133
139
  readonly code: JsonRpcErrorCode.NotFound;
134
140
  readonly when: 'NCBI returned a structured "not found" error for the requested ID(s).';
135
141
  readonly recovery: 'Verify the ID exists in PubMed; the resource was not found in NCBI and retrying will not help.';
136
142
  readonly retryable: false;
143
+ readonly thrownBy: 'service';
137
144
  }, {
138
145
  readonly reason: 'invalid_efetch_response';
139
146
  readonly code: JsonRpcErrorCode.SerializationError;
@@ -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"}