@cyanheads/pubmed-mcp-server 1.1.2 → 1.2.1
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/README.md +25 -24
- package/dist/config/index.js +39 -1
- package/dist/mcp-server/server.d.ts +0 -7
- package/dist/mcp-server/server.js +17 -53
- package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +6 -2
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +102 -311
- package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +1 -1
- package/dist/mcp-server/tools/fetchPubMedContent/registration.js +50 -18
- package/dist/mcp-server/tools/generatePubMedChart/logic.d.ts +9 -28
- package/dist/mcp-server/tools/generatePubMedChart/logic.js +137 -198
- package/dist/mcp-server/tools/generatePubMedChart/registration.d.ts +1 -1
- package/dist/mcp-server/tools/generatePubMedChart/registration.js +62 -27
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +1 -1
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +3 -3
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +1 -1
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +27 -4
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +59 -51
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +1 -1
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +1 -26
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +58 -62
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +2 -5
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +7 -40
- package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +1 -1
- package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +55 -19
- package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +12 -10
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +68 -121
- package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +1 -1
- package/dist/mcp-server/tools/searchPubMedArticles/registration.js +54 -21
- package/dist/mcp-server/transports/httpTransport.d.ts +0 -8
- package/dist/mcp-server/transports/httpTransport.js +57 -345
- package/dist/utils/security/rateLimiter.d.ts +4 -0
- package/dist/utils/security/rateLimiter.js +4 -0
- package/package.json +7 -15
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +0 -79
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +0 -82
- package/dist/mcp-server/resources/echoResource/index.d.ts +0 -13
- package/dist/mcp-server/resources/echoResource/index.js +0 -13
- package/dist/mcp-server/resources/echoResource/registration.d.ts +0 -30
- package/dist/mcp-server/resources/echoResource/registration.js +0 -168
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +0 -6
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +0 -6
|
@@ -97,70 +97,35 @@ export const FetchPubMedContentInputSchema = z
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
|
-
function parsePubMedArticleSet(xmlData,
|
|
101
|
-
input, parentContext) {
|
|
100
|
+
function parsePubMedArticleSet(xmlData, input, parentContext) {
|
|
102
101
|
const articles = [];
|
|
103
102
|
const operationContext = requestContextService.createRequestContext({
|
|
104
103
|
parentRequestId: parentContext.requestId,
|
|
105
104
|
operation: "parsePubMedArticleSet",
|
|
106
105
|
});
|
|
107
|
-
// Type guard for xmlData
|
|
108
106
|
if (!xmlData ||
|
|
109
107
|
typeof xmlData !== "object" ||
|
|
110
108
|
!("PubmedArticleSet" in xmlData)) {
|
|
111
|
-
|
|
109
|
+
throw new McpError(BaseErrorCode.PARSING_ERROR, "Invalid or unexpected structure for xmlData in parsePubMedArticleSet.", {
|
|
112
110
|
...operationContext,
|
|
113
111
|
xmlDataType: typeof xmlData,
|
|
114
112
|
xmlDataPreview: sanitizeInputForLogging(JSON.stringify(xmlData).substring(0, 200)),
|
|
115
113
|
});
|
|
116
|
-
return articles;
|
|
117
114
|
}
|
|
118
|
-
const typedXmlData = xmlData;
|
|
115
|
+
const typedXmlData = xmlData;
|
|
119
116
|
const articleSet = typedXmlData.PubmedArticleSet;
|
|
120
117
|
if (!articleSet || !articleSet.PubmedArticle) {
|
|
121
|
-
logger.warning("PubmedArticleSet or PubmedArticle array not found in EFetch XML response.",
|
|
122
|
-
...operationContext,
|
|
123
|
-
xmlDataPreview: sanitizeInputForLogging(JSON.stringify(typedXmlData).substring(0, 200)),
|
|
124
|
-
}));
|
|
118
|
+
logger.warning("PubmedArticleSet or PubmedArticle array not found in EFetch XML response.", operationContext);
|
|
125
119
|
return articles;
|
|
126
120
|
}
|
|
127
121
|
const pubmedArticlesXml = ensureArray(articleSet.PubmedArticle);
|
|
128
|
-
logger.debug("Result of ensureArray(articleSet.PubmedArticle):", {
|
|
129
|
-
...operationContext,
|
|
130
|
-
pubmedArticlesXmlPreview: sanitizeInputForLogging(JSON.stringify(pubmedArticlesXml).substring(0, 500)),
|
|
131
|
-
isPubmedArticlesXmlArray: Array.isArray(pubmedArticlesXml),
|
|
132
|
-
pubmedArticlesXmlLength: Array.isArray(pubmedArticlesXml)
|
|
133
|
-
? pubmedArticlesXml.length
|
|
134
|
-
: undefined,
|
|
135
|
-
});
|
|
136
|
-
if (Array.isArray(pubmedArticlesXml) && pubmedArticlesXml.length > 0) {
|
|
137
|
-
logger.debug("First item of pubmedArticlesXml:", {
|
|
138
|
-
...operationContext,
|
|
139
|
-
firstItemPreview: sanitizeInputForLogging(JSON.stringify(pubmedArticlesXml[0]).substring(0, 500)),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
122
|
for (const articleXml of pubmedArticlesXml) {
|
|
143
|
-
if (!articleXml || typeof articleXml !== "object")
|
|
144
|
-
logger.warning("Skipping invalid articleXml item in pubmedArticlesXml array", {
|
|
145
|
-
...operationContext,
|
|
146
|
-
articleXmlItem: sanitizeInputForLogging(articleXml),
|
|
147
|
-
});
|
|
123
|
+
if (!articleXml || typeof articleXml !== "object")
|
|
148
124
|
continue;
|
|
149
|
-
}
|
|
150
125
|
const medlineCitation = articleXml.MedlineCitation;
|
|
151
|
-
if (!medlineCitation)
|
|
152
|
-
logger.warning("MedlineCitation not found in articleXml, skipping.", {
|
|
153
|
-
...operationContext,
|
|
154
|
-
articleXmlPreview: sanitizeInputForLogging(JSON.stringify(articleXml).substring(0, 200)),
|
|
155
|
-
});
|
|
126
|
+
if (!medlineCitation)
|
|
156
127
|
continue;
|
|
157
|
-
}
|
|
158
128
|
const pmid = extractPmid(medlineCitation);
|
|
159
|
-
logger.debug("Extracted PMID from MedlineCitation:", {
|
|
160
|
-
...operationContext,
|
|
161
|
-
extractedPmid: pmid,
|
|
162
|
-
medlineCitationPreview: sanitizeInputForLogging(JSON.stringify(medlineCitation).substring(0, 200)),
|
|
163
|
-
});
|
|
164
129
|
if (!pmid)
|
|
165
130
|
continue;
|
|
166
131
|
const articleNode = medlineCitation.Article;
|
|
@@ -204,121 +169,33 @@ input, parentContext) {
|
|
|
204
169
|
return articles;
|
|
205
170
|
}
|
|
206
171
|
export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
207
|
-
// Manual validation for conditions superRefine should catch,
|
|
208
|
-
// as SDK might call handler even with refinement issues.
|
|
209
|
-
if (input.queryKey && !input.webEnv) {
|
|
210
|
-
return {
|
|
211
|
-
content: [
|
|
212
|
-
{
|
|
213
|
-
type: "text",
|
|
214
|
-
text: JSON.stringify({
|
|
215
|
-
error: {
|
|
216
|
-
code: BaseErrorCode.VALIDATION_ERROR,
|
|
217
|
-
message: "webEnv is required if queryKey is provided.",
|
|
218
|
-
},
|
|
219
|
-
}),
|
|
220
|
-
},
|
|
221
|
-
],
|
|
222
|
-
isError: true,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
if (!input.queryKey && input.webEnv) {
|
|
226
|
-
return {
|
|
227
|
-
content: [
|
|
228
|
-
{
|
|
229
|
-
type: "text",
|
|
230
|
-
text: JSON.stringify({
|
|
231
|
-
error: {
|
|
232
|
-
code: BaseErrorCode.VALIDATION_ERROR,
|
|
233
|
-
message: "queryKey is required if webEnv is provided.",
|
|
234
|
-
},
|
|
235
|
-
}),
|
|
236
|
-
},
|
|
237
|
-
],
|
|
238
|
-
isError: true,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
if (input.pmids &&
|
|
242
|
-
input.pmids.length > 0 &&
|
|
243
|
-
(input.queryKey || input.webEnv)) {
|
|
244
|
-
return {
|
|
245
|
-
content: [
|
|
246
|
-
{
|
|
247
|
-
type: "text",
|
|
248
|
-
text: JSON.stringify({
|
|
249
|
-
error: {
|
|
250
|
-
code: BaseErrorCode.VALIDATION_ERROR,
|
|
251
|
-
message: "Cannot use pmids and queryKey/webEnv simultaneously. Please choose one method.",
|
|
252
|
-
},
|
|
253
|
-
}),
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
isError: true,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
if ((input.retstart !== undefined || input.retmax !== undefined) &&
|
|
260
|
-
!(input.queryKey && input.webEnv)) {
|
|
261
|
-
return {
|
|
262
|
-
content: [
|
|
263
|
-
{
|
|
264
|
-
type: "text",
|
|
265
|
-
text: JSON.stringify({
|
|
266
|
-
error: {
|
|
267
|
-
code: BaseErrorCode.VALIDATION_ERROR,
|
|
268
|
-
message: "retstart/retmax can only be used with queryKey and webEnv.",
|
|
269
|
-
},
|
|
270
|
-
}),
|
|
271
|
-
},
|
|
272
|
-
],
|
|
273
|
-
isError: true,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
// SuperRefine also checks: if ((!data.pmids || data.pmids.length === 0) && !(data.queryKey && data.webEnv))
|
|
277
|
-
// This should ideally be caught before handler, but as a safeguard:
|
|
278
|
-
if ((!input.pmids || input.pmids.length === 0) &&
|
|
279
|
-
!(input.queryKey && input.webEnv)) {
|
|
280
|
-
return {
|
|
281
|
-
content: [
|
|
282
|
-
{
|
|
283
|
-
type: "text",
|
|
284
|
-
text: JSON.stringify({
|
|
285
|
-
error: {
|
|
286
|
-
code: BaseErrorCode.VALIDATION_ERROR,
|
|
287
|
-
message: "Either pmids (non-empty array) or both queryKey and webEnv must be provided.",
|
|
288
|
-
},
|
|
289
|
-
}),
|
|
290
|
-
},
|
|
291
|
-
],
|
|
292
|
-
isError: true,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
const ncbiService = getNcbiService();
|
|
296
172
|
const toolLogicContext = requestContextService.createRequestContext({
|
|
297
173
|
parentRequestId: parentRequestContext.requestId,
|
|
298
174
|
operation: "fetchPubMedContentLogic",
|
|
299
175
|
input: sanitizeInputForLogging(input),
|
|
300
176
|
});
|
|
177
|
+
const validationResult = FetchPubMedContentInputSchema.safeParse(input);
|
|
178
|
+
if (!validationResult.success) {
|
|
179
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, validationResult.error.errors[0]?.message || "Invalid input", { ...toolLogicContext, details: validationResult.error.flatten() });
|
|
180
|
+
}
|
|
181
|
+
const ncbiService = getNcbiService();
|
|
301
182
|
logger.info("Executing fetch_pubmed_content tool", toolLogicContext);
|
|
302
183
|
const eFetchParams = { db: "pubmed" };
|
|
303
184
|
let usingHistory = false;
|
|
304
185
|
if (input.queryKey && input.webEnv) {
|
|
305
186
|
usingHistory = true;
|
|
306
187
|
eFetchParams.query_key = input.queryKey;
|
|
307
|
-
eFetchParams.WebEnv = input.webEnv;
|
|
308
|
-
if (input.retstart !== undefined)
|
|
188
|
+
eFetchParams.WebEnv = input.webEnv;
|
|
189
|
+
if (input.retstart !== undefined)
|
|
309
190
|
eFetchParams.retstart = String(input.retstart);
|
|
310
|
-
|
|
311
|
-
if (input.retmax !== undefined) {
|
|
191
|
+
if (input.retmax !== undefined)
|
|
312
192
|
eFetchParams.retmax = String(input.retmax);
|
|
313
|
-
}
|
|
314
193
|
}
|
|
315
194
|
else if (input.pmids && input.pmids.length > 0) {
|
|
316
195
|
eFetchParams.id = input.pmids.join(",");
|
|
317
196
|
}
|
|
318
|
-
|
|
319
|
-
let serviceRetmode = "xml"; // Renamed to avoid conflict with local retmode variable if any
|
|
197
|
+
let serviceRetmode = "xml";
|
|
320
198
|
let rettype;
|
|
321
|
-
// responseContentType is determined by input.outputFormat at the end
|
|
322
199
|
switch (input.detailLevel) {
|
|
323
200
|
case "full_xml":
|
|
324
201
|
serviceRetmode = "xml";
|
|
@@ -329,191 +206,105 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
329
206
|
break;
|
|
330
207
|
case "abstract_plus":
|
|
331
208
|
case "citation_data":
|
|
332
|
-
serviceRetmode = "xml";
|
|
209
|
+
serviceRetmode = "xml";
|
|
333
210
|
break;
|
|
334
211
|
}
|
|
335
212
|
eFetchParams.retmode = serviceRetmode;
|
|
336
213
|
if (rettype)
|
|
337
214
|
eFetchParams.rettype = rettype;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
let match;
|
|
353
|
-
while ((match = pmidRegex.exec(medlineText)) !== null) {
|
|
354
|
-
foundPmidsInMedline.add(match[1]);
|
|
355
|
-
}
|
|
356
|
-
let notFoundPmids = [];
|
|
357
|
-
if (input.pmids && input.pmids.length > 0) {
|
|
358
|
-
notFoundPmids = input.pmids.filter((pmid) => !foundPmidsInMedline.has(pmid));
|
|
359
|
-
}
|
|
360
|
-
structuredResponseData = {
|
|
361
|
-
requestedPmids: input.pmids || "N/A (used history query)",
|
|
362
|
-
articles: [
|
|
363
|
-
{
|
|
364
|
-
pmids: input.pmids || "N/A (used history query)",
|
|
365
|
-
medlineText: medlineText,
|
|
366
|
-
},
|
|
367
|
-
],
|
|
368
|
-
notFoundPmids: usingHistory
|
|
369
|
-
? "N/A (used history query)"
|
|
370
|
-
: notFoundPmids,
|
|
371
|
-
eFetchDetails: {
|
|
372
|
-
urls: [eFetchUrl],
|
|
373
|
-
requestMethod: input.pmids && input.pmids.length > 200 && !usingHistory
|
|
374
|
-
? "POST"
|
|
375
|
-
: "GET",
|
|
376
|
-
},
|
|
377
|
-
};
|
|
378
|
-
if (input.outputFormat === "raw_text") {
|
|
379
|
-
finalOutputText = String(eFetchResponseData);
|
|
380
|
-
}
|
|
381
|
-
else {
|
|
382
|
-
finalOutputText = JSON.stringify(structuredResponseData);
|
|
383
|
-
}
|
|
215
|
+
const eFetchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
|
|
216
|
+
const eFetchQueryString = new URLSearchParams(eFetchParams).toString();
|
|
217
|
+
const eFetchUrl = `${eFetchBase}?${eFetchQueryString}`;
|
|
218
|
+
const shouldReturnRawXml = input.detailLevel === "full_xml" && input.outputFormat === "raw_text";
|
|
219
|
+
const eFetchResponseData = await ncbiService.eFetch(eFetchParams, toolLogicContext, { retmode: serviceRetmode, rettype, returnRawXml: shouldReturnRawXml });
|
|
220
|
+
let finalOutputText;
|
|
221
|
+
let articlesCount = 0;
|
|
222
|
+
if (input.detailLevel === "medline_text") {
|
|
223
|
+
const medlineText = String(eFetchResponseData);
|
|
224
|
+
const foundPmidsInMedline = new Set();
|
|
225
|
+
const pmidRegex = /^PMID- (\d+)/gm;
|
|
226
|
+
let match;
|
|
227
|
+
while ((match = pmidRegex.exec(medlineText)) !== null) {
|
|
228
|
+
foundPmidsInMedline.add(match[1]);
|
|
384
229
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
finalOutputText = String(eFetchResponseData);
|
|
389
|
-
// Optionally, wrap it in a minimal JSON structure if that's preferred for raw_text output consistency
|
|
390
|
-
// For now, returning the direct XML string as per user expectation for "raw_text"
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
// outputFormat is 'json', so eFetchResponseData is the parsed XML object
|
|
394
|
-
const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
|
|
395
|
-
const articlesPayload = [];
|
|
396
|
-
const foundPmidsInXml = new Set();
|
|
397
|
-
for (const articleXml of articlesXml) {
|
|
398
|
-
let pmid = "unknown_pmid";
|
|
399
|
-
if (articleXml?.MedlineCitation) {
|
|
400
|
-
const extracted = extractPmid(articleXml.MedlineCitation);
|
|
401
|
-
if (extracted) {
|
|
402
|
-
pmid = extracted;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
if (pmid !== "unknown_pmid") {
|
|
406
|
-
foundPmidsInXml.add(pmid);
|
|
407
|
-
}
|
|
408
|
-
articlesPayload.push({
|
|
409
|
-
pmid: pmid,
|
|
410
|
-
fullXmlContent: articleXml,
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
let notFoundPmids = [];
|
|
414
|
-
if (input.pmids && input.pmids.length > 0) {
|
|
415
|
-
notFoundPmids = input.pmids.filter((pmid) => !foundPmidsInXml.has(pmid));
|
|
416
|
-
}
|
|
417
|
-
else if (usingHistory) {
|
|
418
|
-
notFoundPmids = "N/A (used history query)";
|
|
419
|
-
}
|
|
420
|
-
structuredResponseData = {
|
|
421
|
-
requestedPmids: input.pmids || "N/A (used history query)",
|
|
422
|
-
articles: articlesPayload,
|
|
423
|
-
notFoundPmids,
|
|
424
|
-
eFetchDetails: {
|
|
425
|
-
urls: [eFetchUrl],
|
|
426
|
-
requestMethod: input.pmids && input.pmids.length > 200 && !usingHistory
|
|
427
|
-
? "POST"
|
|
428
|
-
: "GET",
|
|
429
|
-
},
|
|
430
|
-
};
|
|
431
|
-
finalOutputText = JSON.stringify(structuredResponseData);
|
|
432
|
-
}
|
|
230
|
+
articlesCount = foundPmidsInMedline.size;
|
|
231
|
+
if (input.outputFormat === "raw_text") {
|
|
232
|
+
finalOutputText = medlineText;
|
|
433
233
|
}
|
|
434
234
|
else {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
let notFoundPmids = [];
|
|
440
|
-
if (input.pmids && input.pmids.length > 0) {
|
|
441
|
-
notFoundPmids = input.pmids.filter((pmid) => !foundPmids.has(pmid));
|
|
442
|
-
}
|
|
443
|
-
else if (usingHistory) {
|
|
444
|
-
notFoundPmids = "N/A (used history query)";
|
|
445
|
-
}
|
|
446
|
-
structuredResponseData = {
|
|
447
|
-
requestedPmids: input.pmids || "N/A (used history query)",
|
|
448
|
-
articles: parsedArticles,
|
|
235
|
+
const notFoundPmids = input.pmids?.filter((pmid) => !foundPmidsInMedline.has(pmid)) || [];
|
|
236
|
+
finalOutputText = JSON.stringify({
|
|
237
|
+
requestedPmids: input.pmids || "N/A (history query)",
|
|
238
|
+
articles: [{ medlineText }],
|
|
449
239
|
notFoundPmids,
|
|
450
|
-
eFetchDetails: {
|
|
451
|
-
|
|
452
|
-
requestMethod: input.pmids && input.pmids.length > 200 && !usingHistory
|
|
453
|
-
? "POST"
|
|
454
|
-
: "GET",
|
|
455
|
-
},
|
|
456
|
-
};
|
|
457
|
-
if (input.detailLevel === "citation_data") {
|
|
458
|
-
structuredResponseData.articles = structuredResponseData.articles.map((article) => ({
|
|
459
|
-
pmid: article.pmid,
|
|
460
|
-
title: article.title,
|
|
461
|
-
authors: article.authors?.map((a) => ({
|
|
462
|
-
lastName: a.lastName,
|
|
463
|
-
initials: a.initials,
|
|
464
|
-
})),
|
|
465
|
-
journalInfo: {
|
|
466
|
-
title: article.journalInfo?.title,
|
|
467
|
-
isoAbbreviation: article.journalInfo?.isoAbbreviation,
|
|
468
|
-
volume: article.journalInfo?.volume,
|
|
469
|
-
issue: article.journalInfo?.issue,
|
|
470
|
-
pages: article.journalInfo?.pages,
|
|
471
|
-
year: article.journalInfo?.publicationDate?.year,
|
|
472
|
-
},
|
|
473
|
-
doi: article.doi,
|
|
474
|
-
// Conditionally include meshTerms if the input flag was set (it defaults to true)
|
|
475
|
-
...(input.includeMeshTerms && { meshTerms: article.meshTerms }),
|
|
476
|
-
}));
|
|
477
|
-
}
|
|
478
|
-
// For abstract_plus and citation_data, outputFormat 'raw_text' doesn't make sense,
|
|
479
|
-
// as the data is inherently structured. So, always output JSON.
|
|
480
|
-
finalOutputText = JSON.stringify(structuredResponseData);
|
|
240
|
+
eFetchDetails: { urls: [eFetchUrl] },
|
|
241
|
+
});
|
|
481
242
|
}
|
|
482
|
-
return {
|
|
483
|
-
content: [
|
|
484
|
-
{
|
|
485
|
-
type: "text",
|
|
486
|
-
text: finalOutputText,
|
|
487
|
-
},
|
|
488
|
-
],
|
|
489
|
-
isError: false,
|
|
490
|
-
};
|
|
491
243
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
244
|
+
else if (input.detailLevel === "full_xml") {
|
|
245
|
+
if (input.outputFormat === "raw_text") {
|
|
246
|
+
finalOutputText = String(eFetchResponseData);
|
|
247
|
+
articlesCount = (finalOutputText.match(/<PubmedArticle>/g) || []).length;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
|
|
251
|
+
articlesCount = articlesXml.length;
|
|
252
|
+
const foundPmidsInXml = new Set();
|
|
253
|
+
const articlesPayload = articlesXml.map((articleXml) => {
|
|
254
|
+
const pmid = extractPmid(articleXml.MedlineCitation) || "unknown_pmid";
|
|
255
|
+
if (pmid !== "unknown_pmid")
|
|
256
|
+
foundPmidsInXml.add(pmid);
|
|
257
|
+
return { pmid, fullXmlContent: articleXml };
|
|
258
|
+
});
|
|
259
|
+
const notFoundPmids = input.pmids?.filter((pmid) => !foundPmidsInXml.has(pmid)) || [];
|
|
260
|
+
finalOutputText = JSON.stringify({
|
|
261
|
+
requestedPmids: input.pmids || "N/A (history query)",
|
|
262
|
+
articles: articlesPayload,
|
|
263
|
+
notFoundPmids,
|
|
264
|
+
eFetchDetails: { urls: [eFetchUrl] },
|
|
500
265
|
});
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
const parsedArticles = parsePubMedArticleSet(eFetchResponseData, input, toolLogicContext);
|
|
270
|
+
articlesCount = parsedArticles.length;
|
|
271
|
+
const foundPmids = new Set(parsedArticles.map((p) => p.pmid));
|
|
272
|
+
const notFoundPmids = input.pmids?.filter((pmid) => !foundPmids.has(pmid)) || [];
|
|
273
|
+
let articlesToReturn = parsedArticles;
|
|
274
|
+
if (input.detailLevel === "citation_data") {
|
|
275
|
+
articlesToReturn = parsedArticles.map((article) => ({
|
|
276
|
+
pmid: article.pmid,
|
|
277
|
+
title: article.title,
|
|
278
|
+
authors: article.authors?.map((a) => ({
|
|
279
|
+
lastName: a.lastName,
|
|
280
|
+
initials: a.initials,
|
|
281
|
+
})),
|
|
282
|
+
journalInfo: {
|
|
283
|
+
title: article.journalInfo?.title,
|
|
284
|
+
isoAbbreviation: article.journalInfo?.isoAbbreviation,
|
|
285
|
+
volume: article.journalInfo?.volume,
|
|
286
|
+
issue: article.journalInfo?.issue,
|
|
287
|
+
pages: article.journalInfo?.pages,
|
|
288
|
+
year: article.journalInfo?.publicationDate?.year,
|
|
514
289
|
},
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
290
|
+
doi: article.doi,
|
|
291
|
+
...(input.includeMeshTerms && { meshTerms: article.meshTerms }),
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
finalOutputText = JSON.stringify({
|
|
295
|
+
requestedPmids: input.pmids || "N/A (history query)",
|
|
296
|
+
articles: articlesToReturn,
|
|
297
|
+
notFoundPmids,
|
|
298
|
+
eFetchDetails: { urls: [eFetchUrl] },
|
|
299
|
+
});
|
|
518
300
|
}
|
|
301
|
+
logger.notice("Successfully executed fetch_pubmed_content tool.", {
|
|
302
|
+
...toolLogicContext,
|
|
303
|
+
articlesReturned: articlesCount,
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
content: finalOutputText,
|
|
307
|
+
articlesReturned: articlesCount,
|
|
308
|
+
eFetchUrl,
|
|
309
|
+
};
|
|
519
310
|
}
|
|
@@ -7,4 +7,4 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
7
7
|
* Registers the fetch_pubmed_content tool with the MCP server.
|
|
8
8
|
* @param server - The McpServer instance.
|
|
9
9
|
*/
|
|
10
|
-
export declare function registerFetchPubMedContentTool(server: McpServer): void
|
|
10
|
+
export declare function registerFetchPubMedContentTool(server: McpServer): Promise<void>;
|
|
@@ -3,35 +3,67 @@
|
|
|
3
3
|
* @module src/mcp-server/tools/fetchPubMedContent/registration
|
|
4
4
|
*/
|
|
5
5
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
6
|
-
import { ErrorHandler, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
7
7
|
import { FetchPubMedContentInputSchema, fetchPubMedContentLogic, } from "./logic.js";
|
|
8
8
|
/**
|
|
9
9
|
* Registers the fetch_pubmed_content tool with the MCP server.
|
|
10
10
|
* @param server - The McpServer instance.
|
|
11
11
|
*/
|
|
12
|
-
export function registerFetchPubMedContentTool(server) {
|
|
12
|
+
export async function registerFetchPubMedContentTool(server) {
|
|
13
13
|
const operation = "registerFetchPubMedContentTool";
|
|
14
|
+
const toolName = "fetch_pubmed_content";
|
|
15
|
+
const toolDescription = "Fetches detailed information from PubMed using NCBI EFetch. Can be used with a direct list of PMIDs or with queryKey/webEnv from an ESearch history entry. Supports pagination (retstart, retmax) when using history. Available 'detailLevel' options: 'abstract_plus' (parsed title, abstract, authors, journal, keywords, DOI, optional MeSH/grant info), 'full_xml' (JSON representation of the PubMedArticle XML structure), 'medline_text' (MEDLINE format), or 'citation_data' (minimal data for citations). Returns a JSON object containing results, any PMIDs not found (if applicable), and EFetch details.";
|
|
14
16
|
const context = requestContextService.createRequestContext({ operation });
|
|
15
|
-
|
|
16
|
-
server.tool(
|
|
17
|
-
async (input, toolContext) => {
|
|
18
|
-
// Added 'any' type for toolContext
|
|
17
|
+
await ErrorHandler.tryCatch(async () => {
|
|
18
|
+
server.tool(toolName, toolDescription, FetchPubMedContentInputSchema._def.schema.shape, async (input, toolContext) => {
|
|
19
19
|
const richContext = requestContextService.createRequestContext({
|
|
20
20
|
parentRequestId: context.requestId,
|
|
21
21
|
operation: "fetchPubMedContentToolHandler",
|
|
22
22
|
mcpToolContext: toolContext,
|
|
23
|
+
input,
|
|
23
24
|
});
|
|
24
|
-
|
|
25
|
+
try {
|
|
26
|
+
const result = await fetchPubMedContentLogic(input, richContext);
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: result.content }],
|
|
29
|
+
isError: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const handledError = ErrorHandler.handleError(error, {
|
|
34
|
+
operation: "fetchPubMedContentToolHandler",
|
|
35
|
+
context: richContext,
|
|
36
|
+
input,
|
|
37
|
+
rethrow: false,
|
|
38
|
+
});
|
|
39
|
+
const mcpError = handledError instanceof McpError
|
|
40
|
+
? handledError
|
|
41
|
+
: new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred while fetching PubMed content.", {
|
|
42
|
+
originalErrorName: handledError.name,
|
|
43
|
+
originalErrorMessage: handledError.message,
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: JSON.stringify({
|
|
50
|
+
error: {
|
|
51
|
+
code: mcpError.code,
|
|
52
|
+
message: mcpError.message,
|
|
53
|
+
details: mcpError.details,
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
25
61
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
34
|
-
critical: true,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
62
|
+
logger.notice(`Tool '${toolName}' registered.`, context);
|
|
63
|
+
}, {
|
|
64
|
+
operation,
|
|
65
|
+
context,
|
|
66
|
+
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
67
|
+
critical: true,
|
|
68
|
+
});
|
|
37
69
|
}
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Core logic for the generate_pubmed_chart tool.
|
|
3
|
-
* Generates charts from parameterized input by creating Vega-Lite specifications.
|
|
4
|
-
* @module src/mcp-server/tools/generatePubMedChart/logic
|
|
5
|
-
*/
|
|
6
|
-
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
7
1
|
import { z } from "zod";
|
|
8
2
|
import { RequestContext } from "../../../utils/index.js";
|
|
9
3
|
export declare const GeneratePubMedChartInputSchema: z.ZodObject<{
|
|
10
|
-
chartType: z.ZodEnum<["bar", "line", "scatter"]>;
|
|
4
|
+
chartType: z.ZodEnum<["bar", "line", "scatter", "pie", "doughnut", "bubble", "radar", "polarArea"]>;
|
|
11
5
|
title: z.ZodOptional<z.ZodString>;
|
|
12
6
|
width: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
13
7
|
height: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
@@ -15,33 +9,21 @@ export declare const GeneratePubMedChartInputSchema: z.ZodObject<{
|
|
|
15
9
|
outputFormat: z.ZodDefault<z.ZodEnum<["png"]>>;
|
|
16
10
|
xField: z.ZodString;
|
|
17
11
|
yField: z.ZodString;
|
|
18
|
-
xFieldType: z.ZodOptional<z.ZodEnum<["nominal", "ordinal", "quantitative", "temporal"]>>;
|
|
19
|
-
yFieldType: z.ZodOptional<z.ZodEnum<["nominal", "ordinal", "quantitative", "temporal"]>>;
|
|
20
|
-
colorField: z.ZodOptional<z.ZodString>;
|
|
21
|
-
colorFieldType: z.ZodOptional<z.ZodEnum<["nominal", "ordinal", "quantitative", "temporal"]>>;
|
|
22
12
|
seriesField: z.ZodOptional<z.ZodString>;
|
|
23
|
-
seriesFieldType: z.ZodOptional<z.ZodEnum<["nominal", "ordinal", "quantitative", "temporal"]>>;
|
|
24
13
|
sizeField: z.ZodOptional<z.ZodString>;
|
|
25
|
-
sizeFieldType: z.ZodOptional<z.ZodEnum<["quantitative", "ordinal"]>>;
|
|
26
14
|
}, "strip", z.ZodTypeAny, {
|
|
27
15
|
width: number;
|
|
28
16
|
height: number;
|
|
29
17
|
outputFormat: "png";
|
|
30
|
-
chartType: "bar" | "line" | "scatter";
|
|
18
|
+
chartType: "bar" | "line" | "scatter" | "pie" | "doughnut" | "bubble" | "radar" | "polarArea";
|
|
31
19
|
dataValues: Record<string, any>[];
|
|
32
20
|
xField: string;
|
|
33
21
|
yField: string;
|
|
34
22
|
title?: string | undefined;
|
|
35
|
-
xFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
|
|
36
|
-
yFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
|
|
37
|
-
colorField?: string | undefined;
|
|
38
|
-
colorFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
|
|
39
23
|
seriesField?: string | undefined;
|
|
40
|
-
seriesFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
|
|
41
24
|
sizeField?: string | undefined;
|
|
42
|
-
sizeFieldType?: "ordinal" | "quantitative" | undefined;
|
|
43
25
|
}, {
|
|
44
|
-
chartType: "bar" | "line" | "scatter";
|
|
26
|
+
chartType: "bar" | "line" | "scatter" | "pie" | "doughnut" | "bubble" | "radar" | "polarArea";
|
|
45
27
|
dataValues: Record<string, any>[];
|
|
46
28
|
xField: string;
|
|
47
29
|
yField: string;
|
|
@@ -49,14 +31,13 @@ export declare const GeneratePubMedChartInputSchema: z.ZodObject<{
|
|
|
49
31
|
width?: number | undefined;
|
|
50
32
|
height?: number | undefined;
|
|
51
33
|
outputFormat?: "png" | undefined;
|
|
52
|
-
xFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
|
|
53
|
-
yFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
|
|
54
|
-
colorField?: string | undefined;
|
|
55
|
-
colorFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
|
|
56
34
|
seriesField?: string | undefined;
|
|
57
|
-
seriesFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
|
|
58
35
|
sizeField?: string | undefined;
|
|
59
|
-
sizeFieldType?: "ordinal" | "quantitative" | undefined;
|
|
60
36
|
}>;
|
|
61
37
|
export type GeneratePubMedChartInput = z.infer<typeof GeneratePubMedChartInputSchema>;
|
|
62
|
-
export
|
|
38
|
+
export type GeneratePubMedChartOutput = {
|
|
39
|
+
base64Data: string;
|
|
40
|
+
chartType: string;
|
|
41
|
+
dataPoints: number;
|
|
42
|
+
};
|
|
43
|
+
export declare function generatePubMedChartLogic(input: GeneratePubMedChartInput, parentRequestContext: RequestContext): Promise<GeneratePubMedChartOutput>;
|