@cyanheads/pubmed-mcp-server 1.0.12

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 (121) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +174 -0
  3. package/dist/config/index.d.ts +91 -0
  4. package/dist/config/index.js +324 -0
  5. package/dist/index.d.ts +24 -0
  6. package/dist/index.js +190 -0
  7. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +79 -0
  8. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +82 -0
  9. package/dist/mcp-server/resources/echoResource/index.d.ts +13 -0
  10. package/dist/mcp-server/resources/echoResource/index.js +13 -0
  11. package/dist/mcp-server/resources/echoResource/registration.d.ts +30 -0
  12. package/dist/mcp-server/resources/echoResource/registration.js +168 -0
  13. package/dist/mcp-server/server.d.ts +28 -0
  14. package/dist/mcp-server/server.js +159 -0
  15. package/dist/mcp-server/tools/fetchPubMedContent/index.d.ts +6 -0
  16. package/dist/mcp-server/tools/fetchPubMedContent/index.js +6 -0
  17. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +63 -0
  18. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +518 -0
  19. package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +10 -0
  20. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +37 -0
  21. package/dist/mcp-server/tools/getPubMedArticleConnections/index.d.ts +6 -0
  22. package/dist/mcp-server/tools/getPubMedArticleConnections/index.js +6 -0
  23. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +9 -0
  24. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +288 -0
  25. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +9 -0
  26. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +208 -0
  27. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +15 -0
  28. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +82 -0
  29. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +27 -0
  30. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.js +5 -0
  31. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +6 -0
  32. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +6 -0
  33. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +36 -0
  34. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +76 -0
  35. package/dist/mcp-server/tools/pubmedResearchAgent/index.d.ts +6 -0
  36. package/dist/mcp-server/tools/pubmedResearchAgent/index.js +6 -0
  37. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.d.ts +7 -0
  38. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.js +9 -0
  39. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.d.ts +111 -0
  40. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +149 -0
  41. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.d.ts +106 -0
  42. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.js +7 -0
  43. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.d.ts +10 -0
  44. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +251 -0
  45. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +12 -0
  46. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +55 -0
  47. package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +10 -0
  48. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +36 -0
  49. package/dist/mcp-server/tools/searchPubMedArticles/index.d.ts +6 -0
  50. package/dist/mcp-server/tools/searchPubMedArticles/index.js +6 -0
  51. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +61 -0
  52. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +229 -0
  53. package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +10 -0
  54. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +38 -0
  55. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +29 -0
  56. package/dist/mcp-server/transports/authentication/authMiddleware.js +174 -0
  57. package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
  58. package/dist/mcp-server/transports/httpTransport.js +463 -0
  59. package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
  60. package/dist/mcp-server/transports/stdioTransport.js +63 -0
  61. package/dist/services/NCBI/ncbiConstants.d.ts +22 -0
  62. package/dist/services/NCBI/ncbiConstants.js +5 -0
  63. package/dist/services/NCBI/ncbiCoreApiClient.d.ts +24 -0
  64. package/dist/services/NCBI/ncbiCoreApiClient.js +118 -0
  65. package/dist/services/NCBI/ncbiRequestQueueManager.d.ts +36 -0
  66. package/dist/services/NCBI/ncbiRequestQueueManager.js +96 -0
  67. package/dist/services/NCBI/ncbiResponseHandler.d.ts +22 -0
  68. package/dist/services/NCBI/ncbiResponseHandler.js +192 -0
  69. package/dist/services/NCBI/ncbiService.d.ts +24 -0
  70. package/dist/services/NCBI/ncbiService.js +57 -0
  71. package/dist/services/index.d.ts +7 -0
  72. package/dist/services/index.js +7 -0
  73. package/dist/services/llm-providers/index.d.ts +7 -0
  74. package/dist/services/llm-providers/index.js +7 -0
  75. package/dist/services/llm-providers/llmFactory.d.ts +69 -0
  76. package/dist/services/llm-providers/llmFactory.js +132 -0
  77. package/dist/services/llm-providers/openRouter/index.d.ts +6 -0
  78. package/dist/services/llm-providers/openRouter/index.js +7 -0
  79. package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +99 -0
  80. package/dist/services/llm-providers/openRouter/openRouterProvider.js +329 -0
  81. package/dist/types-global/errors.d.ts +119 -0
  82. package/dist/types-global/errors.js +119 -0
  83. package/dist/types-global/pubmedXml.d.ts +350 -0
  84. package/dist/types-global/pubmedXml.js +7 -0
  85. package/dist/utils/index.d.ts +10 -0
  86. package/dist/utils/index.js +18 -0
  87. package/dist/utils/internal/errorHandler.d.ts +176 -0
  88. package/dist/utils/internal/errorHandler.js +335 -0
  89. package/dist/utils/internal/index.d.ts +9 -0
  90. package/dist/utils/internal/index.js +9 -0
  91. package/dist/utils/internal/logger.d.ts +141 -0
  92. package/dist/utils/internal/logger.js +426 -0
  93. package/dist/utils/internal/requestContext.d.ts +83 -0
  94. package/dist/utils/internal/requestContext.js +72 -0
  95. package/dist/utils/metrics/index.d.ts +7 -0
  96. package/dist/utils/metrics/index.js +7 -0
  97. package/dist/utils/metrics/tokenCounter.d.ts +35 -0
  98. package/dist/utils/metrics/tokenCounter.js +110 -0
  99. package/dist/utils/parsing/dateParser.d.ts +73 -0
  100. package/dist/utils/parsing/dateParser.js +107 -0
  101. package/dist/utils/parsing/index.d.ts +8 -0
  102. package/dist/utils/parsing/index.js +8 -0
  103. package/dist/utils/parsing/jsonParser.d.ts +82 -0
  104. package/dist/utils/parsing/jsonParser.js +126 -0
  105. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.d.ts +32 -0
  106. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.js +295 -0
  107. package/dist/utils/parsing/ncbi-parsing/index.d.ts +8 -0
  108. package/dist/utils/parsing/ncbi-parsing/index.js +8 -0
  109. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.d.ts +70 -0
  110. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.js +229 -0
  111. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.d.ts +31 -0
  112. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.js +76 -0
  113. package/dist/utils/security/idGenerator.d.ts +134 -0
  114. package/dist/utils/security/idGenerator.js +198 -0
  115. package/dist/utils/security/index.d.ts +9 -0
  116. package/dist/utils/security/index.js +9 -0
  117. package/dist/utils/security/rateLimiter.d.ts +108 -0
  118. package/dist/utils/security/rateLimiter.js +179 -0
  119. package/dist/utils/security/sanitization.d.ts +173 -0
  120. package/dist/utils/security/sanitization.js +439 -0
  121. package/package.json +97 -0
@@ -0,0 +1,518 @@
1
+ /**
2
+ * @fileoverview Logic for the fetch_pubmed_content MCP tool.
3
+ * Handles EFetch queries for specific PMIDs and formats the results.
4
+ * This tool can fetch various details from PubMed including abstracts, full XML,
5
+ * MEDLINE text, and citation data.
6
+ * @module src/mcp-server/tools/fetchPubMedContent/logic
7
+ */
8
+ import { z } from "zod";
9
+ import { ncbiService } from "../../../services/NCBI/ncbiService.js";
10
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
11
+ import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
12
+ import { ensureArray, extractAbstractText, extractArticleDates, extractAuthors, extractDoi, extractGrants, extractJournalInfo, extractKeywords, extractMeshTerms, extractPmid, extractPublicationTypes, getText, } from "../../../utils/parsing/ncbi-parsing/index.js";
13
+ export const FetchPubMedContentInputSchema = z
14
+ .object({
15
+ pmids: z
16
+ .array(z.string().regex(/^\d+$/))
17
+ .max(200, "Max 200 PMIDs per call if not using history.")
18
+ .optional()
19
+ .describe("An array of PubMed Unique Identifiers (PMIDs) for which to fetch content. Use this OR queryKey/webEnv."),
20
+ queryKey: z
21
+ .string()
22
+ .optional()
23
+ .describe("Query key from ESearch history server. If used, webEnv must also be provided. Use this OR pmids."),
24
+ webEnv: z
25
+ .string()
26
+ .optional()
27
+ .describe("Web environment from ESearch history server. If used, queryKey must also be provided. Use this OR pmids."),
28
+ retstart: z
29
+ .number()
30
+ .int()
31
+ .min(0)
32
+ .optional()
33
+ .describe("Sequential index of the first record to retrieve (0-based). Used with queryKey/webEnv."),
34
+ retmax: z
35
+ .number()
36
+ .int()
37
+ .min(1)
38
+ .optional()
39
+ .describe("Maximum number of records to retrieve. Used with queryKey/webEnv."),
40
+ detailLevel: z
41
+ .enum(["abstract_plus", "full_xml", "medline_text", "citation_data"])
42
+ .optional()
43
+ .default("abstract_plus")
44
+ .describe("Specifies the level of detail for the fetched content. Options: 'abstract_plus' (parsed details including abstract, authors, journal, DOI, etc.), 'full_xml' (raw PubMedArticle XML), 'medline_text' (MEDLINE format), 'citation_data' (minimal parsed data for citations). Defaults to 'abstract_plus'."),
45
+ includeMeshTerms: z
46
+ .boolean()
47
+ .optional()
48
+ .default(true)
49
+ .describe("Applies to 'abstract_plus' and 'citation_data' if parsed from XML."),
50
+ includeGrantInfo: z
51
+ .boolean()
52
+ .optional()
53
+ .default(false)
54
+ .describe("Applies to 'abstract_plus' if parsed from XML."),
55
+ outputFormat: z
56
+ .enum(["json", "raw_text"])
57
+ .optional()
58
+ .default("json")
59
+ .describe("Specifies the final output format of the tool. \n- 'json' (default): Wraps the data in a standard JSON object. \n- 'raw_text': Returns raw text for 'medline_text' or 'full_xml' detailLevels. For other detailLevels, 'outputFormat' defaults to 'json'."),
60
+ })
61
+ .superRefine((data, ctx) => {
62
+ if (data.queryKey && !data.webEnv) {
63
+ ctx.addIssue({
64
+ code: z.ZodIssueCode.custom,
65
+ message: "webEnv is required if queryKey is provided.",
66
+ path: ["webEnv"],
67
+ });
68
+ }
69
+ if (!data.queryKey && data.webEnv) {
70
+ ctx.addIssue({
71
+ code: z.ZodIssueCode.custom,
72
+ message: "queryKey is required if webEnv is provided.",
73
+ path: ["queryKey"],
74
+ });
75
+ }
76
+ if ((!data.pmids || data.pmids.length === 0) &&
77
+ !(data.queryKey && data.webEnv)) {
78
+ ctx.addIssue({
79
+ code: z.ZodIssueCode.custom,
80
+ message: "Either pmids (non-empty array) or both queryKey and webEnv must be provided.",
81
+ path: ["pmids"],
82
+ });
83
+ }
84
+ if (data.pmids && data.pmids.length > 0 && (data.queryKey || data.webEnv)) {
85
+ ctx.addIssue({
86
+ code: z.ZodIssueCode.custom,
87
+ message: "Cannot use pmids and queryKey/webEnv simultaneously. Please choose one method.",
88
+ path: ["pmids"],
89
+ });
90
+ }
91
+ if ((data.retstart !== undefined || data.retmax !== undefined) &&
92
+ !(data.queryKey && data.webEnv)) {
93
+ ctx.addIssue({
94
+ code: z.ZodIssueCode.custom,
95
+ message: "retstart/retmax can only be used with queryKey and webEnv.",
96
+ path: ["retstart"],
97
+ });
98
+ }
99
+ });
100
+ function parsePubMedArticleSet(xmlData, // Changed from { PubmedArticleSet?: XmlPubmedArticleSet } | any
101
+ input, parentContext) {
102
+ const articles = [];
103
+ const operationContext = requestContextService.createRequestContext({
104
+ parentRequestId: parentContext.requestId,
105
+ operation: "parsePubMedArticleSet",
106
+ });
107
+ // Type guard for xmlData
108
+ if (!xmlData ||
109
+ typeof xmlData !== "object" ||
110
+ !("PubmedArticleSet" in xmlData)) {
111
+ logger.warning("Invalid or unexpected structure for xmlData in parsePubMedArticleSet.", {
112
+ ...operationContext,
113
+ xmlDataType: typeof xmlData,
114
+ xmlDataPreview: sanitizeInputForLogging(JSON.stringify(xmlData).substring(0, 200)),
115
+ });
116
+ return articles;
117
+ }
118
+ const typedXmlData = xmlData; // Cast after check
119
+ const articleSet = typedXmlData.PubmedArticleSet;
120
+ 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
+ }));
125
+ return articles;
126
+ }
127
+ 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
+ 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
+ });
148
+ continue;
149
+ }
150
+ 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
+ });
156
+ continue;
157
+ }
158
+ 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
+ if (!pmid)
165
+ continue;
166
+ const articleNode = medlineCitation.Article;
167
+ const parsedArticle = {
168
+ pmid: pmid,
169
+ title: articleNode?.ArticleTitle
170
+ ? getText(articleNode.ArticleTitle)
171
+ : undefined,
172
+ abstractText: articleNode?.Abstract
173
+ ? extractAbstractText(articleNode.Abstract)
174
+ : undefined,
175
+ authors: articleNode?.AuthorList
176
+ ? extractAuthors(articleNode.AuthorList)
177
+ : undefined,
178
+ journalInfo: articleNode?.Journal
179
+ ? extractJournalInfo(articleNode.Journal, medlineCitation)
180
+ : undefined,
181
+ publicationTypes: articleNode?.PublicationTypeList
182
+ ? extractPublicationTypes(articleNode.PublicationTypeList)
183
+ : undefined,
184
+ keywords: articleNode?.KeywordList
185
+ ? extractKeywords(articleNode.KeywordList)
186
+ : undefined,
187
+ doi: articleNode ? extractDoi(articleNode) : undefined,
188
+ articleDates: articleNode?.ArticleDate
189
+ ? extractArticleDates(articleNode)
190
+ : undefined,
191
+ };
192
+ if (input.includeMeshTerms) {
193
+ parsedArticle.meshTerms = medlineCitation.MeshHeadingList
194
+ ? extractMeshTerms(medlineCitation.MeshHeadingList)
195
+ : undefined;
196
+ }
197
+ if (input.includeGrantInfo) {
198
+ parsedArticle.grantList = articleNode?.GrantList
199
+ ? extractGrants(articleNode.GrantList)
200
+ : undefined;
201
+ }
202
+ articles.push(parsedArticle);
203
+ }
204
+ return articles;
205
+ }
206
+ 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 toolLogicContext = requestContextService.createRequestContext({
296
+ parentRequestId: parentRequestContext.requestId,
297
+ operation: "fetchPubMedContentLogic",
298
+ input: sanitizeInputForLogging(input),
299
+ });
300
+ logger.info("Executing fetch_pubmed_content tool", toolLogicContext);
301
+ const eFetchParams = { db: "pubmed" };
302
+ let usingHistory = false;
303
+ if (input.queryKey && input.webEnv) {
304
+ usingHistory = true;
305
+ eFetchParams.query_key = input.queryKey;
306
+ eFetchParams.WebEnv = input.webEnv; // NCBI uses WebEnv with capital E
307
+ if (input.retstart !== undefined) {
308
+ eFetchParams.retstart = String(input.retstart);
309
+ }
310
+ if (input.retmax !== undefined) {
311
+ eFetchParams.retmax = String(input.retmax);
312
+ }
313
+ }
314
+ else if (input.pmids && input.pmids.length > 0) {
315
+ eFetchParams.id = input.pmids.join(",");
316
+ }
317
+ // The superRefine ensures that either pmids or (queryKey & webEnv) is provided.
318
+ let serviceRetmode = "xml"; // Renamed to avoid conflict with local retmode variable if any
319
+ let rettype;
320
+ // responseContentType is determined by input.outputFormat at the end
321
+ switch (input.detailLevel) {
322
+ case "full_xml":
323
+ serviceRetmode = "xml";
324
+ break;
325
+ case "medline_text":
326
+ serviceRetmode = "text";
327
+ rettype = "medline";
328
+ break;
329
+ case "abstract_plus":
330
+ case "citation_data":
331
+ serviceRetmode = "xml"; // Parsed by server, so fetch XML
332
+ break;
333
+ }
334
+ eFetchParams.retmode = serviceRetmode;
335
+ if (rettype)
336
+ eFetchParams.rettype = rettype;
337
+ let eFetchUrl = "";
338
+ try {
339
+ const eFetchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
340
+ const eFetchQueryString = new URLSearchParams(eFetchParams).toString();
341
+ eFetchUrl = `${eFetchBase}?${eFetchQueryString}`;
342
+ // Determine if raw XML should be fetched
343
+ const shouldReturnRawXml = input.detailLevel === "full_xml" && input.outputFormat === "raw_text";
344
+ const eFetchResponseData = await ncbiService.eFetch(eFetchParams, toolLogicContext, { retmode: serviceRetmode, rettype, returnRawXml: shouldReturnRawXml });
345
+ let finalOutputText;
346
+ let structuredResponseData; // Used for building the JSON response
347
+ if (input.detailLevel === "medline_text") {
348
+ const medlineText = String(eFetchResponseData);
349
+ const foundPmidsInMedline = new Set();
350
+ const pmidRegex = /^PMID- (\d+)/gm;
351
+ let match;
352
+ while ((match = pmidRegex.exec(medlineText)) !== null) {
353
+ foundPmidsInMedline.add(match[1]);
354
+ }
355
+ let notFoundPmids = [];
356
+ if (input.pmids && input.pmids.length > 0) {
357
+ notFoundPmids = input.pmids.filter((pmid) => !foundPmidsInMedline.has(pmid));
358
+ }
359
+ structuredResponseData = {
360
+ requestedPmids: input.pmids || "N/A (used history query)",
361
+ articles: [
362
+ {
363
+ pmids: input.pmids || "N/A (used history query)",
364
+ medlineText: medlineText,
365
+ },
366
+ ],
367
+ notFoundPmids: usingHistory
368
+ ? "N/A (used history query)"
369
+ : notFoundPmids,
370
+ eFetchDetails: {
371
+ urls: [eFetchUrl],
372
+ requestMethod: input.pmids && input.pmids.length > 200 && !usingHistory
373
+ ? "POST"
374
+ : "GET",
375
+ },
376
+ };
377
+ if (input.outputFormat === "raw_text") {
378
+ finalOutputText = String(eFetchResponseData);
379
+ }
380
+ else {
381
+ finalOutputText = JSON.stringify(structuredResponseData);
382
+ }
383
+ }
384
+ else if (input.detailLevel === "full_xml") {
385
+ if (input.outputFormat === "raw_text") {
386
+ // eFetchResponseData is already the raw XML string due to returnRawXml: true
387
+ finalOutputText = String(eFetchResponseData);
388
+ // Optionally, wrap it in a minimal JSON structure if that's preferred for raw_text output consistency
389
+ // For now, returning the direct XML string as per user expectation for "raw_text"
390
+ }
391
+ else {
392
+ // outputFormat is 'json', so eFetchResponseData is the parsed XML object
393
+ const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
394
+ const articlesPayload = [];
395
+ const foundPmidsInXml = new Set();
396
+ for (const articleXml of articlesXml) {
397
+ let pmid = "unknown_pmid";
398
+ if (articleXml?.MedlineCitation) {
399
+ const extracted = extractPmid(articleXml.MedlineCitation);
400
+ if (extracted) {
401
+ pmid = extracted;
402
+ }
403
+ }
404
+ if (pmid !== "unknown_pmid") {
405
+ foundPmidsInXml.add(pmid);
406
+ }
407
+ articlesPayload.push({
408
+ pmid: pmid,
409
+ fullXmlContent: articleXml,
410
+ });
411
+ }
412
+ let notFoundPmids = [];
413
+ if (input.pmids && input.pmids.length > 0) {
414
+ notFoundPmids = input.pmids.filter((pmid) => !foundPmidsInXml.has(pmid));
415
+ }
416
+ else if (usingHistory) {
417
+ notFoundPmids = "N/A (used history query)";
418
+ }
419
+ structuredResponseData = {
420
+ requestedPmids: input.pmids || "N/A (used history query)",
421
+ articles: articlesPayload,
422
+ notFoundPmids,
423
+ eFetchDetails: {
424
+ urls: [eFetchUrl],
425
+ requestMethod: input.pmids && input.pmids.length > 200 && !usingHistory
426
+ ? "POST"
427
+ : "GET",
428
+ },
429
+ };
430
+ finalOutputText = JSON.stringify(structuredResponseData);
431
+ }
432
+ }
433
+ else {
434
+ // abstract_plus or citation_data (outputFormat is always 'json' effectively)
435
+ // eFetchResponseData is the parsed XML object
436
+ const parsedArticles = parsePubMedArticleSet(eFetchResponseData, input, toolLogicContext);
437
+ const foundPmids = new Set(parsedArticles.map((p) => p.pmid));
438
+ let notFoundPmids = [];
439
+ if (input.pmids && input.pmids.length > 0) {
440
+ notFoundPmids = input.pmids.filter((pmid) => !foundPmids.has(pmid));
441
+ }
442
+ else if (usingHistory) {
443
+ notFoundPmids = "N/A (used history query)";
444
+ }
445
+ structuredResponseData = {
446
+ requestedPmids: input.pmids || "N/A (used history query)",
447
+ articles: parsedArticles,
448
+ notFoundPmids,
449
+ eFetchDetails: {
450
+ urls: [eFetchUrl],
451
+ requestMethod: input.pmids && input.pmids.length > 200 && !usingHistory
452
+ ? "POST"
453
+ : "GET",
454
+ },
455
+ };
456
+ if (input.detailLevel === "citation_data") {
457
+ structuredResponseData.articles = structuredResponseData.articles.map((article) => ({
458
+ pmid: article.pmid,
459
+ title: article.title,
460
+ authors: article.authors?.map((a) => ({
461
+ lastName: a.lastName,
462
+ initials: a.initials,
463
+ })),
464
+ journalInfo: {
465
+ title: article.journalInfo?.title,
466
+ isoAbbreviation: article.journalInfo?.isoAbbreviation,
467
+ volume: article.journalInfo?.volume,
468
+ issue: article.journalInfo?.issue,
469
+ pages: article.journalInfo?.pages,
470
+ year: article.journalInfo?.publicationDate?.year,
471
+ },
472
+ doi: article.doi,
473
+ // Conditionally include meshTerms if the input flag was set (it defaults to true)
474
+ ...(input.includeMeshTerms && { meshTerms: article.meshTerms }),
475
+ }));
476
+ }
477
+ // For abstract_plus and citation_data, outputFormat 'raw_text' doesn't make sense,
478
+ // as the data is inherently structured. So, always output JSON.
479
+ finalOutputText = JSON.stringify(structuredResponseData);
480
+ }
481
+ return {
482
+ content: [
483
+ {
484
+ type: "text",
485
+ text: finalOutputText,
486
+ },
487
+ ],
488
+ isError: false,
489
+ };
490
+ }
491
+ catch (error) {
492
+ logger.error("Error in fetch_pubmed_content logic", error, toolLogicContext);
493
+ const mcpError = error instanceof McpError
494
+ ? error
495
+ : new McpError(BaseErrorCode.INTERNAL_ERROR, "Failed to fetch PubMed content.", {
496
+ originalErrorName: error.name,
497
+ originalErrorMessage: error.message,
498
+ requestId: toolLogicContext.requestId,
499
+ });
500
+ return {
501
+ content: [
502
+ {
503
+ type: "text",
504
+ text: JSON.stringify({
505
+ error: {
506
+ code: mcpError.code,
507
+ message: mcpError.message,
508
+ details: mcpError.details,
509
+ },
510
+ requestedPmids: input.pmids || "N/A (used history query)",
511
+ eFetchUrl,
512
+ }),
513
+ },
514
+ ],
515
+ isError: true,
516
+ };
517
+ }
518
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @fileoverview Registration for the fetch_pubmed_content MCP tool.
3
+ * @module src/mcp-server/tools/fetchPubMedContent/registration
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ /**
7
+ * Registers the fetch_pubmed_content tool with the MCP server.
8
+ * @param server - The McpServer instance.
9
+ */
10
+ export declare function registerFetchPubMedContentTool(server: McpServer): void;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @fileoverview Registration for the fetch_pubmed_content MCP tool.
3
+ * @module src/mcp-server/tools/fetchPubMedContent/registration
4
+ */
5
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
6
+ import { ErrorHandler, requestContextService } from "../../../utils/index.js";
7
+ import { FetchPubMedContentInputSchema, fetchPubMedContentLogic, } from "./logic.js";
8
+ /**
9
+ * Registers the fetch_pubmed_content tool with the MCP server.
10
+ * @param server - The McpServer instance.
11
+ */
12
+ export function registerFetchPubMedContentTool(server) {
13
+ const operation = "registerFetchPubMedContentTool";
14
+ 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
19
+ const richContext = requestContextService.createRequestContext({
20
+ parentRequestId: context.requestId,
21
+ operation: "fetchPubMedContentToolHandler",
22
+ mcpToolContext: toolContext,
23
+ });
24
+ return fetchPubMedContentLogic(input, richContext);
25
+ });
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
+ }
37
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Barrel file for the getPubMedArticleConnections tool.
3
+ * Exports the registration function for this tool.
4
+ * @module src/mcp-server/tools/getPubMedArticleConnections/index
5
+ */
6
+ export { registerGetPubMedArticleConnectionsTool } from "./registration.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Barrel file for the getPubMedArticleConnections tool.
3
+ * Exports the registration function for this tool.
4
+ * @module src/mcp-server/tools/getPubMedArticleConnections/index
5
+ */
6
+ export { registerGetPubMedArticleConnectionsTool } from "./registration.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Handles citation formatting for the getPubMedArticleConnections tool.
3
+ * Fetches article details using EFetch and formats them into various citation styles.
4
+ * @module src/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter
5
+ */
6
+ import { RequestContext } from "../../../../utils/index.js";
7
+ import type { GetPubMedArticleConnectionsInput } from "../registration.js";
8
+ import type { ToolOutputData } from "./types.js";
9
+ export declare function handleCitationFormats(input: GetPubMedArticleConnectionsInput, outputData: ToolOutputData, context: RequestContext): Promise<void>;