@cyanheads/pubmed-mcp-server 1.2.3 → 1.2.4
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/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +8 -8
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +4 -3
- package/dist/mcp-server/tools/generatePubMedChart/logic.js +19 -19
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +63 -238
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +8 -9
- package/dist/services/NCBI/ncbiCoreApiClient.d.ts +1 -1
- package/dist/services/NCBI/ncbiCoreApiClient.js +51 -47
- package/dist/services/NCBI/ncbiService.d.ts +3 -2
- package/dist/services/NCBI/ncbiService.js +13 -1
- package/dist/types-global/pubmedXml.d.ts +15 -0
- package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.js +4 -0
- package/package.json +13 -11
|
@@ -22,17 +22,17 @@ export declare const FetchPubMedContentInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
22
22
|
includeMeshTerms: boolean;
|
|
23
23
|
includeGrantInfo: boolean;
|
|
24
24
|
outputFormat: "json" | "raw_text";
|
|
25
|
-
pmids?: string[] | undefined;
|
|
26
25
|
queryKey?: string | undefined;
|
|
27
26
|
webEnv?: string | undefined;
|
|
28
|
-
retstart?: number | undefined;
|
|
29
27
|
retmax?: number | undefined;
|
|
30
|
-
|
|
28
|
+
retstart?: number | undefined;
|
|
31
29
|
pmids?: string[] | undefined;
|
|
30
|
+
}, {
|
|
32
31
|
queryKey?: string | undefined;
|
|
33
32
|
webEnv?: string | undefined;
|
|
34
|
-
retstart?: number | undefined;
|
|
35
33
|
retmax?: number | undefined;
|
|
34
|
+
retstart?: number | undefined;
|
|
35
|
+
pmids?: string[] | undefined;
|
|
36
36
|
detailLevel?: "abstract_plus" | "full_xml" | "medline_text" | "citation_data" | undefined;
|
|
37
37
|
includeMeshTerms?: boolean | undefined;
|
|
38
38
|
includeGrantInfo?: boolean | undefined;
|
|
@@ -42,17 +42,17 @@ export declare const FetchPubMedContentInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
42
42
|
includeMeshTerms: boolean;
|
|
43
43
|
includeGrantInfo: boolean;
|
|
44
44
|
outputFormat: "json" | "raw_text";
|
|
45
|
-
pmids?: string[] | undefined;
|
|
46
45
|
queryKey?: string | undefined;
|
|
47
46
|
webEnv?: string | undefined;
|
|
48
|
-
retstart?: number | undefined;
|
|
49
47
|
retmax?: number | undefined;
|
|
50
|
-
|
|
48
|
+
retstart?: number | undefined;
|
|
51
49
|
pmids?: string[] | undefined;
|
|
50
|
+
}, {
|
|
52
51
|
queryKey?: string | undefined;
|
|
53
52
|
webEnv?: string | undefined;
|
|
54
|
-
retstart?: number | undefined;
|
|
55
53
|
retmax?: number | undefined;
|
|
54
|
+
retstart?: number | undefined;
|
|
55
|
+
pmids?: string[] | undefined;
|
|
56
56
|
detailLevel?: "abstract_plus" | "full_xml" | "medline_text" | "citation_data" | undefined;
|
|
57
57
|
includeMeshTerms?: boolean | undefined;
|
|
58
58
|
includeGrantInfo?: boolean | undefined;
|
|
@@ -242,13 +242,14 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
else if (input.detailLevel === "full_xml") {
|
|
245
|
+
const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
|
|
246
|
+
articlesCount = articlesXml.length;
|
|
245
247
|
if (input.outputFormat === "raw_text") {
|
|
248
|
+
// Note: Raw XML output is requested, but we still parse to get an accurate count.
|
|
249
|
+
// This is a trade-off for robustness over performance in this specific case.
|
|
246
250
|
finalOutputText = String(eFetchResponseData);
|
|
247
|
-
articlesCount = (finalOutputText.match(/<PubmedArticle>/g) || []).length;
|
|
248
251
|
}
|
|
249
252
|
else {
|
|
250
|
-
const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
|
|
251
|
-
articlesCount = articlesXml.length;
|
|
252
253
|
const foundPmidsInXml = new Set();
|
|
253
254
|
const articlesPayload = articlesXml.map((articleXml) => {
|
|
254
255
|
const pmid = extractPmid(articleXml.MedlineCitation) || "unknown_pmid";
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Core logic for the generate_pubmed_chart tool.
|
|
3
|
-
* Generates charts from parameterized input by creating Chart.js configurations
|
|
4
|
-
* and rendering them on the server using chartjs-node-canvas.
|
|
5
|
-
* @module src/mcp-server/tools/generatePubMedChart/logic
|
|
6
|
-
*/
|
|
7
1
|
import { ChartJSNodeCanvas } from "chartjs-node-canvas";
|
|
8
2
|
import { z } from "zod";
|
|
9
3
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
@@ -96,8 +90,8 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
|
|
|
96
90
|
const groupedData = groupDataBySeries(dataValues, xField, yField, seriesField);
|
|
97
91
|
datasets = Array.from(groupedData.entries()).map(([seriesName, data]) => ({
|
|
98
92
|
label: seriesName,
|
|
99
|
-
data: labels.map(label => {
|
|
100
|
-
const point = data.find(p => p.x === label);
|
|
93
|
+
data: labels.map((label) => {
|
|
94
|
+
const point = data.find((p) => p.x === label);
|
|
101
95
|
return point ? point.y : null;
|
|
102
96
|
}),
|
|
103
97
|
// You can add backgroundColor, borderColor etc. here for styling
|
|
@@ -107,41 +101,45 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
|
|
|
107
101
|
datasets = [
|
|
108
102
|
{
|
|
109
103
|
label: yField,
|
|
110
|
-
data: labels.map(label => {
|
|
111
|
-
const item = dataValues.find(d => d[xField] === label);
|
|
104
|
+
data: labels.map((label) => {
|
|
105
|
+
const item = dataValues.find((d) => d[xField] === label);
|
|
112
106
|
return item ? item[yField] : null;
|
|
113
107
|
}),
|
|
114
108
|
},
|
|
115
109
|
];
|
|
116
110
|
}
|
|
117
111
|
// For scatter and bubble charts, the data format is different
|
|
118
|
-
if (chartType ===
|
|
112
|
+
if (chartType === "scatter" || chartType === "bubble") {
|
|
119
113
|
if (seriesField) {
|
|
120
114
|
const groupedData = groupDataBySeries(dataValues, xField, yField, seriesField);
|
|
121
115
|
datasets = Array.from(groupedData.entries()).map(([seriesName, data]) => ({
|
|
122
116
|
label: seriesName,
|
|
123
|
-
data: data.map(point => ({
|
|
117
|
+
data: data.map((point) => ({
|
|
124
118
|
x: point.x,
|
|
125
119
|
y: point.y,
|
|
126
|
-
r: chartType ===
|
|
120
|
+
r: chartType === "bubble" && sizeField
|
|
121
|
+
? dataValues.find((d) => d[xField] === point.x)[sizeField]
|
|
122
|
+
: undefined,
|
|
127
123
|
})),
|
|
128
124
|
}));
|
|
129
125
|
}
|
|
130
126
|
else {
|
|
131
|
-
datasets = [
|
|
127
|
+
datasets = [
|
|
128
|
+
{
|
|
132
129
|
label: yField,
|
|
133
|
-
data: dataValues.map(item => ({
|
|
130
|
+
data: dataValues.map((item) => ({
|
|
134
131
|
x: item[xField],
|
|
135
132
|
y: item[yField],
|
|
136
|
-
r: chartType ===
|
|
133
|
+
r: chartType === "bubble" && sizeField ? item[sizeField] : undefined,
|
|
137
134
|
})),
|
|
138
|
-
}
|
|
135
|
+
},
|
|
136
|
+
];
|
|
139
137
|
}
|
|
140
138
|
}
|
|
141
139
|
const configuration = {
|
|
142
140
|
type: chartType,
|
|
143
141
|
data: {
|
|
144
|
-
labels:
|
|
142
|
+
labels: chartType !== "scatter" && chartType !== "bubble" ? labels : undefined,
|
|
145
143
|
datasets: datasets,
|
|
146
144
|
},
|
|
147
145
|
options: {
|
|
@@ -151,7 +149,9 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
|
|
|
151
149
|
text: title,
|
|
152
150
|
},
|
|
153
151
|
},
|
|
154
|
-
scales: chartType === "pie" ||
|
|
152
|
+
scales: chartType === "pie" ||
|
|
153
|
+
chartType === "doughnut" ||
|
|
154
|
+
chartType === "polarArea"
|
|
155
155
|
? undefined
|
|
156
156
|
: {
|
|
157
157
|
x: {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Fetches article details using EFetch and formats them into various citation styles.
|
|
4
4
|
* @module src/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter
|
|
5
5
|
*/
|
|
6
|
+
import Cite from "citation-js";
|
|
6
7
|
import { getNcbiService } from "../../../../services/NCBI/ncbiService.js";
|
|
7
8
|
import { logger, requestContextService, } from "../../../../utils/index.js";
|
|
8
9
|
import { extractAuthors, extractDoi, extractJournalInfo, extractPmid, getText, } from "../../../../utils/parsing/ncbi-parsing/index.js";
|
|
@@ -12,7 +13,6 @@ export async function handleCitationFormats(input, outputData, context) {
|
|
|
12
13
|
db: "pubmed",
|
|
13
14
|
id: input.sourcePmid,
|
|
14
15
|
retmode: "xml",
|
|
15
|
-
// Omitting rettype to hopefully get the fullest XML record by default
|
|
16
16
|
};
|
|
17
17
|
const eFetchBaseUrl = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
|
|
18
18
|
const searchParamsString = new URLSearchParams(eFetchParams).toString();
|
|
@@ -26,264 +26,89 @@ export async function handleCitationFormats(input, outputData, context) {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
const article = eFetchResult.PubmedArticleSet.PubmedArticle[0];
|
|
29
|
+
const csl = pubmedArticleToCsl(article, context);
|
|
30
|
+
const cite = new Cite(csl);
|
|
29
31
|
if (input.citationStyles?.includes("ris")) {
|
|
30
|
-
outputData.citations.ris =
|
|
32
|
+
outputData.citations.ris = cite.format("ris");
|
|
31
33
|
}
|
|
32
34
|
if (input.citationStyles?.includes("bibtex")) {
|
|
33
|
-
outputData.citations.bibtex =
|
|
35
|
+
outputData.citations.bibtex = cite.format("bibtex");
|
|
34
36
|
}
|
|
35
37
|
if (input.citationStyles?.includes("apa_string")) {
|
|
36
|
-
outputData.citations.apa_string =
|
|
38
|
+
outputData.citations.apa_string = cite.format("bibliography", {
|
|
39
|
+
format: "text",
|
|
40
|
+
template: "apa",
|
|
41
|
+
});
|
|
37
42
|
}
|
|
38
43
|
if (input.citationStyles?.includes("mla_string")) {
|
|
39
|
-
outputData.citations.mla_string =
|
|
44
|
+
outputData.citations.mla_string = cite.format("bibliography", {
|
|
45
|
+
format: "text",
|
|
46
|
+
template: "mla",
|
|
47
|
+
});
|
|
40
48
|
}
|
|
41
49
|
outputData.retrievedCount = 1;
|
|
42
50
|
}
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Converts an XML PubMed Article object to a CSL-JSON object.
|
|
53
|
+
* @param article The PubMed article in XML format.
|
|
54
|
+
* @param context The request context for logging.
|
|
55
|
+
* @returns A CSL-JSON object compatible with citation-js.
|
|
56
|
+
*/
|
|
57
|
+
function pubmedArticleToCsl(article, context) {
|
|
45
58
|
const medlineCitation = article.MedlineCitation;
|
|
46
59
|
const articleDetails = medlineCitation?.Article;
|
|
47
60
|
const pmid = extractPmid(medlineCitation);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const authors = extractAuthors(articleDetails.AuthorList);
|
|
52
|
-
const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
|
|
53
|
-
const title = getText(articleDetails.ArticleTitle);
|
|
54
|
-
const doi = extractDoi(articleDetails);
|
|
55
|
-
let risString = "TY - JOUR\n";
|
|
56
|
-
authors.forEach((author) => {
|
|
57
|
-
if (author.lastName && author.firstName) {
|
|
58
|
-
risString += `AU - ${author.lastName}, ${author.firstName}\n`;
|
|
59
|
-
}
|
|
60
|
-
else if (author.lastName) {
|
|
61
|
-
// Handle collective name or incomplete author
|
|
62
|
-
risString += `AU - ${author.lastName}\n`;
|
|
63
|
-
}
|
|
61
|
+
const cslContext = requestContextService.createRequestContext({
|
|
62
|
+
...context,
|
|
63
|
+
pmid,
|
|
64
64
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (journalInfo?.pages) {
|
|
70
|
-
const pages = journalInfo.pages.split("-");
|
|
71
|
-
risString += `SP - ${pages[0] || ""}\n`;
|
|
72
|
-
if (pages.length > 1)
|
|
73
|
-
risString += `EP - ${pages[1] || ""}\n`;
|
|
65
|
+
logger.debug("Converting PubMed XML to CSL-JSON", cslContext);
|
|
66
|
+
if (!articleDetails) {
|
|
67
|
+
logger.warning("Article details not found for CSL conversion", cslContext);
|
|
68
|
+
return { id: pmid || "unknown", title: "Article details not found" };
|
|
74
69
|
}
|
|
75
|
-
|
|
76
|
-
if (doi)
|
|
77
|
-
risString += `DO - ${doi}\n`;
|
|
78
|
-
if (pmid)
|
|
79
|
-
risString += `UR - https://pubmed.ncbi.nlm.nih.gov/${pmid}\n`;
|
|
80
|
-
risString += "ER - \n";
|
|
81
|
-
return risString;
|
|
82
|
-
}
|
|
83
|
-
function formatAsBibTeX(article, context) {
|
|
84
|
-
const medlineCitation = article.MedlineCitation;
|
|
85
|
-
const articleDetails = medlineCitation?.Article;
|
|
86
|
-
const pmid = extractPmid(medlineCitation);
|
|
87
|
-
logger.debug("Formatting BibTeX for article", requestContextService.createRequestContext({ ...context, pmid }));
|
|
88
|
-
if (!articleDetails)
|
|
89
|
-
return `@article{ERROR_ArticleNotFound${pmid || ""},\n title = {Article details not found}\n}\n`;
|
|
90
|
-
const parsedAuthors = extractAuthors(articleDetails.AuthorList);
|
|
91
|
-
const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
|
|
92
|
-
const title = getText(articleDetails.ArticleTitle) || "N/A";
|
|
93
|
-
const doi = extractDoi(articleDetails);
|
|
94
|
-
const authorsBibtex = parsedAuthors
|
|
95
|
-
.map((author) => {
|
|
96
|
-
if (author.lastName && author.firstName)
|
|
97
|
-
return `${author.lastName}, ${author.firstName}`;
|
|
98
|
-
if (author.lastName)
|
|
99
|
-
return `{${author.lastName}}`; // For collective names or single names
|
|
100
|
-
return "";
|
|
101
|
-
})
|
|
102
|
-
.filter(Boolean)
|
|
103
|
-
.join(" and ");
|
|
104
|
-
const year = journalInfo?.publicationDate?.year || "ND";
|
|
105
|
-
const firstAuthorLastName = parsedAuthors[0]?.lastName?.replace(/\s+/g, "") || "Unknown";
|
|
106
|
-
const bibKey = `${firstAuthorLastName}${year}`;
|
|
107
|
-
let bibtexString = `@article{${bibKey},\n`;
|
|
108
|
-
if (authorsBibtex)
|
|
109
|
-
bibtexString += ` author = {${authorsBibtex}},\n`;
|
|
110
|
-
bibtexString += ` title = {${title}},\n`;
|
|
111
|
-
bibtexString += ` journal = {${journalInfo?.title || "N/A"}},\n`;
|
|
112
|
-
if (journalInfo?.publicationDate?.year)
|
|
113
|
-
bibtexString += ` year = {${journalInfo.publicationDate.year}},\n`;
|
|
114
|
-
if (journalInfo?.volume)
|
|
115
|
-
bibtexString += ` volume = {${journalInfo.volume}},\n`;
|
|
116
|
-
if (journalInfo?.issue)
|
|
117
|
-
bibtexString += ` number = {${journalInfo.issue}},\n`;
|
|
118
|
-
if (journalInfo?.pages)
|
|
119
|
-
bibtexString += ` pages = {${journalInfo.pages.replace("-", "--")}},\n`;
|
|
120
|
-
if (journalInfo?.publicationDate?.month)
|
|
121
|
-
bibtexString += ` month = {${journalInfo.publicationDate.month.toLowerCase()}},\n`;
|
|
122
|
-
if (doi)
|
|
123
|
-
bibtexString += ` doi = {${doi}},\n`;
|
|
124
|
-
if (pmid)
|
|
125
|
-
bibtexString += ` pmid = {${pmid}}\n`;
|
|
126
|
-
bibtexString += `}\n`;
|
|
127
|
-
return bibtexString;
|
|
128
|
-
}
|
|
129
|
-
function formatAsAPA(article, context) {
|
|
130
|
-
const medlineCitation = article.MedlineCitation;
|
|
131
|
-
const articleDetails = medlineCitation?.Article;
|
|
132
|
-
const pmid = extractPmid(medlineCitation);
|
|
133
|
-
logger.debug("Formatting APA for article", requestContextService.createRequestContext({ ...context, pmid }));
|
|
134
|
-
if (!articleDetails)
|
|
135
|
-
return "Article details not found.";
|
|
136
|
-
const parsedAuthors = extractAuthors(articleDetails.AuthorList);
|
|
137
|
-
const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
|
|
138
|
-
const titleText = getText(articleDetails.ArticleTitle) || "N/A";
|
|
139
|
-
const doi = extractDoi(articleDetails);
|
|
140
|
-
let authorsString = "N/A";
|
|
141
|
-
if (parsedAuthors.length > 0) {
|
|
142
|
-
if (parsedAuthors.length <= 20) {
|
|
143
|
-
authorsString = parsedAuthors
|
|
144
|
-
.map((author) => {
|
|
145
|
-
if (author.lastName && author.firstName) {
|
|
146
|
-
const initials = author.firstName
|
|
147
|
-
.split(/\s+|-/)
|
|
148
|
-
.map((namePart) => namePart.charAt(0).toUpperCase() + ".")
|
|
149
|
-
.join("");
|
|
150
|
-
return `${author.lastName}, ${initials}`;
|
|
151
|
-
}
|
|
152
|
-
if (author.lastName)
|
|
153
|
-
return author.lastName; // Collective name
|
|
154
|
-
return "";
|
|
155
|
-
})
|
|
156
|
-
.filter(Boolean)
|
|
157
|
-
.join(", ");
|
|
158
|
-
if (parsedAuthors.length > 1) {
|
|
159
|
-
const lastCommaIndex = authorsString.lastIndexOf(", ");
|
|
160
|
-
if (lastCommaIndex !== -1) {
|
|
161
|
-
authorsString =
|
|
162
|
-
authorsString.substring(0, lastCommaIndex) +
|
|
163
|
-
" & " +
|
|
164
|
-
authorsString.substring(lastCommaIndex + 2);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
const first19 = parsedAuthors
|
|
170
|
-
.slice(0, 19)
|
|
171
|
-
.map((author) => {
|
|
172
|
-
if (author.lastName && author.firstName) {
|
|
173
|
-
const initials = author.firstName
|
|
174
|
-
.split(/\s+|-/)
|
|
175
|
-
.map((namePart) => namePart.charAt(0).toUpperCase() + ".")
|
|
176
|
-
.join("");
|
|
177
|
-
return `${author.lastName}, ${initials}`;
|
|
178
|
-
}
|
|
179
|
-
if (author.lastName)
|
|
180
|
-
return author.lastName;
|
|
181
|
-
return "";
|
|
182
|
-
})
|
|
183
|
-
.filter(Boolean)
|
|
184
|
-
.join(", ");
|
|
185
|
-
const lastAuthor = parsedAuthors[parsedAuthors.length - 1];
|
|
186
|
-
let lastAuthorString = "";
|
|
187
|
-
if (lastAuthor.lastName && lastAuthor.firstName) {
|
|
188
|
-
const initials = lastAuthor.firstName
|
|
189
|
-
.split(/\s+|-/)
|
|
190
|
-
.map((namePart) => namePart.charAt(0).toUpperCase() + ".")
|
|
191
|
-
.join("");
|
|
192
|
-
lastAuthorString = `${lastAuthor.lastName}, ${initials}`;
|
|
193
|
-
}
|
|
194
|
-
else if (lastAuthor.lastName) {
|
|
195
|
-
lastAuthorString = lastAuthor.lastName;
|
|
196
|
-
}
|
|
197
|
-
authorsString = `${first19}, ..., ${lastAuthorString}`;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const year = journalInfo?.publicationDate?.year || "n.d.";
|
|
201
|
-
const apaTitle = titleText.charAt(0).toUpperCase() + titleText.slice(1); // APA typically sentence case for article titles.
|
|
202
|
-
const journal = journalInfo?.title || "N/A";
|
|
203
|
-
const volume = journalInfo?.volume || "";
|
|
204
|
-
const issue = journalInfo?.issue ? `(${journalInfo.issue})` : "";
|
|
205
|
-
const pages = journalInfo?.pages || "";
|
|
206
|
-
const doiLink = doi ? ` https://doi.org/${doi}` : "";
|
|
207
|
-
let apaString = `${authorsString}. (${year}). ${apaTitle}. ${journal}`;
|
|
208
|
-
if (volume)
|
|
209
|
-
apaString += `, ${volume}`;
|
|
210
|
-
if (issue)
|
|
211
|
-
apaString += issue;
|
|
212
|
-
if (pages)
|
|
213
|
-
apaString += `, ${pages}`;
|
|
214
|
-
apaString += `.${doiLink}`;
|
|
215
|
-
return apaString;
|
|
216
|
-
}
|
|
217
|
-
function formatAsMLA(article, context) {
|
|
218
|
-
const medlineCitation = article.MedlineCitation;
|
|
219
|
-
const articleDetails = medlineCitation?.Article;
|
|
220
|
-
const pmid = extractPmid(medlineCitation);
|
|
221
|
-
logger.debug("Formatting MLA for article", requestContextService.createRequestContext({ ...context, pmid }));
|
|
222
|
-
if (!articleDetails)
|
|
223
|
-
return "Article details not found.";
|
|
224
|
-
const parsedAuthors = extractAuthors(articleDetails.AuthorList);
|
|
70
|
+
const authors = extractAuthors(articleDetails.AuthorList);
|
|
225
71
|
const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
|
|
226
|
-
const
|
|
72
|
+
const title = getText(articleDetails.ArticleTitle);
|
|
227
73
|
const doi = extractDoi(articleDetails);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
74
|
+
const cslAuthors = authors.map((author) => author.collectiveName
|
|
75
|
+
? { literal: author.collectiveName }
|
|
76
|
+
: { family: author.lastName, given: author.firstName });
|
|
77
|
+
const dateParts = [];
|
|
78
|
+
if (journalInfo?.publicationDate?.year) {
|
|
79
|
+
dateParts.push(parseInt(journalInfo.publicationDate.year, 10));
|
|
80
|
+
if (journalInfo.publicationDate.month) {
|
|
81
|
+
// Convert month name/number to number
|
|
82
|
+
const monthNumber = new Date(`${journalInfo.publicationDate.month} 1, 2000`).getMonth();
|
|
83
|
+
if (!isNaN(monthNumber)) {
|
|
84
|
+
dateParts.push(monthNumber + 1);
|
|
85
|
+
if (journalInfo.publicationDate.day) {
|
|
86
|
+
dateParts.push(parseInt(journalInfo.publicationDate.day, 10));
|
|
237
87
|
}
|
|
238
|
-
if (author.lastName)
|
|
239
|
-
return author.lastName;
|
|
240
|
-
return "";
|
|
241
|
-
})
|
|
242
|
-
.filter(Boolean)
|
|
243
|
-
.join(" and ");
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
const firstAuthor = parsedAuthors[0];
|
|
247
|
-
if (firstAuthor.lastName && firstAuthor.firstName) {
|
|
248
|
-
authorsString = `${firstAuthor.lastName}, ${firstAuthor.firstName}, et al`;
|
|
249
|
-
}
|
|
250
|
-
else if (firstAuthor.lastName) {
|
|
251
|
-
authorsString = `${firstAuthor.lastName}, et al`;
|
|
252
88
|
}
|
|
253
89
|
}
|
|
254
90
|
}
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
91
|
+
const cslData = {
|
|
92
|
+
id: pmid,
|
|
93
|
+
type: "article-journal",
|
|
94
|
+
title: title,
|
|
95
|
+
author: cslAuthors,
|
|
96
|
+
issued: {
|
|
97
|
+
"date-parts": [dateParts],
|
|
98
|
+
},
|
|
99
|
+
"container-title": journalInfo?.title,
|
|
100
|
+
volume: journalInfo?.volume,
|
|
101
|
+
issue: journalInfo?.issue,
|
|
102
|
+
page: journalInfo?.pages,
|
|
103
|
+
DOI: doi,
|
|
104
|
+
PMID: pmid,
|
|
105
|
+
URL: pmid ? `https://pubmed.ncbi.nlm.nih.gov/${pmid}` : undefined,
|
|
106
|
+
};
|
|
107
|
+
// Clean up any undefined/null properties
|
|
108
|
+
for (const key in cslData) {
|
|
109
|
+
if (cslData[key] === undefined || cslData[key] === null) {
|
|
110
|
+
delete cslData[key];
|
|
263
111
|
}
|
|
264
112
|
}
|
|
265
|
-
|
|
266
|
-
publicationDateString = journalInfo.publicationDate.medlineDate;
|
|
267
|
-
}
|
|
268
|
-
const volume = journalInfo?.volume ? `vol. ${journalInfo.volume}` : "";
|
|
269
|
-
const issue = journalInfo?.issue ? `no. ${journalInfo.issue}` : "";
|
|
270
|
-
const pages = journalInfo?.pages
|
|
271
|
-
? `pp. ${journalInfo.pages.replace("-", "–")}`
|
|
272
|
-
: ""; // en-dash for MLA
|
|
273
|
-
const accessUrl = doi
|
|
274
|
-
? `doi:${doi}`
|
|
275
|
-
: pmid
|
|
276
|
-
? `https://pubmed.ncbi.nlm.nih.gov/${pmid}`
|
|
277
|
-
: "";
|
|
278
|
-
let mlaString = `${authorsString}. ${title} ${journal}`;
|
|
279
|
-
if (volume)
|
|
280
|
-
mlaString += `, ${volume}`;
|
|
281
|
-
if (issue)
|
|
282
|
-
mlaString += `, ${issue}`;
|
|
283
|
-
if (publicationDateString)
|
|
284
|
-
mlaString += `, ${publicationDateString}`;
|
|
285
|
-
if (pages)
|
|
286
|
-
mlaString += `, ${pages}`;
|
|
287
|
-
mlaString += `. PubMed Central, ${accessUrl}.`; // Assuming PubMed Central or just PubMed
|
|
288
|
-
return mlaString;
|
|
113
|
+
return cslData;
|
|
289
114
|
}
|
|
@@ -99,7 +99,7 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
|
|
|
99
99
|
sort: input.sortBy,
|
|
100
100
|
usehistory: currentFetchBriefSummaries > 0 ? "y" : "n",
|
|
101
101
|
};
|
|
102
|
-
const
|
|
102
|
+
const esResult = await ncbiService.eSearch(eSearchParams, toolLogicContext);
|
|
103
103
|
const eSearchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi";
|
|
104
104
|
const eSearchQueryStringParams = {};
|
|
105
105
|
for (const key in eSearchParams) {
|
|
@@ -109,15 +109,14 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
|
|
|
109
109
|
}
|
|
110
110
|
const eSearchQueryString = new URLSearchParams(eSearchQueryStringParams).toString();
|
|
111
111
|
const eSearchUrl = `${eSearchBase}?${eSearchQueryString}`;
|
|
112
|
-
if (!
|
|
112
|
+
if (!esResult) {
|
|
113
113
|
throw new McpError(BaseErrorCode.NCBI_PARSING_ERROR, "Invalid or empty ESearch response from NCBI.", {
|
|
114
114
|
...toolLogicContext,
|
|
115
|
-
responsePreview: sanitizeInputForLogging(JSON.stringify(
|
|
115
|
+
responsePreview: sanitizeInputForLogging(JSON.stringify(esResult).substring(0, 200)),
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const totalFound = parseInt(esResult.Count || "0", 10);
|
|
118
|
+
const pmids = esResult.idList || [];
|
|
119
|
+
const totalFound = esResult.count || 0;
|
|
121
120
|
const retrievedPmidCount = pmids.length;
|
|
122
121
|
let briefSummaries = [];
|
|
123
122
|
let eSummaryUrl;
|
|
@@ -127,9 +126,9 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
|
|
|
127
126
|
version: "2.0",
|
|
128
127
|
retmode: "xml",
|
|
129
128
|
};
|
|
130
|
-
if (esResult.
|
|
131
|
-
eSummaryParams.WebEnv = esResult.
|
|
132
|
-
eSummaryParams.query_key = esResult.
|
|
129
|
+
if (esResult.webEnv && esResult.queryKey) {
|
|
130
|
+
eSummaryParams.WebEnv = esResult.webEnv;
|
|
131
|
+
eSummaryParams.query_key = esResult.queryKey;
|
|
133
132
|
eSummaryParams.retmax = currentFetchBriefSummaries;
|
|
134
133
|
}
|
|
135
134
|
else {
|
|
@@ -20,5 +20,5 @@ export declare class NcbiCoreApiClient {
|
|
|
20
20
|
* @returns A Promise resolving to the raw AxiosResponse.
|
|
21
21
|
* @throws {McpError} If the request fails after all retries or an unexpected error occurs.
|
|
22
22
|
*/
|
|
23
|
-
makeRequest(endpoint: string, params: NcbiRequestParams, context: RequestContext, options?: NcbiRequestOptions
|
|
23
|
+
makeRequest(endpoint: string, params: NcbiRequestParams, context: RequestContext, options?: NcbiRequestOptions): Promise<AxiosResponse>;
|
|
24
24
|
}
|
|
@@ -25,14 +25,13 @@ export class NcbiCoreApiClient {
|
|
|
25
25
|
* @returns A Promise resolving to the raw AxiosResponse.
|
|
26
26
|
* @throws {McpError} If the request fails after all retries or an unexpected error occurs.
|
|
27
27
|
*/
|
|
28
|
-
async makeRequest(endpoint, params, context, options = {}
|
|
28
|
+
async makeRequest(endpoint, params, context, options = {}) {
|
|
29
29
|
const rawParams = {
|
|
30
30
|
tool: config.ncbiToolIdentifier,
|
|
31
31
|
email: config.ncbiAdminEmail,
|
|
32
32
|
api_key: config.ncbiApiKey,
|
|
33
33
|
...params,
|
|
34
34
|
};
|
|
35
|
-
// Filter out undefined/null values and convert others to string for URLSearchParams/request body
|
|
36
35
|
const finalParams = {};
|
|
37
36
|
for (const key in rawParams) {
|
|
38
37
|
if (Object.prototype.hasOwnProperty.call(rawParams, key)) {
|
|
@@ -55,59 +54,64 @@ export class NcbiCoreApiClient {
|
|
|
55
54
|
else {
|
|
56
55
|
requestConfig.params = finalParams;
|
|
57
56
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
operation: "NCBI_HttpRequest",
|
|
62
|
-
endpoint,
|
|
63
|
-
method: requestConfig.method,
|
|
64
|
-
requestParams: sanitizeInputForLogging(finalParams),
|
|
65
|
-
attempt: retries + 1,
|
|
66
|
-
}));
|
|
67
|
-
const response = await this.axiosInstance(requestConfig);
|
|
68
|
-
return response;
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
if (retries < config.ncbiMaxRetries) {
|
|
72
|
-
const retryDelay = Math.pow(2, retries) * 200; // Increased base delay for retries
|
|
73
|
-
logger.warning(`NCBI request to ${endpoint} failed. Retrying (${retries + 1}/${config.ncbiMaxRetries}) in ${retryDelay}ms...`, requestContextService.createRequestContext({
|
|
57
|
+
for (let attempt = 0; attempt <= config.ncbiMaxRetries; attempt++) {
|
|
58
|
+
try {
|
|
59
|
+
logger.debug(`Making NCBI HTTP request: ${requestConfig.method} ${requestConfig.url}`, requestContextService.createRequestContext({
|
|
74
60
|
...context,
|
|
75
|
-
operation: "
|
|
61
|
+
operation: "NCBI_HttpRequest",
|
|
76
62
|
endpoint,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
delay: retryDelay,
|
|
63
|
+
method: requestConfig.method,
|
|
64
|
+
requestParams: sanitizeInputForLogging(finalParams),
|
|
65
|
+
attempt: attempt + 1,
|
|
81
66
|
}));
|
|
82
|
-
|
|
83
|
-
return
|
|
67
|
+
const response = await this.axiosInstance(requestConfig);
|
|
68
|
+
return response;
|
|
84
69
|
}
|
|
85
|
-
|
|
86
|
-
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (attempt < config.ncbiMaxRetries) {
|
|
72
|
+
const retryDelay = Math.pow(2, attempt) * 200;
|
|
73
|
+
logger.warning(`NCBI request to ${endpoint} failed. Retrying (${attempt + 1}/${config.ncbiMaxRetries}) in ${retryDelay}ms...`, requestContextService.createRequestContext({
|
|
74
|
+
...context,
|
|
75
|
+
operation: "NCBI_HttpRequestRetry",
|
|
76
|
+
endpoint,
|
|
77
|
+
error: error.message,
|
|
78
|
+
retryCount: attempt + 1,
|
|
79
|
+
maxRetries: config.ncbiMaxRetries,
|
|
80
|
+
delay: retryDelay,
|
|
81
|
+
}));
|
|
82
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
83
|
+
continue; // Continue to the next iteration of the loop
|
|
84
|
+
}
|
|
85
|
+
// If all retries are exhausted, handle the final error
|
|
86
|
+
if (axios.isAxiosError(error)) {
|
|
87
|
+
logger.error(`Axios error during NCBI request to ${endpoint} after ${attempt} retries`, error, requestContextService.createRequestContext({
|
|
88
|
+
...context,
|
|
89
|
+
operation: "NCBI_AxiosError",
|
|
90
|
+
endpoint,
|
|
91
|
+
status: error.response?.status,
|
|
92
|
+
responseData: sanitizeInputForLogging(error.response?.data),
|
|
93
|
+
}));
|
|
94
|
+
throw new McpError(BaseErrorCode.NCBI_SERVICE_UNAVAILABLE, `NCBI request failed: ${error.message}`, {
|
|
95
|
+
endpoint,
|
|
96
|
+
status: error.response?.status,
|
|
97
|
+
details: error.response?.data
|
|
98
|
+
? String(error.response.data).substring(0, 500)
|
|
99
|
+
: undefined,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (error instanceof McpError)
|
|
103
|
+
throw error;
|
|
104
|
+
logger.error(`Unexpected error during NCBI request to ${endpoint} after ${attempt} retries`, error, requestContextService.createRequestContext({
|
|
87
105
|
...context,
|
|
88
|
-
operation: "
|
|
106
|
+
operation: "NCBI_UnexpectedError",
|
|
89
107
|
endpoint,
|
|
90
|
-
|
|
91
|
-
responseData: sanitizeInputForLogging(error.response?.data),
|
|
108
|
+
errorMessage: error.message,
|
|
92
109
|
}));
|
|
93
|
-
throw new McpError(BaseErrorCode.
|
|
94
|
-
endpoint,
|
|
95
|
-
status: error.response?.status,
|
|
96
|
-
details: error.response?.data
|
|
97
|
-
? String(error.response.data).substring(0, 500)
|
|
98
|
-
: undefined,
|
|
99
|
-
});
|
|
110
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Unexpected error communicating with NCBI: ${error.message}`, { endpoint });
|
|
100
111
|
}
|
|
101
|
-
// If it's already an McpError, rethrow it (could be from a previous stage if this function is used more broadly)
|
|
102
|
-
if (error instanceof McpError)
|
|
103
|
-
throw error;
|
|
104
|
-
logger.error(`Unexpected error during NCBI request to ${endpoint} after ${retries} retries`, error, requestContextService.createRequestContext({
|
|
105
|
-
...context,
|
|
106
|
-
operation: "NCBI_UnexpectedError",
|
|
107
|
-
endpoint,
|
|
108
|
-
errorMessage: error.message,
|
|
109
|
-
}));
|
|
110
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Unexpected error communicating with NCBI: ${error.message}`, { endpoint });
|
|
111
112
|
}
|
|
113
|
+
// This line should theoretically be unreachable, but it satisfies TypeScript's need
|
|
114
|
+
// for a return path if the loop completes without returning or throwing.
|
|
115
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Request failed after all retries.", { endpoint });
|
|
112
116
|
}
|
|
113
117
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* access PubMed data.
|
|
8
8
|
* @module src/services/NCBI/ncbiService
|
|
9
9
|
*/
|
|
10
|
+
import { ESearchResult, EFetchArticleSet } from "../../types-global/pubmedXml.js";
|
|
10
11
|
import { RequestContext } from "../../utils/index.js";
|
|
11
12
|
import { NcbiRequestParams, NcbiRequestOptions } from "./ncbiConstants.js";
|
|
12
13
|
export declare class NcbiService {
|
|
@@ -15,9 +16,9 @@ export declare class NcbiService {
|
|
|
15
16
|
private responseHandler;
|
|
16
17
|
constructor();
|
|
17
18
|
private performNcbiRequest;
|
|
18
|
-
eSearch(params: NcbiRequestParams, context: RequestContext): Promise<
|
|
19
|
+
eSearch(params: NcbiRequestParams, context: RequestContext): Promise<ESearchResult>;
|
|
19
20
|
eSummary(params: NcbiRequestParams, context: RequestContext): Promise<any>;
|
|
20
|
-
eFetch(params: NcbiRequestParams, context: RequestContext, options?: NcbiRequestOptions): Promise<
|
|
21
|
+
eFetch(params: NcbiRequestParams, context: RequestContext, options?: NcbiRequestOptions): Promise<EFetchArticleSet>;
|
|
21
22
|
eLink(params: NcbiRequestParams, context: RequestContext): Promise<any>;
|
|
22
23
|
eInfo(params: NcbiRequestParams, context: RequestContext): Promise<any>;
|
|
23
24
|
}
|
|
@@ -25,9 +25,21 @@ export class NcbiService {
|
|
|
25
25
|
return this.queueManager.enqueueRequest(task, context, endpoint, params);
|
|
26
26
|
}
|
|
27
27
|
async eSearch(params, context) {
|
|
28
|
-
|
|
28
|
+
const response = await this.performNcbiRequest("esearch", params, context, {
|
|
29
29
|
retmode: "xml",
|
|
30
30
|
});
|
|
31
|
+
const esResult = response.eSearchResult;
|
|
32
|
+
return {
|
|
33
|
+
count: parseInt(esResult.Count, 10) || 0,
|
|
34
|
+
retmax: parseInt(esResult.RetMax, 10) || 0,
|
|
35
|
+
retstart: parseInt(esResult.RetStart, 10) || 0,
|
|
36
|
+
queryKey: esResult.QueryKey,
|
|
37
|
+
webEnv: esResult.WebEnv,
|
|
38
|
+
idList: esResult.IdList?.Id || [],
|
|
39
|
+
queryTranslation: esResult.QueryTranslation,
|
|
40
|
+
errorList: esResult.ErrorList,
|
|
41
|
+
warningList: esResult.WarningList,
|
|
42
|
+
};
|
|
31
43
|
}
|
|
32
44
|
async eSummary(params, context) {
|
|
33
45
|
// Determine retmode based on params, default to xml
|
|
@@ -166,6 +166,7 @@ export interface ParsedArticleAuthor {
|
|
|
166
166
|
firstName?: string;
|
|
167
167
|
initials?: string;
|
|
168
168
|
affiliation?: string;
|
|
169
|
+
collectiveName?: string;
|
|
169
170
|
}
|
|
170
171
|
export interface ParsedArticleDate {
|
|
171
172
|
dateType?: string;
|
|
@@ -348,3 +349,17 @@ export interface ESearchResultContent {
|
|
|
348
349
|
export interface ESearchResponseContainer {
|
|
349
350
|
eSearchResult: ESearchResultContent;
|
|
350
351
|
}
|
|
352
|
+
export interface ESearchResult {
|
|
353
|
+
count: number;
|
|
354
|
+
retmax: number;
|
|
355
|
+
retstart: number;
|
|
356
|
+
queryKey?: string;
|
|
357
|
+
webEnv?: string;
|
|
358
|
+
idList: string[];
|
|
359
|
+
queryTranslation: string;
|
|
360
|
+
errorList?: ESearchErrorList;
|
|
361
|
+
warningList?: ESearchWarningList;
|
|
362
|
+
}
|
|
363
|
+
export interface EFetchArticleSet {
|
|
364
|
+
articles: ParsedArticle[];
|
|
365
|
+
}
|
|
@@ -14,6 +14,10 @@ export function extractAuthors(authorListXml) {
|
|
|
14
14
|
return [];
|
|
15
15
|
const authors = ensureArray(authorListXml.Author);
|
|
16
16
|
return authors.map((auth) => {
|
|
17
|
+
const collectiveName = getText(auth.CollectiveName);
|
|
18
|
+
if (collectiveName) {
|
|
19
|
+
return { collectiveName };
|
|
20
|
+
}
|
|
17
21
|
let affiliation = "";
|
|
18
22
|
const affiliations = ensureArray(auth.AffiliationInfo);
|
|
19
23
|
if (affiliations.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/pubmed-mcp-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server enabling AI agents to intelligently search, retrieve, and analyze biomedical literature from PubMed via NCBI E-utilities. Built on the mcp-ts-template for robust, production-ready performance.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -32,31 +32,32 @@
|
|
|
32
32
|
"inspector": "mcp-inspector --config mcp.json --server pubmed-mcp-server"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@hono/node-server": "^1.
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
35
|
+
"@hono/node-server": "^1.15.0",
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
37
37
|
"@types/jsonwebtoken": "^9.0.10",
|
|
38
|
-
"@types/node": "^24.0.
|
|
38
|
+
"@types/node": "^24.0.10",
|
|
39
39
|
"@types/sanitize-html": "^2.16.0",
|
|
40
40
|
"@types/validator": "13.15.2",
|
|
41
41
|
"axios": "^1.10.0",
|
|
42
|
+
"chart.js": "^4.5.0",
|
|
43
|
+
"chartjs-node-canvas": "^5.0.0",
|
|
42
44
|
"chrono-node": "^2.8.3",
|
|
45
|
+
"citation-js": "^0.7.20",
|
|
43
46
|
"dotenv": "^16.5.0",
|
|
44
47
|
"fast-xml-parser": "^5.2.5",
|
|
45
|
-
"hono": "^4.8.
|
|
48
|
+
"hono": "^4.8.4",
|
|
46
49
|
"jose": "^6.0.11",
|
|
47
50
|
"jsonwebtoken": "^9.0.2",
|
|
48
|
-
"openai": "^5.
|
|
51
|
+
"openai": "^5.8.2",
|
|
49
52
|
"partial-json": "^0.1.7",
|
|
50
53
|
"sanitize-html": "^2.17.0",
|
|
51
54
|
"tiktoken": "^1.0.21",
|
|
52
55
|
"ts-node": "^10.9.2",
|
|
53
56
|
"typescript": "^5.8.3",
|
|
54
57
|
"validator": "13.15.15",
|
|
55
|
-
"chart.js": "^4.5.0",
|
|
56
|
-
"chartjs-node-canvas": "^5.0.0",
|
|
57
58
|
"winston": "^3.17.0",
|
|
58
59
|
"winston-transport": "^4.9.0",
|
|
59
|
-
"zod": "^3.25.
|
|
60
|
+
"zod": "^3.25.74"
|
|
60
61
|
},
|
|
61
62
|
"keywords": [
|
|
62
63
|
"mcp",
|
|
@@ -98,7 +99,8 @@
|
|
|
98
99
|
"devDependencies": {
|
|
99
100
|
"@types/js-yaml": "^4.0.9",
|
|
100
101
|
"js-yaml": "^4.1.0",
|
|
101
|
-
"
|
|
102
|
-
"
|
|
102
|
+
"patch-package": "^8.0.0",
|
|
103
|
+
"prettier": "^3.6.2",
|
|
104
|
+
"typedoc": "^0.28.7"
|
|
103
105
|
}
|
|
104
106
|
}
|