@cyanheads/pubmed-mcp-server 2.10.15 → 2.10.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/README.md +26 -15
- package/changelog/2.10.x/2.10.16.md +26 -0
- package/changelog/2.10.x/2.10.17.md +19 -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/_text.d.ts +20 -3
- package/dist/mcp-server/tools/definitions/_text.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_text.js +35 -3
- package/dist/mcp-server/tools/definitions/_text.js.map +1 -1
- package/dist/mcp-server/tools/definitions/convert-ids.tool.d.ts +3 -0
- 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 +3 -1
- 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 +25 -31
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js +373 -131
- 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 -3
- package/dist/mcp-server/tools/definitions/find-related.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/find-related.tool.js +38 -21
- package/dist/mcp-server/tools/definitions/find-related.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/format-citations.tool.d.ts +3 -1
- 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 +11 -2
- 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 +3 -3
- 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-search.tool.d.ts +5 -2
- 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 +19 -10
- 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 +9 -3
- 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 +5 -4
- 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 +7 -5
- package/dist/services/error-contracts.d.ts.map +1 -1
- package/dist/services/error-contracts.js +7 -5
- package/dist/services/error-contracts.js.map +1 -1
- package/dist/services/europe-pmc/api-client.d.ts +14 -11
- package/dist/services/europe-pmc/api-client.d.ts.map +1 -1
- package/dist/services/europe-pmc/api-client.js +21 -18
- package/dist/services/europe-pmc/api-client.js.map +1 -1
- package/dist/services/europe-pmc/europe-pmc-service.d.ts +2 -0
- package/dist/services/europe-pmc/europe-pmc-service.d.ts.map +1 -1
- package/dist/services/europe-pmc/europe-pmc-service.js +6 -2
- package/dist/services/europe-pmc/europe-pmc-service.js.map +1 -1
- package/dist/services/ncbi/parsing/pmc-article-parser.d.ts +8 -0
- package/dist/services/ncbi/parsing/pmc-article-parser.d.ts.map +1 -1
- package/dist/services/ncbi/parsing/pmc-article-parser.js +159 -16
- package/dist/services/ncbi/parsing/pmc-article-parser.js.map +1 -1
- package/dist/services/unpaywall/types.d.ts +16 -2
- package/dist/services/unpaywall/types.d.ts.map +1 -1
- package/dist/services/unpaywall/unpaywall-service.d.ts +4 -3
- package/dist/services/unpaywall/unpaywall-service.d.ts.map +1 -1
- package/dist/services/unpaywall/unpaywall-service.js +15 -4
- package/dist/services/unpaywall/unpaywall-service.js.map +1 -1
- package/package.json +1 -1
- package/server.json +3 -3
package/AGENTS.md
CHANGED
package/CLAUDE.md
CHANGED
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,20 +89,23 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
|
|
|
86
89
|
|
|
87
90
|
### `pubmed_fetch_fulltext` <sub>tool</sub>
|
|
88
91
|
|
|
89
|
-
- Exactly one of `pmcids`, `pmids`, or `dois` (one id per element), up to 10 per request
|
|
92
|
+
- Exactly one of `pmcids`, `pmids`, or `dois` (one id per element), up to 10 per request; a zero-padded PMID or PMC ID resolves as the ID it spells, DOIs match case-insensitively, and PMC IDs match with or without the `PMC` prefix in any case. Each record is fetched once however many spellings name it; `unavailable[].id` keeps your PMID or DOI spelling, and reports a PMC ID as `PMC<digits>`
|
|
90
93
|
- Three-tier chain: NCBI PMC EFetch → Europe PMC `fullTextXML` (`EUROPEPMC_ENABLED`, default on) → Unpaywall (needs `UNPAYWALL_EMAIL`); `viaSource` names which tier served each article
|
|
91
94
|
- Preprints, patents, and Agricola records have metadata via `pubmed_europepmc_search` but no full text through this chain — Europe PMC's `fullTextXML` is PMC-keyed
|
|
92
|
-
- `source: "pmc"` returns structured sections plus `tables[]` (cells, caption, label, footnotes) and `assets[]` (figures and supplementary material, with `[Figure: <label>]` markers left in the body); `source: "unpaywall"` returns a best-effort body with `contentFormat` (`html-markdown` / `pdf-text`)
|
|
95
|
+
- `source: "pmc"` returns structured sections plus `tables[]` (cells, caption, label, footnotes) and `assets[]` (figures and supplementary material, with `[Figure: <label>]` markers left in the body, and the file pointer read through `<alternatives>` when a figure offers several formats); `source: "unpaywall"` returns a best-effort body with `contentFormat` (`html-markdown` / `pdf-text`), a `title` from Unpaywall's record (else the Europe PMC record, else the HTML page), and `journalName` / `year` when Unpaywall has them
|
|
96
|
+
- A titled list, definition list, or boxed text that sits in the body outside any section, or is the only content of an untitled section, such as an abbreviations list, becomes a section under its own title, and a figure or table inside it names that section; a section with no title is headed `untitled section`, the same name the `truncation` ledger gives it, and section titles in headings and ledger lines are Markdown-escaped
|
|
93
97
|
- Unavailable entries carry a typed `reason` (`not-found`, `no-doi`, `doi-lookup-failed`, `no-oa`, `service-error`, …), `idType`, `triedTiers` (per-tier outcome in execution order), and `unqueriedTiers` when an unconfigured tier could have served the id
|
|
94
|
-
- Filters and budgets: `sections` (case-insensitive title match), `maxSections`, `includeTables`, `includeAssets`, `maxCharacters`, `maxCharactersPerSection`, `overflowMode` (`truncate` / `outline`), and `maxResponseCharacters`, which defers whole articles past the ceiling to `deferred.ids`; a `truncation` object reports what was shortened or omitted
|
|
98
|
+
- Filters and budgets: `sections` (case-insensitive title match), `maxSections`, `includeTables`, `includeAssets`, `maxCharacters`, `maxCharactersPerSection`, `overflowMode` (`truncate` / `outline`), and `maxResponseCharacters`, which defers whole articles past the ceiling to `deferred.ids`; a `truncation` object reports what was shortened or omitted, per section and subsection
|
|
99
|
+
- A budget cut ends at a word boundary; `truncate` drops every section and subsection past the cut, while `outline` keeps each heading and marks one the budget left empty
|
|
95
100
|
|
|
96
101
|
---
|
|
97
102
|
|
|
98
103
|
### `pubmed_europepmc_search` <sub>tool</sub>
|
|
99
104
|
|
|
100
105
|
- Reaches records PubMed can't: preprints (`PPR`), patents (`PAT`), Agricola (`AGR`), alongside `MED` and `PMC`; default `sources` is `["MED", "PMC", "PPR"]`
|
|
101
|
-
- Cursor pagination via `cursorMark` — `*` for the first page, then `nextCursorMark`
|
|
106
|
+
- Cursor pagination via `cursorMark` — `*` for the first page, then `nextCursorMark`; `pageSize` up to 100, with `max_results` and `limit` accepted for it
|
|
102
107
|
- Hits carry `source` plus `pmid` / `pmcId` / `doi` when known; `abstractSnippet` is capped at 400 characters, with `abstractTruncated` flagging the cut
|
|
108
|
+
- `totalCount` reports the full hit count, stated in the header beside the page; `searchUrl` opens the same source-filtered query on europepmc.org
|
|
103
109
|
- Not registered when `EUROPEPMC_ENABLED=false`
|
|
104
110
|
|
|
105
111
|
---
|
|
@@ -117,7 +123,7 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
|
|
|
117
123
|
- APA 7th, MLA 9th, BibTeX, RIS, Vancouver (ICMJE/NLM); several styles per article in one call, up to 50 articles
|
|
118
124
|
- Bookshelf chapters and books cite in each style's edited-book form; articles without a page range cite by electronic locator in each style's convention
|
|
119
125
|
- Hand-rolled formatters — zero dependencies, Workers-compatible
|
|
120
|
-
- Reports formatted counts and unavailable PMIDs
|
|
126
|
+
- Reports formatted counts and unavailable PMIDs; `ids` is accepted for `pmids`, and a zero-padded PMID resolves as the PMID it spells
|
|
121
127
|
|
|
122
128
|
---
|
|
123
129
|
|
|
@@ -125,12 +131,15 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
|
|
|
125
131
|
|
|
126
132
|
- `similar`, `cited_by`, or `references` for a PMID, in NCBI relevance order, enriched with title, authors, date, and source (or Bookshelf book title and publisher)
|
|
127
133
|
- Falls back to Europe PMC, then OpenAlex, when NCBI can't answer; the response names the provider. Fails with a typed `all_providers_failed` error rather than an empty result
|
|
134
|
+
- `maxResults` up to 50 (`limit` also accepted) with offset pagination; `totalCount` reports the full match count, stated in the header beside the page
|
|
135
|
+
- A zero-padded source PMID resolves as the PMID it spells and is never listed among its own related articles
|
|
128
136
|
|
|
129
137
|
---
|
|
130
138
|
|
|
131
139
|
### `pubmed_spell_check` <sub>tool</sub>
|
|
132
140
|
|
|
133
|
-
- Runs a query through NCBI ESpell and returns `original`, `corrected`, and `hasSuggestion`
|
|
141
|
+
- Runs a PubMed query through NCBI ESpell and returns `original`, `corrected`, and `hasSuggestion`; every misspelled token is corrected in one call (`alzhiemer diseese treatmnt outcomse` → `alzheimer disease treatment outcomes`)
|
|
142
|
+
- Reach for it after a zero-hit or thin `pubmed_search_articles` result, or when a drug, gene, disease, or author name may be misspelled, then re-run the search with `corrected`
|
|
134
143
|
- A blank or whitespace-only query is rejected rather than sent upstream
|
|
135
144
|
|
|
136
145
|
---
|
|
@@ -139,13 +148,14 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
|
|
|
139
148
|
|
|
140
149
|
- Looks up MeSH descriptors by name or free-text term, pinning the exact-heading match to the top of the first page
|
|
141
150
|
- Records carry `meshId` (DescriptorUI), `entrezUid`, and, with `includeDetails` (default on), tree numbers, scope notes, and entry terms
|
|
142
|
-
- `maxResults` up to 50 with offset pagination via `nextOffset`; `totalCount` reports the upstream match count
|
|
151
|
+
- `maxResults` up to 50 (`limit` also accepted) with offset pagination via `nextOffset`; `totalCount` reports the upstream match count
|
|
143
152
|
|
|
144
153
|
---
|
|
145
154
|
|
|
146
155
|
### `pubmed_lookup_citation` <sub>tool</sub>
|
|
147
156
|
|
|
148
|
-
- Match on journal, year, volume, first page, and/or author —
|
|
157
|
+
- Match on journal, year, volume, first page, and/or author — journal or year required, more fields for better precision
|
|
158
|
+
- `citations` takes an array of up to 25 or a single citation object; `citation` is accepted for it
|
|
149
159
|
- Pipes and line breaks are rejected at the schema (ECitMatch's wire format is pipe-delimited); the free-form `key` label is exempt
|
|
150
160
|
- Explicit `matched`, `not_found`, and `ambiguous` statuses with recovery detail
|
|
151
161
|
|
|
@@ -155,7 +165,8 @@ The biomedical literature via NCBI's E-utilities, PubMed Central, and Europe PMC
|
|
|
155
165
|
|
|
156
166
|
- Up to 50 DOIs, PMIDs, or PMCIDs per call, all one type; only PMC-indexed articles resolve
|
|
157
167
|
- One id per element — a packed `"23193287,37952131"` is rejected rather than expanded
|
|
158
|
-
-
|
|
168
|
+
- One success/error row per submitted element, in order, with `requestedId` exactly as sent — repeats, a bare-digit PMCID, and a DOI's casing included; a partial batch never fails as a whole
|
|
169
|
+
- A zero-padded PMID resolves as the PMID it spells
|
|
159
170
|
|
|
160
171
|
---
|
|
161
172
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Normalizes a zero-padded PMID to the PMID it spells across every ID-diffing tool, and adds snake_case/synonym parameter aliases (ids, limit, max_results, citation) across the tool surface."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 2.10.16 — 2026-09-22
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`inputAliases` accept snake_case and near-synonym parameter names**, rewritten to the canonical key before the schema parses and never advertised in `tools/list`: `ids` for `pmids` (`pubmed_fetch_articles`, `pubmed_format_citations`), `limit` for `maxResults`/`pageSize` (`pubmed_search_articles`, `pubmed_find_related`, `pubmed_lookup_mesh`, `pubmed_europepmc_search`), `max_results` for `pageSize` (`pubmed_europepmc_search`), and `citation` for `citations` (`pubmed_lookup_citation`). [#156](https://github.com/cyanheads/pubmed-mcp-server/issues/156)
|
|
12
|
+
|
|
13
|
+
## Changed
|
|
14
|
+
|
|
15
|
+
- **`pubmed_search_articles`, `pubmed_find_related`, and `pubmed_europepmc_search` state `totalCount` in the header** (`Returned: 3 of 2924`) instead of the enrichment trailer. It moves from `enrichment` into each tool's `output`, so `structuredContent.totalCount` keeps its value and type. [#147](https://github.com/cyanheads/pubmed-mcp-server/issues/147)
|
|
16
|
+
- **`docType` is rendered only when it marks something other than an ordinary journal article (`citation`)** in `pubmed_search_articles` and `pubmed_find_related` summaries. [#146](https://github.com/cyanheads/pubmed-mcp-server/issues/146)
|
|
17
|
+
- **`pubmed_lookup_citation`'s `citations` accepts a single citation object or an array of up to 25**, with a named error for a value that fits neither shape. [#156](https://github.com/cyanheads/pubmed-mcp-server/issues/156)
|
|
18
|
+
- **`pubmed_europepmc_search`'s query description documents that identifier tokens may be quoted inside its source-filter wrapper**, and that a PubMed-indexed article resolves under `SRC:MED`, not `SRC:PMC`. [#149](https://github.com/cyanheads/pubmed-mcp-server/issues/149)
|
|
19
|
+
- **`pubmed_spell_check`'s description names PubMed and ESpell and states its role as the recovery step after a zero-hit or thin search**, with a worked correction example. [#151](https://github.com/cyanheads/pubmed-mcp-server/issues/151)
|
|
20
|
+
|
|
21
|
+
## Fixed
|
|
22
|
+
|
|
23
|
+
- **A zero-padded PMID (`00000001`) is no longer both returned and reported unavailable in the same response.** NCBI reads it as the PMID it spells; `pubmed_fetch_articles`, `pubmed_fetch_fulltext`, `pubmed_format_citations`, `pubmed_find_related`, and `pubmed_convert_ids` now send and compare the canonical form while reporting the caller's own spelling. [#161](https://github.com/cyanheads/pubmed-mcp-server/issues/161)
|
|
24
|
+
- **`pubmed_convert_ids` gives every submitted element its own record, in submission order, under `requestedId` exactly as sent** — a repeated ID, including one DOI in two casings, gets a record per element instead of one shared record; a bare-digit PMCID is reported as sent rather than as `PMC<digits>`; and an element the converter returns nothing for gets an error row instead of disappearing. [#165](https://github.com/cyanheads/pubmed-mcp-server/issues/165)
|
|
25
|
+
- **`pubmed_search_articles`'s blank-query guard now catches a bare field tag (`[pdat]`) or empty parentheses**, which previously passed the check: `[pdat]` reached NCBI as a zero-hit search pointing at `pubmed_spell_check`, and `()` drew NCBI's blank-term error, misread as a retryable outage. Only brackets holding a PubMed field tag are stripped for the check, so `[18F]` and `benzo[a]pyrene` are still searched as text. [#145](https://github.com/cyanheads/pubmed-mcp-server/issues/145)
|
|
26
|
+
- **`pubmed_europepmc_search`'s `searchUrl` now carries the same source filter the search actually ran**, instead of opening an unfiltered query with a different hit count. The reported `query` falls back to that same source-filtered query when Europe PMC echoes none. [#150](https://github.com/cyanheads/pubmed-mcp-server/issues/150)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Fixes pubmed_fetch_fulltext body parsing (alternatives figures, titled body-level blocks), word-boundary truncation with per-subsection accounting, Unpaywall title/journal/year, and case-insensitive DOI and zero-padded PMC ID matching."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 2.10.17 — 2026-09-23
|
|
8
|
+
|
|
9
|
+
## Fixed
|
|
10
|
+
|
|
11
|
+
- **`pubmed_fetch_fulltext` reads a figure's or supplementary file's pointer from `<alternatives>`** when it has no direct `<graphic>`/`<media>`, preferring the rendering whose `specific-use` names the web, else the first. Its `href`, and a label or caption hung on that pointer, are no longer lost. [#142](https://github.com/cyanheads/pubmed-mcp-server/issues/142)
|
|
12
|
+
- **Character-budget cuts end at the last word boundary inside the allowance**, in sections and Unpaywall bodies alike; a single token longer than the allowance is still cut at it. In `truncate` mode every subsection past the cut is dropped and counted in `omittedSections`, where only whole top-level sections were before; `outline` mode keeps those headings and marks them in `content[]`. The `truncation` ledger lists a shortened section's `subsections` with their own accounting, and each entry carries its `label`. [#143](https://github.com/cyanheads/pubmed-mcp-server/issues/143)
|
|
13
|
+
- **Unpaywall articles carry a `title`**, from Unpaywall's own DOI record, else the Europe PMC record the chain searched, else — for HTML content only — the page's own title, **plus `journalName` and `year`** when Unpaywall's record has them. [#144](https://github.com/cyanheads/pubmed-mcp-server/issues/144)
|
|
14
|
+
- **A titled list, definition list, or boxed text sitting outside any `<sec>`, or alone inside an untitled one, becomes a section under its own title** instead of an untitled block with no heading to separate it from its neighbors. A figure or table inside a titled box names that section as its `sectionTitle`, so a `sections` filter on the box keeps them. [#148](https://github.com/cyanheads/pubmed-mcp-server/issues/148), [#169](https://github.com/cyanheads/pubmed-mcp-server/issues/169)
|
|
15
|
+
- **Every section gets a heading in `content[]`**: an untitled one is headed `untitled section` and a label-only one by its label, the same name its truncation-ledger line uses, and section titles are Markdown-escaped in both places. [#148](https://github.com/cyanheads/pubmed-mcp-server/issues/148), [#169](https://github.com/cyanheads/pubmed-mcp-server/issues/169)
|
|
16
|
+
- **A `<boxed-text>` caption's title and paragraph render as separate lines** instead of running together. [#169](https://github.com/cyanheads/pubmed-mcp-server/issues/169)
|
|
17
|
+
- **DOI input matches case-insensitively**: every casing of one DOI runs the chain once, and `unavailable[]` reports each casing as submitted. Two different DOIs the ID Converter places on one PMC record also share one chain, and that record is fetched once. [#166](https://github.com/cyanheads/pubmed-mcp-server/issues/166)
|
|
18
|
+
- **`errors[]` no longer declares Unpaywall or Europe PMC failure reasons the handler never throws** — both are folded into each id's `triedTiers` instead of propagated. [#168](https://github.com/cyanheads/pubmed-mcp-server/issues/168)
|
|
19
|
+
- **A zero-padded PMC ID (`PMC0123`) resolves as the record it spells** instead of being returned and reported unavailable in the same response. Every spelling of one PMC ID — with or without the `PMC` prefix, in any case, zero-padded or not — is fetched once and reported in `PMC<digits>` form. [#170](https://github.com/cyanheads/pubmed-mcp-server/issues/170)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Shared Zod schemas reused across tool definitions
|
|
2
|
+
* @fileoverview Shared Zod schemas reused across tool definitions, plus the
|
|
3
|
+
* normalizer for the PMID form {@link pmidStringSchema} accepts.
|
|
3
4
|
* @module src/mcp-server/tools/definitions/_schemas
|
|
4
5
|
*/
|
|
5
6
|
import { z } from '@cyanheads/mcp-ts-core';
|
|
@@ -11,6 +12,15 @@ import { z } from '@cyanheads/mcp-ts-core';
|
|
|
11
12
|
* stray prefixes like "PMID:").
|
|
12
13
|
*/
|
|
13
14
|
export declare const pmidStringSchema: z.ZodString;
|
|
15
|
+
/**
|
|
16
|
+
* Canonical form of a PMID {@link pmidStringSchema} accepted: leading zeros
|
|
17
|
+
* stripped, `0` kept for an all-zero input. NCBI reads `00000001` as PMID 1 and
|
|
18
|
+
* answers with `<PMID>1</PMID>`, so a handler sends this form upstream and
|
|
19
|
+
* compares upstream PMIDs against it — the schema itself stays as advertised,
|
|
20
|
+
* since tool schemas carry no transforms. `0` is still no UID, so an all-zero
|
|
21
|
+
* list keeps NCBI's empty-list answer. (#161)
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizePmid(pmid: string): string;
|
|
14
24
|
/**
|
|
15
25
|
* Zod string schema for a single PMC ID. Digits, with the "PMC" prefix optional
|
|
16
26
|
* and case-insensitive — both forms are accepted upstream.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_schemas.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_schemas.ts"],"names":[],"mappings":"AAAA
|
|
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,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Text helpers shared by the tool definitions: the surrogate-safe
|
|
3
|
-
* character cut
|
|
4
|
-
* Markdown escapes applied to upstream strings
|
|
5
|
-
* one for an inline position, one for a table
|
|
3
|
+
* character cut and the word-boundary cut built on it, which bound returned text
|
|
4
|
+
* to a budget, and the render-time Markdown escapes applied to upstream strings
|
|
5
|
+
* interpolated into `format()` — one for an inline position, one for a table
|
|
6
|
+
* cell.
|
|
6
7
|
* @module src/mcp-server/tools/definitions/_text
|
|
7
8
|
*/
|
|
8
9
|
/**
|
|
@@ -21,6 +22,22 @@
|
|
|
21
22
|
* than assume the allowance was spent exactly. (#93)
|
|
22
23
|
*/
|
|
23
24
|
export declare function sliceCodeUnits(text: string, limit: number): string;
|
|
25
|
+
/**
|
|
26
|
+
* Take at most `limit` UTF-16 code units from `text`, ending at the last word
|
|
27
|
+
* boundary inside the allowance rather than partway through a word.
|
|
28
|
+
*
|
|
29
|
+
* The cut is {@link sliceCodeUnits}' cut, backed off past the partial word it
|
|
30
|
+
* would end on — unless the character after it is whitespace, in which case
|
|
31
|
+
* the last word is already whole — with the whitespace before that word
|
|
32
|
+
* trimmed. A cut is therefore never mid-word and never ends in whitespace.
|
|
33
|
+
* `limit` stays a ceiling: the result can come back several units short of it,
|
|
34
|
+
* so callers reporting character counts measure the returned string. (#143)
|
|
35
|
+
*
|
|
36
|
+
* An allowance holding no boundary at all — one token longer than the
|
|
37
|
+
* allowance, such as a sequence or a URL — is cut at the allowance instead:
|
|
38
|
+
* backing off would return nothing, dropping a field that has text to give.
|
|
39
|
+
*/
|
|
40
|
+
export declare function sliceAtWordBoundary(text: string, limit: number): string;
|
|
24
41
|
/**
|
|
25
42
|
* Escape the Markdown-significant characters in an upstream string that is
|
|
26
43
|
* about to be interpolated *inline* into a `format()` line — a `### `/`#### `
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_text.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_text.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"_text.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_text.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlE;AAQD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAUvE;AA2CD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAazD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Text helpers shared by the tool definitions: the surrogate-safe
|
|
3
|
-
* character cut
|
|
4
|
-
* Markdown escapes applied to upstream strings
|
|
5
|
-
* one for an inline position, one for a table
|
|
3
|
+
* character cut and the word-boundary cut built on it, which bound returned text
|
|
4
|
+
* to a budget, and the render-time Markdown escapes applied to upstream strings
|
|
5
|
+
* interpolated into `format()` — one for an inline position, one for a table
|
|
6
|
+
* cell.
|
|
6
7
|
* @module src/mcp-server/tools/definitions/_text
|
|
7
8
|
*/
|
|
8
9
|
const HIGH_SURROGATE_FIRST = 0xd800;
|
|
@@ -31,6 +32,37 @@ export function sliceCodeUnits(text, limit) {
|
|
|
31
32
|
const splitsPair = last >= HIGH_SURROGATE_FIRST && last <= HIGH_SURROGATE_LAST;
|
|
32
33
|
return text.slice(0, splitsPair ? limit - 1 : limit);
|
|
33
34
|
}
|
|
35
|
+
/** A whitespace character — the boundary a word-boundary cut backs off to. */
|
|
36
|
+
const WHITESPACE_RE = /\s/;
|
|
37
|
+
const isWhitespace = (text, index) => WHITESPACE_RE.test(text.charAt(index));
|
|
38
|
+
/**
|
|
39
|
+
* Take at most `limit` UTF-16 code units from `text`, ending at the last word
|
|
40
|
+
* boundary inside the allowance rather than partway through a word.
|
|
41
|
+
*
|
|
42
|
+
* The cut is {@link sliceCodeUnits}' cut, backed off past the partial word it
|
|
43
|
+
* would end on — unless the character after it is whitespace, in which case
|
|
44
|
+
* the last word is already whole — with the whitespace before that word
|
|
45
|
+
* trimmed. A cut is therefore never mid-word and never ends in whitespace.
|
|
46
|
+
* `limit` stays a ceiling: the result can come back several units short of it,
|
|
47
|
+
* so callers reporting character counts measure the returned string. (#143)
|
|
48
|
+
*
|
|
49
|
+
* An allowance holding no boundary at all — one token longer than the
|
|
50
|
+
* allowance, such as a sequence or a URL — is cut at the allowance instead:
|
|
51
|
+
* backing off would return nothing, dropping a field that has text to give.
|
|
52
|
+
*/
|
|
53
|
+
export function sliceAtWordBoundary(text, limit) {
|
|
54
|
+
const cut = sliceCodeUnits(text, limit);
|
|
55
|
+
if (cut.length === text.length)
|
|
56
|
+
return cut;
|
|
57
|
+
let end = cut.length;
|
|
58
|
+
if (!isWhitespace(text, end)) {
|
|
59
|
+
while (end > 0 && !isWhitespace(cut, end - 1))
|
|
60
|
+
end -= 1;
|
|
61
|
+
}
|
|
62
|
+
while (end > 0 && isWhitespace(cut, end - 1))
|
|
63
|
+
end -= 1;
|
|
64
|
+
return end > 0 ? cut.slice(0, end) : cut.trimEnd();
|
|
65
|
+
}
|
|
34
66
|
/** Any line break, including the Unicode line and paragraph separators. */
|
|
35
67
|
const LINE_BREAK_RE = /[\r\n\u2028\u2029]+/g;
|
|
36
68
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_text.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_text.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"_text.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_text.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Article ID conversion tool. Converts between DOI, PMID, and PMCID
|
|
3
3
|
* using the NCBI PMC ID Converter API for deterministic, batch-friendly resolution.
|
|
4
|
+
* Every submitted element gets its own record, in submission order, with the
|
|
5
|
+
* caller's own spelling as `requestedId`; a zero-padded PMID is converted as the
|
|
6
|
+
* PMID it spells.
|
|
4
7
|
* @module src/mcp-server/tools/definitions/convert-ids.tool
|
|
5
8
|
*/
|
|
6
9
|
import { z } from '@cyanheads/mcp-ts-core';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convert-ids.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/convert-ids.tool.ts"],"names":[],"mappings":"AAAA
|
|
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';
|
|
@@ -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"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview PubMed fetch tool. Fetches full article metadata by PubMed IDs,
|
|
3
|
-
* including abstracts, authors, journal info, and MeSH terms.
|
|
3
|
+
* including abstracts, authors, journal info, and MeSH terms. A zero-padded
|
|
4
|
+
* PMID is fetched and matched as the PMID it spells; `ids` is accepted as an
|
|
5
|
+
* alias for `pmids`.
|
|
4
6
|
* @module src/mcp-server/tools/definitions/fetch-articles.tool
|
|
5
7
|
*/
|
|
6
8
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
@@ -10,7 +12,7 @@ import { getNcbiService } from '../../../services/ncbi/ncbi-service.js';
|
|
|
10
12
|
import { parseArticleSet } from '../../../services/ncbi/parsing/article-parser.js';
|
|
11
13
|
import { fitWholeItems } from './_budget.js';
|
|
12
14
|
import { conceptMeta, EDAM_DATA_RETRIEVAL, EDAM_PUBMED_ID, SCHEMA_SCHOLARLY_ARTICLE, } from './_concepts.js';
|
|
13
|
-
import { pmidStringSchema } from './_schemas.js';
|
|
15
|
+
import { normalizePmid, pmidStringSchema } from './_schemas.js';
|
|
14
16
|
import { escapeMarkdownInline } from './_text.js';
|
|
15
17
|
const AuthorSchema = z
|
|
16
18
|
.object({
|
|
@@ -225,6 +227,8 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
|
|
|
225
227
|
recovery: 'Retry once; if it persists, NCBI returned malformed data — try fewer PMIDs at once.',
|
|
226
228
|
},
|
|
227
229
|
],
|
|
230
|
+
// Never advertised; rewritten to the canonical key before the schema parses. (#156)
|
|
231
|
+
inputAliases: { ids: 'pmids' },
|
|
228
232
|
input: z.object({
|
|
229
233
|
pmids: z.array(pmidStringSchema).min(1).max(200).describe('PubMed IDs to fetch'),
|
|
230
234
|
includeMesh: z.boolean().default(true).describe('Include MeSH terms'),
|
|
@@ -263,7 +267,11 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
|
|
|
263
267
|
},
|
|
264
268
|
async handler(input, ctx) {
|
|
265
269
|
ctx.log.info('Executing pubmed_fetch', { pmidCount: input.pmids.length });
|
|
266
|
-
|
|
270
|
+
// NCBI reads `00000001` as PMID 1 and answers with `<PMID>1</PMID>`, so the
|
|
271
|
+
// request and the unavailability diff below both use the canonical form;
|
|
272
|
+
// `unavailablePmids` still reports the caller's own spelling. (#161)
|
|
273
|
+
const requested = [...new Set(input.pmids.map(normalizePmid))];
|
|
274
|
+
const xmlData = await getNcbiService().eFetch({ db: 'pubmed', id: requested.join(','), retmode: 'xml' }, { retmode: 'xml', usePost: requested.length >= 100, signal: ctx.signal });
|
|
267
275
|
if (!xmlData || !('PubmedArticleSet' in xmlData)) {
|
|
268
276
|
throw ctx.fail('invalid_efetch_response', 'Invalid EFetch response from NCBI: missing PubmedArticleSet', { requestedPmids: input.pmids.length, ...ctx.recoveryFor('invalid_efetch_response') });
|
|
269
277
|
}
|
|
@@ -281,7 +289,7 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
|
|
|
281
289
|
}),
|
|
282
290
|
}));
|
|
283
291
|
const returnedPmids = new Set(articles.map((a) => a.pmid).filter(Boolean));
|
|
284
|
-
const unavailable = input.pmids.filter((id) => !returnedPmids.has(id));
|
|
292
|
+
const unavailable = input.pmids.filter((id) => !returnedPmids.has(normalizePmid(id)));
|
|
285
293
|
// Whole-response budget: fill with complete records in response order and
|
|
286
294
|
// hand the remainder back as PMIDs the caller can re-submit. Without
|
|
287
295
|
// `maxResponseCharacters` nothing is measured and the response is exactly
|