@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.
Files changed (41) hide show
  1. package/README.md +25 -24
  2. package/dist/config/index.js +39 -1
  3. package/dist/mcp-server/server.d.ts +0 -7
  4. package/dist/mcp-server/server.js +17 -53
  5. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +6 -2
  6. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +102 -311
  7. package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +1 -1
  8. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +50 -18
  9. package/dist/mcp-server/tools/generatePubMedChart/logic.d.ts +9 -28
  10. package/dist/mcp-server/tools/generatePubMedChart/logic.js +137 -198
  11. package/dist/mcp-server/tools/generatePubMedChart/registration.d.ts +1 -1
  12. package/dist/mcp-server/tools/generatePubMedChart/registration.js +62 -27
  13. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +1 -1
  14. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +3 -3
  15. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +1 -1
  16. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +27 -4
  17. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +59 -51
  18. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +1 -1
  19. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +1 -26
  20. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +58 -62
  21. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +2 -5
  22. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +7 -40
  23. package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +1 -1
  24. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +55 -19
  25. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +12 -10
  26. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +68 -121
  27. package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +1 -1
  28. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +54 -21
  29. package/dist/mcp-server/transports/httpTransport.d.ts +0 -8
  30. package/dist/mcp-server/transports/httpTransport.js +57 -345
  31. package/dist/utils/security/rateLimiter.d.ts +4 -0
  32. package/dist/utils/security/rateLimiter.js +4 -0
  33. package/package.json +7 -15
  34. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +0 -79
  35. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +0 -82
  36. package/dist/mcp-server/resources/echoResource/index.d.ts +0 -13
  37. package/dist/mcp-server/resources/echoResource/index.js +0 -13
  38. package/dist/mcp-server/resources/echoResource/registration.d.ts +0 -30
  39. package/dist/mcp-server/resources/echoResource/registration.js +0 -168
  40. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +0 -6
  41. 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, // Changed from { PubmedArticleSet?: XmlPubmedArticleSet } | any
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
- logger.warning("Invalid or unexpected structure for xmlData in parsePubMedArticleSet.", {
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; // Cast after check
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.", requestContextService.createRequestContext({
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; // NCBI uses WebEnv with capital E
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
- // The superRefine ensures that either pmids or (queryKey & webEnv) is provided.
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"; // Parsed by server, so fetch XML
209
+ serviceRetmode = "xml";
333
210
  break;
334
211
  }
335
212
  eFetchParams.retmode = serviceRetmode;
336
213
  if (rettype)
337
214
  eFetchParams.rettype = rettype;
338
- let eFetchUrl = "";
339
- try {
340
- const eFetchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
341
- const eFetchQueryString = new URLSearchParams(eFetchParams).toString();
342
- eFetchUrl = `${eFetchBase}?${eFetchQueryString}`;
343
- // Determine if raw XML should be fetched
344
- const shouldReturnRawXml = input.detailLevel === "full_xml" && input.outputFormat === "raw_text";
345
- const eFetchResponseData = await ncbiService.eFetch(eFetchParams, toolLogicContext, { retmode: serviceRetmode, rettype, returnRawXml: shouldReturnRawXml });
346
- let finalOutputText;
347
- let structuredResponseData; // Used for building the JSON response
348
- if (input.detailLevel === "medline_text") {
349
- const medlineText = String(eFetchResponseData);
350
- const foundPmidsInMedline = new Set();
351
- const pmidRegex = /^PMID- (\d+)/gm;
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
- else if (input.detailLevel === "full_xml") {
386
- if (input.outputFormat === "raw_text") {
387
- // eFetchResponseData is already the raw XML string due to returnRawXml: true
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
- // abstract_plus or citation_data (outputFormat is always 'json' effectively)
436
- // eFetchResponseData is the parsed XML object
437
- const parsedArticles = parsePubMedArticleSet(eFetchResponseData, input, toolLogicContext);
438
- const foundPmids = new Set(parsedArticles.map((p) => p.pmid));
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
- urls: [eFetchUrl],
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
- catch (error) {
493
- logger.error("Error in fetch_pubmed_content logic", error, toolLogicContext);
494
- const mcpError = error instanceof McpError
495
- ? error
496
- : new McpError(BaseErrorCode.INTERNAL_ERROR, "Failed to fetch PubMed content.", {
497
- originalErrorName: error.name,
498
- originalErrorMessage: error.message,
499
- requestId: toolLogicContext.requestId,
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
- return {
502
- content: [
503
- {
504
- type: "text",
505
- text: JSON.stringify({
506
- error: {
507
- code: mcpError.code,
508
- message: mcpError.message,
509
- details: mcpError.details,
510
- },
511
- requestedPmids: input.pmids || "N/A (used history query)",
512
- eFetchUrl,
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
- isError: true,
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
- try {
16
- server.tool("fetch_pubmed_content", "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.", FetchPubMedContentInputSchema._def.schema.shape, // Access .shape from the ZodObject before superRefine
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
- return fetchPubMedContentLogic(input, richContext);
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
- catch (error) {
28
- ErrorHandler.handleError(new McpError(BaseErrorCode.INITIALIZATION_FAILED, "Failed to register fetch_pubmed_content tool", {
29
- originalError: error instanceof Error ? error.message : String(error),
30
- }), {
31
- operation,
32
- context,
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 declare function generatePubMedChartLogic(input: GeneratePubMedChartInput, parentRequestContext: RequestContext): Promise<CallToolResult>;
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>;