@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.
- package/AGENTS.md +4 -4
- package/CLAUDE.md +4 -4
- package/README.md +24 -15
- package/changelog/2.10.x/2.10.14.md +1 -0
- package/changelog/2.10.x/2.10.15.md +28 -0
- package/changelog/2.10.x/2.10.16.md +26 -0
- package/dist/mcp-server/tools/definitions/_schemas.d.ts +11 -1
- package/dist/mcp-server/tools/definitions/_schemas.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_schemas.js +13 -1
- package/dist/mcp-server/tools/definitions/_schemas.js.map +1 -1
- package/dist/mcp-server/tools/definitions/convert-ids.tool.d.ts +10 -2
- package/dist/mcp-server/tools/definitions/convert-ids.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/convert-ids.tool.js +41 -9
- package/dist/mcp-server/tools/definitions/convert-ids.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts +10 -3
- package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-articles.tool.js +12 -4
- package/dist/mcp-server/tools/definitions/fetch-articles.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts +16 -5
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js +35 -12
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/find-related.tool.d.ts +7 -63
- package/dist/mcp-server/tools/definitions/find-related.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/find-related.tool.js +42 -25
- package/dist/mcp-server/tools/definitions/find-related.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/format-citations.tool.d.ts +10 -3
- package/dist/mcp-server/tools/definitions/format-citations.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/format-citations.tool.js +12 -4
- package/dist/mcp-server/tools/definitions/format-citations.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-citation.tool.d.ts +18 -4
- package/dist/mcp-server/tools/definitions/lookup-citation.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-citation.tool.js +62 -41
- package/dist/mcp-server/tools/definitions/lookup-citation.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-mesh.tool.d.ts +10 -5
- package/dist/mcp-server/tools/definitions/lookup-mesh.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-mesh.tool.js +3 -1
- package/dist/mcp-server/tools/definitions/lookup-mesh.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-fetch.tool.d.ts +6 -3
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-fetch.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts +11 -5
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js +20 -11
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-articles.tool.d.ts +16 -5
- package/dist/mcp-server/tools/definitions/search-articles.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-articles.tool.js +71 -11
- package/dist/mcp-server/tools/definitions/search-articles.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/spell-check.tool.d.ts +12 -6
- package/dist/mcp-server/tools/definitions/spell-check.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/spell-check.tool.js +4 -3
- package/dist/mcp-server/tools/definitions/spell-check.tool.js.map +1 -1
- package/dist/services/error-contracts.d.ts +32 -15
- package/dist/services/error-contracts.d.ts.map +1 -1
- package/dist/services/error-contracts.js +32 -15
- package/dist/services/error-contracts.js.map +1 -1
- package/dist/services/europe-pmc/api-client.d.ts +24 -9
- package/dist/services/europe-pmc/api-client.d.ts.map +1 -1
- package/dist/services/europe-pmc/api-client.js +47 -18
- package/dist/services/europe-pmc/api-client.js.map +1 -1
- package/dist/services/europe-pmc/europe-pmc-service.d.ts +22 -1
- package/dist/services/europe-pmc/europe-pmc-service.d.ts.map +1 -1
- package/dist/services/europe-pmc/europe-pmc-service.js +102 -43
- package/dist/services/europe-pmc/europe-pmc-service.js.map +1 -1
- package/dist/services/ncbi/ncbi-service.d.ts +18 -24
- package/dist/services/ncbi/ncbi-service.d.ts.map +1 -1
- package/dist/services/ncbi/ncbi-service.js +111 -120
- package/dist/services/ncbi/ncbi-service.js.map +1 -1
- package/dist/services/ncbi/request-queue.d.ts +22 -30
- package/dist/services/ncbi/request-queue.d.ts.map +1 -1
- package/dist/services/ncbi/request-queue.js +29 -128
- package/dist/services/ncbi/request-queue.js.map +1 -1
- package/dist/services/ncbi/response-handler.d.ts +14 -1
- package/dist/services/ncbi/response-handler.d.ts.map +1 -1
- package/dist/services/ncbi/response-handler.js +65 -9
- package/dist/services/ncbi/response-handler.js.map +1 -1
- package/package.json +6 -6
- 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.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
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 #
|
|
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.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
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 #
|
|
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
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/pubmed-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/pubmed-mcp-server) [](https://www.typescriptlang.org/) [](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
|
|
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
|
-
-
|
|
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 —
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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` |
|
|
@@ -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
|
|
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
|
|
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: '
|
|
28
|
-
readonly recovery: '
|
|
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
|
|
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
|
-
|
|
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
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
111
|
-
readonly recovery: '
|
|
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
|
|
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"}
|