@cyanheads/pubmed-mcp-server 2.10.10 → 2.10.11
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 +10 -3
- package/dist/mcp-server/tools/definitions/_schemas.d.ts +16 -0
- package/dist/mcp-server/tools/definitions/_schemas.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_schemas.js +20 -0
- package/dist/mcp-server/tools/definitions/_schemas.js.map +1 -1
- package/dist/mcp-server/tools/definitions/convert-ids.tool.d.ts +6 -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 +28 -3
- package/dist/mcp-server/tools/definitions/convert-ids.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts +27 -0
- package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-articles.tool.js +166 -24
- package/dist/mcp-server/tools/definitions/fetch-articles.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts +1 -0
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js +10 -6
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/find-related.tool.d.ts +4 -0
- package/dist/mcp-server/tools/definitions/find-related.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/find-related.tool.js +40 -4
- package/dist/mcp-server/tools/definitions/find-related.tool.js.map +1 -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 +11 -13
- package/dist/mcp-server/tools/definitions/format-citations.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-citation.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-citation.tool.js +42 -8
- package/dist/mcp-server/tools/definitions/lookup-citation.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-articles.tool.d.ts +4 -0
- package/dist/mcp-server/tools/definitions/search-articles.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-articles.tool.js +37 -2
- package/dist/mcp-server/tools/definitions/search-articles.tool.js.map +1 -1
- package/dist/services/error-contracts.d.ts +17 -0
- package/dist/services/error-contracts.d.ts.map +1 -1
- package/dist/services/error-contracts.js +19 -0
- package/dist/services/error-contracts.js.map +1 -1
- package/dist/services/ncbi/formatting/citation-formatter.d.ts +0 -29
- package/dist/services/ncbi/formatting/citation-formatter.d.ts.map +1 -1
- package/dist/services/ncbi/formatting/citation-formatter.js +381 -30
- package/dist/services/ncbi/formatting/citation-formatter.js.map +1 -1
- package/dist/services/ncbi/ncbi-service.d.ts +3 -2
- package/dist/services/ncbi/ncbi-service.d.ts.map +1 -1
- package/dist/services/ncbi/ncbi-service.js +22 -10
- package/dist/services/ncbi/ncbi-service.js.map +1 -1
- package/dist/services/ncbi/parsing/article-parser.d.ts +45 -2
- package/dist/services/ncbi/parsing/article-parser.d.ts.map +1 -1
- package/dist/services/ncbi/parsing/article-parser.js +208 -1
- package/dist/services/ncbi/parsing/article-parser.js.map +1 -1
- package/dist/services/ncbi/parsing/esummary-parser.d.ts.map +1 -1
- package/dist/services/ncbi/parsing/esummary-parser.js +32 -1
- package/dist/services/ncbi/parsing/esummary-parser.js.map +1 -1
- package/dist/services/ncbi/parsing/pmc-article-parser.d.ts.map +1 -1
- package/dist/services/ncbi/parsing/pmc-article-parser.js +7 -1
- package/dist/services/ncbi/parsing/pmc-article-parser.js.map +1 -1
- package/dist/services/ncbi/response-handler.d.ts.map +1 -1
- package/dist/services/ncbi/response-handler.js +42 -1
- package/dist/services/ncbi/response-handler.js.map +1 -1
- package/dist/services/ncbi/types.d.ts +172 -2
- package/dist/services/ncbi/types.d.ts.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
|
|
|
@@ -59,6 +59,7 @@ Search PubMed with full NCBI query syntax and filters.
|
|
|
59
59
|
- Sort by relevance, publication date, author, or journal
|
|
60
60
|
- Pagination via offset for paging through large result sets
|
|
61
61
|
- Optional brief summaries for top N results via ESummary
|
|
62
|
+
- NCBI Bookshelf results carry their own venue — `bookTitle`, `publisherName`, and `docType` (`chapter`, `book`, or `citation`) — because PubMed leaves `source` empty on them; the book's editors are reported in `editors`, apart from the chapter's own authors
|
|
62
63
|
- Returns the original query plus the fully applied PubMed query and normalized filter metadata
|
|
63
64
|
|
|
64
65
|
---
|
|
@@ -72,6 +73,8 @@ Fetch full article metadata by PubMed IDs.
|
|
|
72
73
|
- Direct links to PubMed and PubMed Central (when available)
|
|
73
74
|
- Optional MeSH terms, grant information, and publication types
|
|
74
75
|
- Handles PubMed's inconsistent XML (structured abstracts, missing fields, varying date formats)
|
|
76
|
+
- NCBI Bookshelf chapters and whole books are returned as first-class records, not reported unavailable: `recordType` (`journal-article`, `book-chapter`, `book`) tells them apart, and a `book` object carries the book title, publisher, place, dates, medium, edition, series, ISBNs, book DOI, editors, and Bookshelf accession. `journalInfo` is absent on those records — a book title is never reported as a journal
|
|
77
|
+
- Journals that assign article numbers instead of page ranges often carry no pagination at all; the number is reported as `journalInfo.elocationId` with its `journalInfo.elocationIdType` (`pii`), never merged into `journalInfo.pages` and never confused with the DOI
|
|
75
78
|
- Opt-in whole-response ceiling: `maxResponseCharacters` keeps complete article records in response order until the next one would cross it, then defers the rest whole and lists their PMIDs in `deferred.ids`. Re-call with those PMIDs to resume exactly where the response stopped — no article is split, skipped, or duplicated. Each article is measured as the JSON record it is returned as, so a ceiling under the first article returns zero articles, the full deferred list, and the size to clear
|
|
76
79
|
|
|
77
80
|
---
|
|
@@ -80,7 +83,7 @@ Fetch full article metadata by PubMed IDs.
|
|
|
80
83
|
|
|
81
84
|
Fetch full-text articles via a three-stage chain: NCBI PMC EFetch → Europe PMC `fullTextXML` → Unpaywall.
|
|
82
85
|
|
|
83
|
-
- Accepts exactly one of `pmcids` (direct PMC IDs), `pmids` (PubMed IDs, auto-resolved), or `dois` (auto-resolved to PMC via the ID Converter; preprints and EPMC-only OA fall through to Europe PMC / Unpaywall)
|
|
86
|
+
- Accepts exactly one of `pmcids` (direct PMC IDs), `pmids` (PubMed IDs, auto-resolved), or `dois` (auto-resolved to PMC via the ID Converter; preprints and EPMC-only OA fall through to Europe PMC / Unpaywall). One identifier per element in every branch — a DOI carrying a comma or whitespace is rejected at the schema
|
|
84
87
|
- NCBI PMC and Europe PMC both return structured JATS; output records origin via `viaSource: "pmc" | "europepmc" | "unpaywall"`
|
|
85
88
|
- Europe PMC layer (enabled by default; disable with `EUROPEPMC_ENABLED=false`) recovers PMC-counterpart records that NCBI PMC EFetch missed, and resolves DOI input to PMC counterparts when one exists. EPMC's `fullTextXML` is PMC-keyed, so preprints (PPR), patents (PAT), and Agricola (AGR) are reachable via `pubmed_europepmc_search` for metadata but have no full text via this chain.
|
|
86
89
|
- Unpaywall layer (enabled by setting `UNPAYWALL_EMAIL`) resolves DOIs to legal OA copies; extracts HTML landing pages to Markdown via Defuddle or PDFs to text via unpdf
|
|
@@ -127,6 +130,8 @@ Fetch complete Europe PMC records by `source` + `epmcId`, the detail counterpart
|
|
|
127
130
|
Generate formatted citations for articles.
|
|
128
131
|
|
|
129
132
|
- Five citation styles: APA 7th, MLA 9th, BibTeX, RIS, Vancouver (ICMJE/NLM)
|
|
133
|
+
- NCBI Bookshelf chapters and whole books cite in their own form in every style — Vancouver's `In: … editors` contribution pattern, APA's chapter-in-edited-book, MLA's `edited by`, BibTeX `@incollection` / `@book`, RIS `CHAP` / `BOOK` — carrying the book title, editors, publisher, place, ISBNs and Bookshelf URL
|
|
134
|
+
- An article with no page range cites by its electronic article locator in each style's own convention — Vancouver's trailing `pii:` note, APA's `Article <n>`, MLA's `art. <n>`, biblatex `eid`, RIS `C7` — rather than dropping it or writing it into a page field
|
|
130
135
|
- Request multiple styles per article in a single call
|
|
131
136
|
- Hand-rolled formatters — zero external dependencies, fully Workers-compatible
|
|
132
137
|
- Up to 50 articles per request
|
|
@@ -139,7 +144,7 @@ Generate formatted citations for articles.
|
|
|
139
144
|
Find articles related to a source article via ELink.
|
|
140
145
|
|
|
141
146
|
- Three relationship types: `similar` (content similarity), `cited_by`, `references`
|
|
142
|
-
- Results enriched with title, authors, publication date, and source via ESummary
|
|
147
|
+
- Results enriched with title, authors, publication date, and source via ESummary — or, for an NCBI Bookshelf record, its book title, publisher, and doc type in place of the empty source
|
|
143
148
|
- Results returned in NCBI's relevance order
|
|
144
149
|
- Falls back to Europe PMC, then OpenAlex, when NCBI cannot answer; the response names which provider served it. A request no provider can answer fails with a typed `all_providers_failed` error instead of an empty result
|
|
145
150
|
|
|
@@ -170,6 +175,7 @@ Resolve partial bibliographic references to PubMed IDs via NCBI ECitMatch.
|
|
|
170
175
|
|
|
171
176
|
- Match citations by journal, year, volume, first page, and/or author name
|
|
172
177
|
- More fields = better match accuracy; at least one field required
|
|
178
|
+
- Bibliographic fields cannot contain a pipe (`|`) or a line break — ECitMatch's wire format is pipe-delimited, so those characters are rejected at the schema; the free-form `key` label is exempt
|
|
173
179
|
- Batch up to 25 citations per request
|
|
174
180
|
- Deterministic matching — more reliable than free-text search for known references
|
|
175
181
|
- Returns explicit `matched`, `not_found`, and `ambiguous` statuses with recovery detail
|
|
@@ -182,6 +188,7 @@ Convert between article identifiers (DOI, PMID, PMCID) using the PMC ID Converte
|
|
|
182
188
|
|
|
183
189
|
- Batch up to 50 IDs per request
|
|
184
190
|
- Accepts DOIs, PMIDs, or PMCIDs (all IDs must be the same type)
|
|
191
|
+
- One identifier per array element, checked against `idType` before the request — a packed value like `"23193287,37952131"` is rejected rather than expanded into extra records, since a comma is the converter's list delimiter in any encoding
|
|
185
192
|
- Only resolves articles indexed in PubMed Central
|
|
186
193
|
- Per-ID success/error reporting — partial batches return resolved mappings alongside structured errors for unresolvable IDs, not a batch-level failure
|
|
187
194
|
|
|
@@ -11,4 +11,20 @@ import { z } from '@cyanheads/mcp-ts-core';
|
|
|
11
11
|
* stray prefixes like "PMID:").
|
|
12
12
|
*/
|
|
13
13
|
export declare const pmidStringSchema: z.ZodString;
|
|
14
|
+
/**
|
|
15
|
+
* Zod string schema for a single PMC ID. Digits, with the "PMC" prefix optional
|
|
16
|
+
* and case-insensitive — both forms are accepted upstream.
|
|
17
|
+
*/
|
|
18
|
+
export declare const pmcidStringSchema: z.ZodString;
|
|
19
|
+
/**
|
|
20
|
+
* Zod string schema for a single DOI: the "10." directory indicator, a
|
|
21
|
+
* registrant prefix, "/", and a suffix.
|
|
22
|
+
*
|
|
23
|
+
* Every DOI character is allowed except whitespace and the comma, so the
|
|
24
|
+
* suffix punctuation a real DOI carries — parens, colons, angle brackets,
|
|
25
|
+
* semicolons, further slashes — passes through. The comma is excluded because
|
|
26
|
+
* it is the PMC ID Converter's list delimiter in any encoding, so an element
|
|
27
|
+
* carrying one is read upstream as two identifiers rather than one DOI.
|
|
28
|
+
*/
|
|
29
|
+
export declare const doiStringSchema: z.ZodString;
|
|
14
30
|
//# sourceMappingURL=_schemas.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_schemas.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_schemas.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,aAK1B,CAAC"}
|
|
1
|
+
{"version":3,"file":"_schemas.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_schemas.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,aAK1B,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,iBAAiB,aAK3B,CAAC;AAEJ;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,aAKzB,CAAC"}
|
|
@@ -13,4 +13,24 @@ import { z } from '@cyanheads/mcp-ts-core';
|
|
|
13
13
|
export const pmidStringSchema = z
|
|
14
14
|
.string()
|
|
15
15
|
.regex(/^\d+$/, 'PMID must be a numeric identifier (e.g. "13054692"). Remove any whitespace, commas, or non-digit characters — provide each PMID separately.');
|
|
16
|
+
/**
|
|
17
|
+
* Zod string schema for a single PMC ID. Digits, with the "PMC" prefix optional
|
|
18
|
+
* and case-insensitive — both forms are accepted upstream.
|
|
19
|
+
*/
|
|
20
|
+
export const pmcidStringSchema = z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(/^(?:PMC)?\d+$/i, 'PMC ID must be digits, optionally prefixed with "PMC" (e.g. "PMC9575052" or "9575052"). Remove any whitespace or commas — provide each PMC ID separately.');
|
|
23
|
+
/**
|
|
24
|
+
* Zod string schema for a single DOI: the "10." directory indicator, a
|
|
25
|
+
* registrant prefix, "/", and a suffix.
|
|
26
|
+
*
|
|
27
|
+
* Every DOI character is allowed except whitespace and the comma, so the
|
|
28
|
+
* suffix punctuation a real DOI carries — parens, colons, angle brackets,
|
|
29
|
+
* semicolons, further slashes — passes through. The comma is excluded because
|
|
30
|
+
* it is the PMC ID Converter's list delimiter in any encoding, so an element
|
|
31
|
+
* carrying one is read upstream as two identifiers rather than one DOI.
|
|
32
|
+
*/
|
|
33
|
+
export const doiStringSchema = z
|
|
34
|
+
.string()
|
|
35
|
+
.regex(/^10\.[^\s,]+\/[^\s,]+$/, 'DOI must start with "10." and contain a "/" (e.g. "10.1093/nar/gks1195"). Remove any whitespace or commas — provide each DOI separately.');
|
|
16
36
|
//# sourceMappingURL=_schemas.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_schemas.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_schemas.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC;KAC9B,MAAM,EAAE;KACR,KAAK,CACJ,OAAO,EACP,6IAA6I,CAC9I,CAAC"}
|
|
1
|
+
{"version":3,"file":"_schemas.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_schemas.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC;KAC9B,MAAM,EAAE;KACR,KAAK,CACJ,OAAO,EACP,6IAA6I,CAC9I,CAAC;AAEJ;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,EAAE;KACR,KAAK,CACJ,gBAAgB,EAChB,2JAA2J,CAC5J,CAAC;AAEJ;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,KAAK,CACJ,wBAAwB,EACxB,0IAA0I,CAC3I,CAAC"}
|
|
@@ -51,5 +51,11 @@ export declare const convertIdsTool: import("@cyanheads/mcp-ts-core").ToolDefini
|
|
|
51
51
|
readonly when: 'NCBI returned a structured "not found" error for the requested ID(s).';
|
|
52
52
|
readonly recovery: 'Verify the ID exists in PubMed; the resource was not found in NCBI and retrying will not help.';
|
|
53
53
|
readonly retryable: false;
|
|
54
|
+
}, {
|
|
55
|
+
readonly reason: 'malformed_id';
|
|
56
|
+
readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
|
|
57
|
+
readonly when: 'An `ids` element does not match the declared `idType` — most often several identifiers packed into one element, which the comma-delimited upstream batch would split into extra records.';
|
|
58
|
+
readonly recovery: 'Submit one identifier per `ids` element, in the declared idType format; the same packed value will be rejected again.';
|
|
59
|
+
readonly retryable: false;
|
|
54
60
|
}], undefined>;
|
|
55
61
|
//# sourceMappingURL=convert-ids.tool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convert-ids.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/convert-ids.tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"convert-ids.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/convert-ids.tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAiCjD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAyHzB,CAAC"}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* @module src/mcp-server/tools/definitions/convert-ids.tool
|
|
5
5
|
*/
|
|
6
6
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
7
|
-
import { NCBI_SERVICE_ERRORS } from '../../../services/error-contracts.js';
|
|
7
|
+
import { NCBI_ID_INPUT_ERRORS, NCBI_SERVICE_ERRORS } from '../../../services/error-contracts.js';
|
|
8
8
|
import { getNcbiService } from '../../../services/ncbi/ncbi-service.js';
|
|
9
9
|
import { conceptMeta, EDAM_ACCESSION, EDAM_ID_MAPPING } from './_concepts.js';
|
|
10
|
+
import { doiStringSchema, pmcidStringSchema, pmidStringSchema } from './_schemas.js';
|
|
10
11
|
/**
|
|
11
12
|
* NCBI's PMC ID Converter returns this exact wording for any non-PMC ID — even
|
|
12
13
|
* articles that exist in PubMed and have a recoverable DOI. Rewrite to point
|
|
@@ -14,18 +15,34 @@ import { conceptMeta, EDAM_ACCESSION, EDAM_ID_MAPPING } from './_concepts.js';
|
|
|
14
15
|
*/
|
|
15
16
|
const PMC_NOT_FOUND_RE = /^identifier not found in pmc$/i;
|
|
16
17
|
const PMC_NOT_FOUND_REWRITE = 'Not in PMC ID Converter. Article may still exist in PubMed — try pubmed_fetch_articles (PMID → DOI) or pubmed_search_articles.';
|
|
18
|
+
/**
|
|
19
|
+
* Per-`idType` element format, checked in the handler rather than the schema:
|
|
20
|
+
* a Zod `.regex()` on `ids` cannot branch on the sibling `idType` field.
|
|
21
|
+
*
|
|
22
|
+
* Every one of these rejects an element carrying a comma, which is what closes
|
|
23
|
+
* #120 — the converter reads a comma as its list delimiter, so a packed element
|
|
24
|
+
* comes back as several records and breaks the one-record-per-submitted-ID
|
|
25
|
+
* contract the counts are computed from.
|
|
26
|
+
*/
|
|
27
|
+
const ID_ELEMENT_SCHEMAS = {
|
|
28
|
+
doi: doiStringSchema,
|
|
29
|
+
pmcid: pmcidStringSchema,
|
|
30
|
+
pmid: pmidStringSchema,
|
|
31
|
+
};
|
|
32
|
+
/** Cap the offending value echoed back so an oversized element can't bloat the error. */
|
|
33
|
+
const MAX_ECHOED_ID_LENGTH = 120;
|
|
17
34
|
export const convertIdsTool = tool('pubmed_convert_ids', {
|
|
18
35
|
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.`,
|
|
19
36
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
20
37
|
_meta: conceptMeta([EDAM_ID_MAPPING, EDAM_ACCESSION]),
|
|
21
38
|
sourceUrl: 'https://github.com/cyanheads/pubmed-mcp-server/blob/main/src/mcp-server/tools/definitions/convert-ids.tool.ts',
|
|
22
|
-
errors: [...NCBI_SERVICE_ERRORS],
|
|
39
|
+
errors: [...NCBI_SERVICE_ERRORS, ...NCBI_ID_INPUT_ERRORS],
|
|
23
40
|
input: z.object({
|
|
24
41
|
ids: z
|
|
25
42
|
.array(z.string().min(1))
|
|
26
43
|
.min(1)
|
|
27
44
|
.max(50)
|
|
28
|
-
.describe('Article identifiers to convert
|
|
45
|
+
.describe('Article identifiers to convert — one identifier per element, all of the same type. Each element is checked against `idType` before the request: `doi` starts with "10." and carries a "/" ("10.1093/nar/gks1195"); `pmid` is digits ("23193287"); `pmcid` is digits with an optional "PMC" prefix ("PMC3531190" or "3531190"). No element may contain a comma or whitespace — a packed value like "23193287,37952131" is rejected, so split it across elements.'),
|
|
29
46
|
idType: z
|
|
30
47
|
.enum(['pmcid', 'pmid', 'doi'])
|
|
31
48
|
.describe('The type of IDs being submitted. Required so the API can unambiguously resolve them.'),
|
|
@@ -59,6 +76,14 @@ export const convertIdsTool = tool('pubmed_convert_ids', {
|
|
|
59
76
|
count: input.ids.length,
|
|
60
77
|
idType: input.idType,
|
|
61
78
|
});
|
|
79
|
+
const elementSchema = ID_ELEMENT_SCHEMAS[input.idType];
|
|
80
|
+
for (const id of input.ids) {
|
|
81
|
+
const parsed = elementSchema.safeParse(id);
|
|
82
|
+
if (parsed.success)
|
|
83
|
+
continue;
|
|
84
|
+
const shown = id.length > MAX_ECHOED_ID_LENGTH ? `${id.slice(0, MAX_ECHOED_ID_LENGTH)}…` : id;
|
|
85
|
+
throw ctx.fail('malformed_id', `Invalid ${input.idType} element "${shown}". ${parsed.error.issues[0]?.message}`, { ...ctx.recoveryFor('malformed_id') });
|
|
86
|
+
}
|
|
62
87
|
const raw = await getNcbiService().idConvert(input.ids, input.idType, { signal: ctx.signal });
|
|
63
88
|
// NCBI returns pmid as a number in JSON — coerce all ID fields to strings
|
|
64
89
|
const records = raw.map((r) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convert-ids.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/convert-ids.tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"convert-ids.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/convert-ids.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,6 +12,11 @@ export declare const fetchArticlesTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
12
12
|
maxResponseCharacters: z.ZodOptional<z.ZodNumber>;
|
|
13
13
|
}, z.core.$strip>, z.ZodObject<{
|
|
14
14
|
articles: z.ZodArray<z.ZodObject<{
|
|
15
|
+
recordType: z.ZodEnum<{
|
|
16
|
+
book: "book";
|
|
17
|
+
"book-chapter": "book-chapter";
|
|
18
|
+
"journal-article": "journal-article";
|
|
19
|
+
}>;
|
|
15
20
|
pmid: z.ZodOptional<z.ZodString>;
|
|
16
21
|
title: z.ZodOptional<z.ZodString>;
|
|
17
22
|
abstractText: z.ZodOptional<z.ZodString>;
|
|
@@ -32,6 +37,8 @@ export declare const fetchArticlesTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
32
37
|
volume: z.ZodOptional<z.ZodString>;
|
|
33
38
|
issue: z.ZodOptional<z.ZodString>;
|
|
34
39
|
pages: z.ZodOptional<z.ZodString>;
|
|
40
|
+
elocationId: z.ZodOptional<z.ZodString>;
|
|
41
|
+
elocationIdType: z.ZodOptional<z.ZodString>;
|
|
35
42
|
publicationDate: z.ZodOptional<z.ZodObject<{
|
|
36
43
|
year: z.ZodOptional<z.ZodString>;
|
|
37
44
|
month: z.ZodOptional<z.ZodString>;
|
|
@@ -39,6 +46,26 @@ export declare const fetchArticlesTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
39
46
|
medlineDate: z.ZodOptional<z.ZodString>;
|
|
40
47
|
}, z.core.$strip>>;
|
|
41
48
|
}, z.core.$strip>>;
|
|
49
|
+
book: z.ZodOptional<z.ZodObject<{
|
|
50
|
+
title: z.ZodOptional<z.ZodString>;
|
|
51
|
+
publisher: z.ZodOptional<z.ZodString>;
|
|
52
|
+
publisherLocation: z.ZodOptional<z.ZodString>;
|
|
53
|
+
pubDate: z.ZodOptional<z.ZodString>;
|
|
54
|
+
beginningDate: z.ZodOptional<z.ZodString>;
|
|
55
|
+
endingDate: z.ZodOptional<z.ZodString>;
|
|
56
|
+
medium: z.ZodOptional<z.ZodString>;
|
|
57
|
+
edition: z.ZodOptional<z.ZodString>;
|
|
58
|
+
collectionTitle: z.ZodOptional<z.ZodString>;
|
|
59
|
+
isbns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
60
|
+
doi: z.ZodOptional<z.ZodString>;
|
|
61
|
+
editors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
62
|
+
lastName: z.ZodOptional<z.ZodString>;
|
|
63
|
+
firstName: z.ZodOptional<z.ZodString>;
|
|
64
|
+
initials: z.ZodOptional<z.ZodString>;
|
|
65
|
+
collectiveName: z.ZodOptional<z.ZodString>;
|
|
66
|
+
}, z.core.$strip>>>;
|
|
67
|
+
accession: z.ZodOptional<z.ZodString>;
|
|
68
|
+
}, z.core.$strip>>;
|
|
42
69
|
doi: z.ZodOptional<z.ZodString>;
|
|
43
70
|
pmcId: z.ZodOptional<z.ZodString>;
|
|
44
71
|
pubmedUrl: z.ZodOptional<z.ZodString>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-articles.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fetch-articles.tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch-articles.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fetch-articles.tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAwRjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAWhB,yBAAyB;;mBAE3B,sEAAsE;uBAE1E,qFAAqF;;;;EAkR3F,CAAC"}
|
|
@@ -7,8 +7,7 @@ import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
|
7
7
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
8
8
|
import { NCBI_SERVICE_ERRORS } from '../../../services/error-contracts.js';
|
|
9
9
|
import { getNcbiService } from '../../../services/ncbi/ncbi-service.js';
|
|
10
|
-
import {
|
|
11
|
-
import { ensureArray } from '../../../services/ncbi/parsing/xml-helpers.js';
|
|
10
|
+
import { parseArticleSet } from '../../../services/ncbi/parsing/article-parser.js';
|
|
12
11
|
import { fitWholeItems } from './_budget.js';
|
|
13
12
|
import { conceptMeta, EDAM_DATA_RETRIEVAL, EDAM_PUBMED_ID, SCHEMA_SCHOLARLY_ARTICLE, } from './_concepts.js';
|
|
14
13
|
import { pmidStringSchema } from './_schemas.js';
|
|
@@ -43,9 +42,93 @@ const JournalInfoSchema = z
|
|
|
43
42
|
volume: z.string().optional().describe('Volume number'),
|
|
44
43
|
issue: z.string().optional().describe('Issue number'),
|
|
45
44
|
pages: z.string().optional().describe('Page range (e.g. "48-55")'),
|
|
45
|
+
elocationId: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Electronic article locator from NCBI `ELocationID` — the publisher-assigned article number (e.g. "2400512"). Journals that assign article numbers instead of pages often omit pagination entirely, leaving this the only locator. Never a substitute for `pages`, and never the DOI: a DOI-typed `ELocationID` is reported in `doi` instead. Absent when the only locator NCBI supplies is marked invalid.'),
|
|
49
|
+
elocationIdType: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Type of `elocationId`, from NCBI\'s `EIdType` attribute — "pii" in practice. Free-form: NCBI does not close the set, so treat an unfamiliar value as opaque.'),
|
|
46
53
|
publicationDate: JournalPublicationDateSchema.optional(),
|
|
47
54
|
})
|
|
48
|
-
.describe('Journal information');
|
|
55
|
+
.describe('Journal information. Present on `journal-article` records only — absent on `book-chapter` and `book` records, because a Bookshelf record has no journal and its book title is never reported as one; read `book` for those. (#114)');
|
|
56
|
+
const BookEditorSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
lastName: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Editor surname, from the book\'s `Book/AuthorList Type="editors"` entry. Absent on a group editor, which carries `collectiveName` instead.'),
|
|
62
|
+
firstName: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Editor given name as NCBI supplies it (`ForeName`, often "Margaret P"). Absent when NCBI carries initials only, or on a group editor.'),
|
|
66
|
+
initials: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Editor initials with no separators (e.g. "MP"). Absent when NCBI supplies none, or on a group editor.'),
|
|
70
|
+
collectiveName: z
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Group or committee credited as editor, when the entry names an organization rather than a person. Mutually exclusive with the name-part fields.'),
|
|
74
|
+
})
|
|
75
|
+
.describe('One editor of the containing book. Name parts only — editors are a citation credit, not a contributor record, so no affiliations or ORCID are reported for them.');
|
|
76
|
+
const BookInfoSchema = z
|
|
77
|
+
.object({
|
|
78
|
+
title: z
|
|
79
|
+
.string()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('Title of the containing book, from `Book/BookTitle` (e.g. "GeneReviews®"). On a `book` record this is the same value as the record\'s own `title`.'),
|
|
82
|
+
publisher: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Publisher of the book, from `Book/Publisher/PublisherName`.'),
|
|
86
|
+
publisherLocation: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('Place of publication, from `Book/Publisher/PublisherLocation` (e.g. "Seattle (WA)"). Absent when NCBI supplies no place.'),
|
|
90
|
+
pubDate: z
|
|
91
|
+
.string()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe("Publication year from `Book/PubDate`. Year only — NCBI's month and day are not reported, since no citation style uses them for a book."),
|
|
94
|
+
beginningDate: z
|
|
95
|
+
.string()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe('First year of a continuously-updated book, from `Book/BeginningDate` (GeneReviews runs from 1993). Absent on a book published once.'),
|
|
98
|
+
endingDate: z
|
|
99
|
+
.string()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Last year of a closed date range, from `Book/EndingDate`. Absent while a book is still being updated, which leaves the range open-ended.'),
|
|
102
|
+
medium: z
|
|
103
|
+
.string()
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Medium the book is published in, from `Book/Medium` — "Internet" wherever NCBI supplies it. Absent when NCBI supplies none; it is never defaulted.'),
|
|
106
|
+
edition: z
|
|
107
|
+
.string()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Edition statement from `Book/Edition`. Rare on Bookshelf titles — absent unless NCBI supplies one.'),
|
|
110
|
+
collectionTitle: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('Series the book belongs to, from `Book/CollectionTitle` (e.g. "ADA Clinical Compendia Series"). Absent for a book outside a series.'),
|
|
114
|
+
isbns: z
|
|
115
|
+
.array(z.string().describe('One ISBN, verbatim as NCBI reports it — leading zeros intact'))
|
|
116
|
+
.optional()
|
|
117
|
+
.describe('Every `Book/Isbn` on the record. A book commonly carries a print and an electronic ISBN, so this is a list. Absent for a Bookshelf title with no ISBN, which is most of them.'),
|
|
118
|
+
doi: z
|
|
119
|
+
.string()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe('The book\'s own DOI, from `Book/ELocationID` with `EIdType="doi"`. Distinct from the record-level `doi`, which is the chapter\'s: a chapter does not inherit this one.'),
|
|
122
|
+
editors: z
|
|
123
|
+
.array(BookEditorSchema)
|
|
124
|
+
.optional()
|
|
125
|
+
.describe('Editors of the containing book, from `Book/AuthorList` marked `Type="editors"`. Kept out of `authors`, which carries the chapter\'s own writers. Absent when the book credits no editors.'),
|
|
126
|
+
accession: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe('NCBI Bookshelf accession from `ArticleIdList` (`bookaccession`), e.g. "NBK1247". The record is readable at `https://www.ncbi.nlm.nih.gov/books/<accession>/`.'),
|
|
130
|
+
})
|
|
131
|
+
.describe('The containing book of a `book-chapter`, or the book itself on a `book` record. Present only on those two record types, and never a stand-in for `journalInfo`.');
|
|
49
132
|
const MeshQualifierSchema = z
|
|
50
133
|
.object({
|
|
51
134
|
qualifierName: z.string().describe('Qualifier/subheading name'),
|
|
@@ -79,12 +162,22 @@ const ArticleDateSchema = z
|
|
|
79
162
|
.describe('Dated article event');
|
|
80
163
|
const FetchedArticleSchema = z
|
|
81
164
|
.object({
|
|
165
|
+
recordType: z
|
|
166
|
+
.enum(['journal-article', 'book-chapter', 'book'])
|
|
167
|
+
.describe('Which kind of PubMed record this is, set from the XML element it arrived in: `journal-article` for an ordinary article, `book-chapter` for an NCBI Bookshelf chapter, `book` for a whole Bookshelf book. Read this to tell the three apart — `publicationTypes` cannot, because PubMed labels a Bookshelf record "Review" or "Study Guide". `journalInfo` is present only on `journal-article`; `book` only on the other two.'),
|
|
82
168
|
pmid: z.string().optional().describe('PubMed ID'),
|
|
83
|
-
title: z
|
|
169
|
+
title: z
|
|
170
|
+
.string()
|
|
171
|
+
.optional()
|
|
172
|
+
.describe('Article title — the chapter title on a `book-chapter`, and the book title on a `book` record, where it repeats `book.title`.'),
|
|
84
173
|
abstractText: z.string().optional().describe('Abstract text'),
|
|
85
174
|
affiliations: z.array(z.string()).optional().describe('Deduplicated author affiliations'),
|
|
86
|
-
authors: z
|
|
175
|
+
authors: z
|
|
176
|
+
.array(AuthorSchema)
|
|
177
|
+
.optional()
|
|
178
|
+
.describe("Author list. On a `book-chapter` these are the chapter's own authors, never the book's editors, which are in `book.editors`. Empty on a Bookshelf record that credits neither."),
|
|
87
179
|
journalInfo: JournalInfoSchema.optional(),
|
|
180
|
+
book: BookInfoSchema.optional(),
|
|
88
181
|
doi: z
|
|
89
182
|
.string()
|
|
90
183
|
.optional()
|
|
@@ -152,7 +245,7 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
|
|
|
152
245
|
unavailablePmids: z
|
|
153
246
|
.array(z.string())
|
|
154
247
|
.optional()
|
|
155
|
-
.describe('PMIDs
|
|
248
|
+
.describe('PMIDs PubMed returned no record for. That is all this reports: PubMed omits an unknown PMID silently, with no error and no reason, so the absence says nothing about whether the PMID exists. Reported in full regardless of where a `maxResponseCharacters` cutoff lands — these are misses, not deferrals. Use `pubmed_search_articles` to find PMIDs that do resolve.'),
|
|
156
249
|
deferred: DeferredSchema.optional(),
|
|
157
250
|
}),
|
|
158
251
|
// Recovery guidance for two cases — no articles returned at all, and articles
|
|
@@ -174,23 +267,19 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
|
|
|
174
267
|
if (!xmlData || !('PubmedArticleSet' in xmlData)) {
|
|
175
268
|
throw ctx.fail('invalid_efetch_response', 'Invalid EFetch response from NCBI: missing PubmedArticleSet', { requestedPmids: input.pmids.length, ...ctx.recoveryFor('invalid_efetch_response') });
|
|
176
269
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
pmcUrl: `https://www.ncbi.nlm.nih.gov/pmc/articles/${parsed.pmcId}/`,
|
|
191
|
-
}),
|
|
192
|
-
};
|
|
193
|
-
});
|
|
270
|
+
// Reads both members of the set. Taking `PubmedArticleSet.PubmedArticle`
|
|
271
|
+
// alone discards every NCBI Bookshelf record, whose PMIDs then surface as
|
|
272
|
+
// unavailable even though PubMed returned them. (#114)
|
|
273
|
+
const articles = parseArticleSet(xmlData.PubmedArticleSet, {
|
|
274
|
+
includeMesh: input.includeMesh,
|
|
275
|
+
includeGrants: input.includeGrants,
|
|
276
|
+
}).map((parsed) => ({
|
|
277
|
+
...parsed,
|
|
278
|
+
pubmedUrl: `https://pubmed.ncbi.nlm.nih.gov/${parsed.pmid}/`,
|
|
279
|
+
...(parsed.pmcId && {
|
|
280
|
+
pmcUrl: `https://www.ncbi.nlm.nih.gov/pmc/articles/${parsed.pmcId}/`,
|
|
281
|
+
}),
|
|
282
|
+
}));
|
|
194
283
|
const returnedPmids = new Set(articles.map((a) => a.pmid).filter(Boolean));
|
|
195
284
|
const unavailable = input.pmids.filter((id) => !returnedPmids.has(id));
|
|
196
285
|
// Whole-response budget: fill with complete records in response order and
|
|
@@ -222,7 +311,7 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
|
|
|
222
311
|
// Keyed on what resolved, not on what the budget kept: a batch emptied by a
|
|
223
312
|
// small ceiling is a budget outcome, not a batch of invalid PMIDs.
|
|
224
313
|
if (articles.length === 0) {
|
|
225
|
-
ctx.enrich.notice('No articles were returned
|
|
314
|
+
ctx.enrich.notice('No articles were returned: PubMed matched no record to any of these PMIDs. It omits a PMID it does not recognize silently, without an error or a reason, so nothing more than that is known here. Try pubmed_search_articles to discover PMIDs that resolve.');
|
|
226
315
|
}
|
|
227
316
|
if (deferred) {
|
|
228
317
|
ctx.enrich({ truncated: true });
|
|
@@ -275,6 +364,9 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
|
|
|
275
364
|
parts.push(`**${ji.volume}**${ji.issue ? `(${ji.issue})` : ''}`);
|
|
276
365
|
if (ji.pages)
|
|
277
366
|
parts.push(ji.pages);
|
|
367
|
+
if (ji.elocationId) {
|
|
368
|
+
parts.push(ji.elocationIdType ? `${ji.elocationIdType}: ${ji.elocationId}` : ji.elocationId);
|
|
369
|
+
}
|
|
278
370
|
if (ji.issn)
|
|
279
371
|
parts.push(`ISSN ${ji.issn}`);
|
|
280
372
|
if (ji.eIssn)
|
|
@@ -282,6 +374,56 @@ export const fetchArticlesTool = tool('pubmed_fetch_articles', {
|
|
|
282
374
|
if (parts.length)
|
|
283
375
|
lines.push(`\n**Journal:** ${parts.join(', ')}`);
|
|
284
376
|
}
|
|
377
|
+
// Venue block for a Bookshelf record. `journalInfo` is never set on one,
|
|
378
|
+
// so a book would otherwise render as a bare title with no publisher,
|
|
379
|
+
// date or permalink — the fields a citation actually needs. (#114)
|
|
380
|
+
const bk = a.book;
|
|
381
|
+
if (bk) {
|
|
382
|
+
const bookLines = [];
|
|
383
|
+
// On a whole-book record the heading above already is the book title;
|
|
384
|
+
// the medium marker then stands alone rather than repeating it.
|
|
385
|
+
if (a.recordType !== 'book' && bk.title) {
|
|
386
|
+
bookLines.push(`**Book:** ${bk.medium ? `${bk.title} [${bk.medium}]` : bk.title}`);
|
|
387
|
+
}
|
|
388
|
+
else if (bk.medium) {
|
|
389
|
+
bookLines.push(`**Medium:** ${bk.medium}`);
|
|
390
|
+
}
|
|
391
|
+
if (bk.editors?.length) {
|
|
392
|
+
bookLines.push('', `**Editors (${bk.editors.length}):**`);
|
|
393
|
+
for (const ed of bk.editors) {
|
|
394
|
+
bookLines.push(`- ${formatAuthor(ed)}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (bk.publisher)
|
|
398
|
+
bookLines.push(`**Publisher:** ${bk.publisher}`);
|
|
399
|
+
if (bk.publisherLocation)
|
|
400
|
+
bookLines.push(`**Publisher Location:** ${bk.publisherLocation}`);
|
|
401
|
+
// A book published over several years carries a closed range
|
|
402
|
+
// (GeneReviews 1993–2026); otherwise the single publication year. A
|
|
403
|
+
// publication year that differs from the range's start is kept beside it.
|
|
404
|
+
const range = bk.beginningDate && bk.endingDate && bk.beginningDate !== bk.endingDate
|
|
405
|
+
? `${bk.beginningDate}–${bk.endingDate}`
|
|
406
|
+
: undefined;
|
|
407
|
+
const bookDate = range && bk.pubDate && bk.pubDate !== bk.beginningDate
|
|
408
|
+
? `${bk.pubDate} (${range})`
|
|
409
|
+
: (range ?? bk.pubDate ?? bk.beginningDate ?? bk.endingDate);
|
|
410
|
+
if (bookDate)
|
|
411
|
+
bookLines.push(`**Published:** ${bookDate}`);
|
|
412
|
+
if (bk.edition)
|
|
413
|
+
bookLines.push(`**Edition:** ${bk.edition}`);
|
|
414
|
+
if (bk.collectionTitle)
|
|
415
|
+
bookLines.push(`**Collection:** ${bk.collectionTitle}`);
|
|
416
|
+
if (bk.isbns?.length)
|
|
417
|
+
bookLines.push(`**ISBN:** ${bk.isbns.join(', ')}`);
|
|
418
|
+
if (bk.doi)
|
|
419
|
+
bookLines.push(`**Book DOI:** ${bk.doi}`);
|
|
420
|
+
if (bk.accession) {
|
|
421
|
+
bookLines.push(`**Bookshelf:** https://www.ncbi.nlm.nih.gov/books/${bk.accession}/`);
|
|
422
|
+
}
|
|
423
|
+
if (bookLines.length > 0)
|
|
424
|
+
lines.push('', ...bookLines);
|
|
425
|
+
}
|
|
426
|
+
lines.push(`**Record Type:** ${a.recordType}`);
|
|
285
427
|
if (a.publicationTypes?.length)
|
|
286
428
|
lines.push(`**Type:** ${a.publicationTypes.join(', ')}`);
|
|
287
429
|
if (a.pmid)
|