@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,288 @@
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 { ncbiService } from "../../../../services/NCBI/ncbiService.js";
7
+ import { logger, requestContextService, } from "../../../../utils/index.js";
8
+ import { extractAuthors, extractDoi, extractJournalInfo, extractPmid, getText, } from "../../../../utils/parsing/ncbi-parsing/index.js";
9
+ // Main handler for citation formats
10
+ export async function handleCitationFormats(input, outputData, context) {
11
+ const eFetchParams = {
12
+ db: "pubmed",
13
+ id: input.sourcePmid,
14
+ retmode: "xml",
15
+ // Omitting rettype to hopefully get the fullest XML record by default
16
+ };
17
+ const eFetchBaseUrl = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
18
+ const searchParamsString = new URLSearchParams(eFetchParams).toString();
19
+ outputData.eUtilityUrl = `${eFetchBaseUrl}?${searchParamsString}`;
20
+ const eFetchResult = await ncbiService.eFetch(eFetchParams, context);
21
+ if (!eFetchResult?.PubmedArticleSet?.PubmedArticle?.[0]) {
22
+ outputData.message =
23
+ "Could not retrieve article details for citation formatting.";
24
+ logger.warning(outputData.message, context);
25
+ return;
26
+ }
27
+ const article = eFetchResult.PubmedArticleSet.PubmedArticle[0];
28
+ if (input.citationStyles?.includes("ris")) {
29
+ outputData.citations.ris = formatAsRIS(article, context);
30
+ }
31
+ if (input.citationStyles?.includes("bibtex")) {
32
+ outputData.citations.bibtex = formatAsBibTeX(article, context);
33
+ }
34
+ if (input.citationStyles?.includes("apa_string")) {
35
+ outputData.citations.apa_string = formatAsAPA(article, context);
36
+ }
37
+ if (input.citationStyles?.includes("mla_string")) {
38
+ outputData.citations.mla_string = formatAsMLA(article, context);
39
+ }
40
+ outputData.retrievedCount = 1;
41
+ }
42
+ // --- Citation Formatting Helper Functions ---
43
+ function formatAsRIS(article, context) {
44
+ const medlineCitation = article.MedlineCitation;
45
+ const articleDetails = medlineCitation?.Article;
46
+ const pmid = extractPmid(medlineCitation);
47
+ logger.debug("Formatting RIS for article", requestContextService.createRequestContext({ ...context, pmid }));
48
+ if (!articleDetails)
49
+ return "TY - ERROR\nTI - Article details not found\nER - \n";
50
+ const authors = extractAuthors(articleDetails.AuthorList);
51
+ const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
52
+ const title = getText(articleDetails.ArticleTitle);
53
+ const doi = extractDoi(articleDetails);
54
+ let risString = "TY - JOUR\n";
55
+ authors.forEach((author) => {
56
+ if (author.lastName && author.firstName) {
57
+ risString += `AU - ${author.lastName}, ${author.firstName}\n`;
58
+ }
59
+ else if (author.lastName) {
60
+ // Handle collective name or incomplete author
61
+ risString += `AU - ${author.lastName}\n`;
62
+ }
63
+ });
64
+ risString += `TI - ${title || "N/A"}\n`;
65
+ risString += `JO - ${journalInfo?.title || "N/A"}\n`;
66
+ risString += `VL - ${journalInfo?.volume || ""}\n`;
67
+ risString += `IS - ${journalInfo?.issue || ""}\n`;
68
+ if (journalInfo?.pages) {
69
+ const pages = journalInfo.pages.split("-");
70
+ risString += `SP - ${pages[0] || ""}\n`;
71
+ if (pages.length > 1)
72
+ risString += `EP - ${pages[1] || ""}\n`;
73
+ }
74
+ risString += `PY - ${journalInfo?.publicationDate?.year || ""}\n`;
75
+ if (doi)
76
+ risString += `DO - ${doi}\n`;
77
+ if (pmid)
78
+ risString += `UR - https://pubmed.ncbi.nlm.nih.gov/${pmid}\n`;
79
+ risString += "ER - \n";
80
+ return risString;
81
+ }
82
+ function formatAsBibTeX(article, context) {
83
+ const medlineCitation = article.MedlineCitation;
84
+ const articleDetails = medlineCitation?.Article;
85
+ const pmid = extractPmid(medlineCitation);
86
+ logger.debug("Formatting BibTeX for article", requestContextService.createRequestContext({ ...context, pmid }));
87
+ if (!articleDetails)
88
+ return `@article{ERROR_ArticleNotFound${pmid || ""},\n title = {Article details not found}\n}\n`;
89
+ const parsedAuthors = extractAuthors(articleDetails.AuthorList);
90
+ const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
91
+ const title = getText(articleDetails.ArticleTitle) || "N/A";
92
+ const doi = extractDoi(articleDetails);
93
+ const authorsBibtex = parsedAuthors
94
+ .map((author) => {
95
+ if (author.lastName && author.firstName)
96
+ return `${author.lastName}, ${author.firstName}`;
97
+ if (author.lastName)
98
+ return `{${author.lastName}}`; // For collective names or single names
99
+ return "";
100
+ })
101
+ .filter(Boolean)
102
+ .join(" and ");
103
+ const year = journalInfo?.publicationDate?.year || "ND";
104
+ const firstAuthorLastName = parsedAuthors[0]?.lastName?.replace(/\s+/g, "") || "Unknown";
105
+ const bibKey = `${firstAuthorLastName}${year}`;
106
+ let bibtexString = `@article{${bibKey},\n`;
107
+ if (authorsBibtex)
108
+ bibtexString += ` author = {${authorsBibtex}},\n`;
109
+ bibtexString += ` title = {${title}},\n`;
110
+ bibtexString += ` journal = {${journalInfo?.title || "N/A"}},\n`;
111
+ if (journalInfo?.publicationDate?.year)
112
+ bibtexString += ` year = {${journalInfo.publicationDate.year}},\n`;
113
+ if (journalInfo?.volume)
114
+ bibtexString += ` volume = {${journalInfo.volume}},\n`;
115
+ if (journalInfo?.issue)
116
+ bibtexString += ` number = {${journalInfo.issue}},\n`;
117
+ if (journalInfo?.pages)
118
+ bibtexString += ` pages = {${journalInfo.pages.replace("-", "--")}},\n`;
119
+ if (journalInfo?.publicationDate?.month)
120
+ bibtexString += ` month = {${journalInfo.publicationDate.month.toLowerCase()}},\n`;
121
+ if (doi)
122
+ bibtexString += ` doi = {${doi}},\n`;
123
+ if (pmid)
124
+ bibtexString += ` pmid = {${pmid}}\n`;
125
+ bibtexString += `}\n`;
126
+ return bibtexString;
127
+ }
128
+ function formatAsAPA(article, context) {
129
+ const medlineCitation = article.MedlineCitation;
130
+ const articleDetails = medlineCitation?.Article;
131
+ const pmid = extractPmid(medlineCitation);
132
+ logger.debug("Formatting APA for article", requestContextService.createRequestContext({ ...context, pmid }));
133
+ if (!articleDetails)
134
+ return "Article details not found.";
135
+ const parsedAuthors = extractAuthors(articleDetails.AuthorList);
136
+ const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
137
+ const titleText = getText(articleDetails.ArticleTitle) || "N/A";
138
+ const doi = extractDoi(articleDetails);
139
+ let authorsString = "N/A";
140
+ if (parsedAuthors.length > 0) {
141
+ if (parsedAuthors.length <= 20) {
142
+ authorsString = parsedAuthors
143
+ .map((author) => {
144
+ if (author.lastName && author.firstName) {
145
+ const initials = author.firstName
146
+ .split(/\s+|-/)
147
+ .map((namePart) => namePart.charAt(0).toUpperCase() + ".")
148
+ .join("");
149
+ return `${author.lastName}, ${initials}`;
150
+ }
151
+ if (author.lastName)
152
+ return author.lastName; // Collective name
153
+ return "";
154
+ })
155
+ .filter(Boolean)
156
+ .join(", ");
157
+ if (parsedAuthors.length > 1) {
158
+ const lastCommaIndex = authorsString.lastIndexOf(", ");
159
+ if (lastCommaIndex !== -1) {
160
+ authorsString =
161
+ authorsString.substring(0, lastCommaIndex) +
162
+ " & " +
163
+ authorsString.substring(lastCommaIndex + 2);
164
+ }
165
+ }
166
+ }
167
+ else {
168
+ const first19 = parsedAuthors
169
+ .slice(0, 19)
170
+ .map((author) => {
171
+ if (author.lastName && author.firstName) {
172
+ const initials = author.firstName
173
+ .split(/\s+|-/)
174
+ .map((namePart) => namePart.charAt(0).toUpperCase() + ".")
175
+ .join("");
176
+ return `${author.lastName}, ${initials}`;
177
+ }
178
+ if (author.lastName)
179
+ return author.lastName;
180
+ return "";
181
+ })
182
+ .filter(Boolean)
183
+ .join(", ");
184
+ const lastAuthor = parsedAuthors[parsedAuthors.length - 1];
185
+ let lastAuthorString = "";
186
+ if (lastAuthor.lastName && lastAuthor.firstName) {
187
+ const initials = lastAuthor.firstName
188
+ .split(/\s+|-/)
189
+ .map((namePart) => namePart.charAt(0).toUpperCase() + ".")
190
+ .join("");
191
+ lastAuthorString = `${lastAuthor.lastName}, ${initials}`;
192
+ }
193
+ else if (lastAuthor.lastName) {
194
+ lastAuthorString = lastAuthor.lastName;
195
+ }
196
+ authorsString = `${first19}, ..., ${lastAuthorString}`;
197
+ }
198
+ }
199
+ const year = journalInfo?.publicationDate?.year || "n.d.";
200
+ const apaTitle = titleText.charAt(0).toUpperCase() + titleText.slice(1); // APA typically sentence case for article titles.
201
+ const journal = journalInfo?.title ? `<em>${journalInfo.title}</em>` : "N/A";
202
+ const volume = journalInfo?.volume ? `<em>${journalInfo.volume}</em>` : "";
203
+ const issue = journalInfo?.issue ? `(${journalInfo.issue})` : "";
204
+ const pages = journalInfo?.pages || "";
205
+ const doiLink = doi ? ` https://doi.org/${doi}` : "";
206
+ let apaString = `${authorsString}. (${year}). ${apaTitle}. ${journal}`;
207
+ if (volume)
208
+ apaString += `, ${volume}`;
209
+ if (issue)
210
+ apaString += issue;
211
+ if (pages)
212
+ apaString += `, ${pages}`;
213
+ apaString += `.${doiLink}`;
214
+ return apaString;
215
+ }
216
+ function formatAsMLA(article, context) {
217
+ const medlineCitation = article.MedlineCitation;
218
+ const articleDetails = medlineCitation?.Article;
219
+ const pmid = extractPmid(medlineCitation);
220
+ logger.debug("Formatting MLA for article", requestContextService.createRequestContext({ ...context, pmid }));
221
+ if (!articleDetails)
222
+ return "Article details not found.";
223
+ const parsedAuthors = extractAuthors(articleDetails.AuthorList);
224
+ const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
225
+ const titleText = getText(articleDetails.ArticleTitle);
226
+ const doi = extractDoi(articleDetails);
227
+ let authorsString = "N/A";
228
+ if (parsedAuthors.length > 0) {
229
+ if (parsedAuthors.length <= 2) {
230
+ authorsString = parsedAuthors
231
+ .map((author, index) => {
232
+ if (author.lastName && author.firstName) {
233
+ return index === 0
234
+ ? `${author.lastName}, ${author.firstName}`
235
+ : `${author.firstName} ${author.lastName}`;
236
+ }
237
+ if (author.lastName)
238
+ return author.lastName;
239
+ return "";
240
+ })
241
+ .filter(Boolean)
242
+ .join(" and ");
243
+ }
244
+ else {
245
+ const firstAuthor = parsedAuthors[0];
246
+ if (firstAuthor.lastName && firstAuthor.firstName) {
247
+ authorsString = `${firstAuthor.lastName}, ${firstAuthor.firstName}, et al`;
248
+ }
249
+ else if (firstAuthor.lastName) {
250
+ authorsString = `${firstAuthor.lastName}, et al`;
251
+ }
252
+ }
253
+ }
254
+ const title = titleText ? `"${titleText}."` : "N/A.";
255
+ const journal = journalInfo?.title ? `<em>${journalInfo.title}</em>` : "N/A";
256
+ let publicationDateString = journalInfo?.publicationDate?.year || "";
257
+ if (journalInfo?.publicationDate?.month && journalInfo.publicationDate.year) {
258
+ const month = journalInfo.publicationDate.month.substring(0, 3) + "."; // Abbreviate month
259
+ publicationDateString = `${month} ${journalInfo.publicationDate.year}`;
260
+ if (journalInfo.publicationDate.day) {
261
+ publicationDateString = `${journalInfo.publicationDate.day} ${month} ${journalInfo.publicationDate.year}`;
262
+ }
263
+ }
264
+ else if (journalInfo?.publicationDate?.medlineDate) {
265
+ publicationDateString = journalInfo.publicationDate.medlineDate;
266
+ }
267
+ const volume = journalInfo?.volume ? `vol. ${journalInfo.volume}` : "";
268
+ const issue = journalInfo?.issue ? `no. ${journalInfo.issue}` : "";
269
+ const pages = journalInfo?.pages
270
+ ? `pp. ${journalInfo.pages.replace("-", "–")}`
271
+ : ""; // en-dash for MLA
272
+ const accessUrl = doi
273
+ ? `doi:${doi}`
274
+ : pmid
275
+ ? `https://pubmed.ncbi.nlm.nih.gov/${pmid}`
276
+ : "";
277
+ let mlaString = `${authorsString}. ${title} ${journal}`;
278
+ if (volume)
279
+ mlaString += `, ${volume}`;
280
+ if (issue)
281
+ mlaString += `, ${issue}`;
282
+ if (publicationDateString)
283
+ mlaString += `, ${publicationDateString}`;
284
+ if (pages)
285
+ mlaString += `, ${pages}`;
286
+ mlaString += `. PubMed Central, ${accessUrl}.`; // Assuming PubMed Central or just PubMed
287
+ return mlaString;
288
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Handles ELink requests and enriches results with ESummary data
3
+ * for the getPubMedArticleConnections tool.
4
+ * @module src/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler
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 handleELinkRelationships(input: GetPubMedArticleConnectionsInput, outputData: ToolOutputData, context: RequestContext): Promise<void>;
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @fileoverview Handles ELink requests and enriches results with ESummary data
3
+ * for the getPubMedArticleConnections tool.
4
+ * @module src/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler
5
+ */
6
+ import { ncbiService } from "../../../../services/NCBI/ncbiService.js";
7
+ import { logger } from "../../../../utils/index.js";
8
+ import { extractBriefSummaries } from "../../../../utils/parsing/ncbi-parsing/index.js";
9
+ import { ensureArray } from "../../../../utils/parsing/ncbi-parsing/xmlGenericHelpers.js"; // Added import
10
+ export async function handleELinkRelationships(input, outputData, context) {
11
+ const eLinkParams = {
12
+ dbfrom: "pubmed",
13
+ db: "pubmed",
14
+ id: input.sourcePmid,
15
+ retmode: "xml",
16
+ // cmd and linkname will be set below based on relationshipType
17
+ };
18
+ switch (input.relationshipType) {
19
+ case "pubmed_citedin":
20
+ eLinkParams.cmd = "neighbor_history";
21
+ eLinkParams.linkname = "pubmed_pubmed_citedin";
22
+ break;
23
+ case "pubmed_references":
24
+ eLinkParams.cmd = "neighbor_history";
25
+ eLinkParams.linkname = "pubmed_pubmed_refs";
26
+ break;
27
+ case "pubmed_similar_articles":
28
+ default: // Default to similar articles
29
+ eLinkParams.cmd = "neighbor_score";
30
+ // No linkname is explicitly needed for neighbor_score when dbfrom and db are pubmed
31
+ break;
32
+ }
33
+ const tempUrl = new URL("https://dummy.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi");
34
+ Object.keys(eLinkParams).forEach((key) => tempUrl.searchParams.append(key, String(eLinkParams[key])));
35
+ outputData.eUtilityUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi?${tempUrl.search.substring(1)}`;
36
+ const eLinkResult = await ncbiService.eLink(eLinkParams, context);
37
+ // Log the full eLinkResult for debugging
38
+ logger.debug("Raw eLinkResult from ncbiService:", {
39
+ ...context,
40
+ eLinkResultString: JSON.stringify(eLinkResult, null, 2),
41
+ });
42
+ // Use ensureArray for robust handling of potentially single or array eLinkResult
43
+ const eLinkResultsArray = ensureArray(eLinkResult?.eLinkResult);
44
+ const firstELinkResult = eLinkResultsArray[0]; // Get the first (or only) eLinkResult object
45
+ // Use ensureArray for LinkSet as well
46
+ const linkSetsArray = ensureArray(firstELinkResult?.LinkSet);
47
+ const linkSet = linkSetsArray[0]; // Get the first (or only) LinkSet object
48
+ let foundPmids = [];
49
+ if (firstELinkResult?.ERROR) {
50
+ const errorMsg = typeof firstELinkResult.ERROR === "string"
51
+ ? firstELinkResult.ERROR
52
+ : JSON.stringify(firstELinkResult.ERROR);
53
+ logger.warning(`ELink returned an error: ${errorMsg}`, context);
54
+ outputData.message = `ELink error: ${errorMsg}`;
55
+ outputData.retrievedCount = 0;
56
+ return;
57
+ }
58
+ if (linkSet?.LinkSetDbHistory) {
59
+ // Handle cmd=neighbor_history response (citedin, references)
60
+ const history = Array.isArray(linkSet.LinkSetDbHistory)
61
+ ? linkSet.LinkSetDbHistory[0]
62
+ : linkSet.LinkSetDbHistory;
63
+ if (history?.QueryKey && linkSet.WebEnv) {
64
+ const eSearchParams = {
65
+ db: "pubmed",
66
+ query_key: history.QueryKey,
67
+ WebEnv: linkSet.WebEnv,
68
+ retmode: "xml",
69
+ retmax: input.maxRelatedResults * 2, // Fetch a bit more to allow filtering sourcePmid
70
+ };
71
+ const eSearchResult = await ncbiService.eSearch(eSearchParams, context);
72
+ if (eSearchResult?.eSearchResult?.IdList?.Id) {
73
+ const ids = ensureArray(eSearchResult.eSearchResult.IdList.Id);
74
+ foundPmids = ids
75
+ .map((idNode) => {
76
+ // Allow number for idNode
77
+ let pmidVal;
78
+ if (typeof idNode === "object" && idNode !== null) {
79
+ pmidVal = idNode["#text"];
80
+ }
81
+ else {
82
+ pmidVal = idNode;
83
+ }
84
+ return {
85
+ pmid: pmidVal !== undefined ? String(pmidVal) : "",
86
+ // No scores from this ESearch path
87
+ };
88
+ })
89
+ .filter((item) => item.pmid && item.pmid !== input.sourcePmid && item.pmid !== "0");
90
+ }
91
+ }
92
+ }
93
+ else if (linkSet?.LinkSetDb) {
94
+ // Handle cmd=neighbor_score response (similar_articles)
95
+ const linkSetDbArray = Array.isArray(linkSet.LinkSetDb)
96
+ ? linkSet.LinkSetDb
97
+ : [linkSet.LinkSetDb];
98
+ const targetLinkSetDbEntry = linkSetDbArray.find((db) => db.LinkName === "pubmed_pubmed");
99
+ if (targetLinkSetDbEntry?.Link) {
100
+ const links = ensureArray(targetLinkSetDbEntry.Link); // Use ensureArray here too
101
+ foundPmids = links
102
+ .map((link) => {
103
+ let pmidValue;
104
+ if (typeof link.Id === "object" && link.Id !== null) {
105
+ pmidValue = link.Id["#text"];
106
+ }
107
+ else if (link.Id !== undefined) {
108
+ pmidValue = link.Id;
109
+ }
110
+ let scoreValue;
111
+ if (typeof link.Score === "object" && link.Score !== null) {
112
+ scoreValue = link.Score["#text"];
113
+ }
114
+ else if (link.Score !== undefined) {
115
+ scoreValue = link.Score;
116
+ }
117
+ const pmidString = pmidValue !== undefined ? String(pmidValue) : "";
118
+ return {
119
+ pmid: pmidString,
120
+ score: scoreValue !== undefined ? Number(scoreValue) : undefined,
121
+ };
122
+ })
123
+ .filter((item) => item.pmid && item.pmid !== input.sourcePmid && item.pmid !== "0");
124
+ }
125
+ }
126
+ if (foundPmids.length === 0) {
127
+ logger.warning("No related PMIDs found after ELink/ESearch processing.", context);
128
+ outputData.message = "No related articles found or ELink error."; // Generic message if no PMIDs
129
+ outputData.retrievedCount = 0;
130
+ return;
131
+ }
132
+ logger.debug("Found PMIDs after initial parsing and filtering (before sort):", {
133
+ ...context,
134
+ foundPmidsCount: foundPmids.length,
135
+ firstFewFoundPmids: foundPmids.slice(0, 3),
136
+ });
137
+ if (foundPmids.every((p) => p.score !== undefined)) {
138
+ foundPmids.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
139
+ }
140
+ logger.debug("Found PMIDs after sorting:", {
141
+ ...context,
142
+ sortedFoundPmidsCount: foundPmids.length,
143
+ firstFewSortedFoundPmids: foundPmids.slice(0, 3),
144
+ });
145
+ const pmidsToEnrich = foundPmids
146
+ .slice(0, input.maxRelatedResults)
147
+ .map((p) => p.pmid);
148
+ logger.debug("PMIDs to enrich with ESummary:", {
149
+ ...context,
150
+ pmidsToEnrichCount: pmidsToEnrich.length,
151
+ pmidsToEnrichList: pmidsToEnrich,
152
+ });
153
+ if (pmidsToEnrich.length > 0) {
154
+ try {
155
+ const summaryParams = {
156
+ db: "pubmed",
157
+ id: pmidsToEnrich.join(","),
158
+ version: "2.0",
159
+ retmode: "xml",
160
+ };
161
+ const summaryResultContainer = await ncbiService.eSummary(summaryParams, context);
162
+ const summaryResult = summaryResultContainer?.eSummaryResult ||
163
+ summaryResultContainer?.result ||
164
+ summaryResultContainer;
165
+ if (summaryResult) {
166
+ const briefSummaries = await extractBriefSummaries(summaryResult, context);
167
+ const pmidDetailsMap = new Map();
168
+ briefSummaries.forEach((bs) => pmidDetailsMap.set(bs.pmid, bs));
169
+ outputData.relatedArticles = foundPmids
170
+ .filter((p) => pmidsToEnrich.includes(p.pmid))
171
+ .map((p) => {
172
+ const details = pmidDetailsMap.get(p.pmid);
173
+ return {
174
+ pmid: p.pmid,
175
+ title: details?.title,
176
+ authors: details?.authors,
177
+ score: p.score,
178
+ linkUrl: `https://pubmed.ncbi.nlm.nih.gov/${p.pmid}/`,
179
+ };
180
+ })
181
+ .slice(0, input.maxRelatedResults);
182
+ }
183
+ else {
184
+ logger.warning("ESummary did not return usable data for enrichment.", context);
185
+ outputData.relatedArticles = foundPmids
186
+ .slice(0, input.maxRelatedResults)
187
+ .map((p) => ({
188
+ pmid: p.pmid,
189
+ score: p.score,
190
+ linkUrl: `https://pubmed.ncbi.nlm.nih.gov/${p.pmid}/`,
191
+ }));
192
+ }
193
+ }
194
+ catch (summaryError) {
195
+ logger.error("Failed to enrich related articles with summaries", summaryError instanceof Error
196
+ ? summaryError
197
+ : new Error(String(summaryError)), context);
198
+ outputData.relatedArticles = foundPmids
199
+ .slice(0, input.maxRelatedResults)
200
+ .map((p) => ({
201
+ pmid: p.pmid,
202
+ score: p.score,
203
+ linkUrl: `https://pubmed.ncbi.nlm.nih.gov/${p.pmid}/`,
204
+ }));
205
+ }
206
+ }
207
+ outputData.retrievedCount = outputData.relatedArticles.length;
208
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview Main logic handler for the 'get_pubmed_article_connections' MCP tool.
3
+ * Orchestrates calls to ELink or citation formatting handlers.
4
+ * @module src/mcp-server/tools/getPubMedArticleConnections/logic/index
5
+ */
6
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
7
+ import { RequestContext } from "../../../../utils/index.js";
8
+ import type { GetPubMedArticleConnectionsInput } from "../registration.js";
9
+ /**
10
+ * Main handler for the 'get_pubmed_article_connections' tool.
11
+ * @param {GetPubMedArticleConnectionsInput} input - Validated input parameters.
12
+ * @param {RequestContext} context - The request context for this tool invocation.
13
+ * @returns {Promise<CallToolResult>} The result of the tool call.
14
+ */
15
+ export declare function handleGetPubMedArticleConnections(input: GetPubMedArticleConnectionsInput, context: RequestContext): Promise<CallToolResult>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @fileoverview Main logic handler for the 'get_pubmed_article_connections' MCP tool.
3
+ * Orchestrates calls to ELink or citation formatting handlers.
4
+ * @module src/mcp-server/tools/getPubMedArticleConnections/logic/index
5
+ */
6
+ import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
7
+ import { logger, requestContextService, sanitizeInputForLogging, } from "../../../../utils/index.js";
8
+ import { handleCitationFormats } from "./citationFormatter.js";
9
+ import { handleELinkRelationships } from "./elinkHandler.js";
10
+ /**
11
+ * Main handler for the 'get_pubmed_article_connections' tool.
12
+ * @param {GetPubMedArticleConnectionsInput} input - Validated input parameters.
13
+ * @param {RequestContext} context - The request context for this tool invocation.
14
+ * @returns {Promise<CallToolResult>} The result of the tool call.
15
+ */
16
+ export async function handleGetPubMedArticleConnections(input, context) {
17
+ const toolLogicContext = requestContextService.createRequestContext({
18
+ parentRequestId: context.requestId, // Link to parent context
19
+ operation: "handleGetPubMedArticleConnections",
20
+ toolName: "get_pubmed_article_connections",
21
+ input: sanitizeInputForLogging(input),
22
+ });
23
+ logger.info("Executing get_pubmed_article_connections tool", toolLogicContext);
24
+ const outputData = {
25
+ sourcePmid: input.sourcePmid,
26
+ relationshipType: input.relationshipType,
27
+ relatedArticles: [],
28
+ citations: {},
29
+ retrievedCount: 0,
30
+ eUtilityUrl: undefined, // Initialize
31
+ message: undefined, // Initialize
32
+ };
33
+ try {
34
+ switch (input.relationshipType) {
35
+ case "pubmed_similar_articles":
36
+ case "pubmed_citedin":
37
+ case "pubmed_references":
38
+ await handleELinkRelationships(input, outputData, toolLogicContext);
39
+ break;
40
+ case "citation_formats":
41
+ await handleCitationFormats(input, outputData, toolLogicContext);
42
+ break;
43
+ default:
44
+ // This case should ideally be caught by Zod schema validation beforehand
45
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unsupported relationshipType: ${input.relationshipType}`, { receivedType: input.relationshipType });
46
+ }
47
+ // Ensure message is set if no specific error but also no results from handlers
48
+ if (outputData.retrievedCount === 0 &&
49
+ !outputData.message &&
50
+ (input.relationshipType !== "citation_formats" ||
51
+ Object.keys(outputData.citations).length === 0)) {
52
+ outputData.message = "No results found for the given parameters.";
53
+ }
54
+ }
55
+ catch (error) {
56
+ logger.error("Error in getPubMedArticleConnections main logic", error, toolLogicContext);
57
+ outputData.message =
58
+ error instanceof McpError
59
+ ? error.message
60
+ : "An unexpected error occurred while processing the request.";
61
+ // Ensure critical details from McpError are passed through if available
62
+ const errorDetails = error instanceof McpError
63
+ ? error.details
64
+ : { originalError: error.message };
65
+ const errorPayload = {
66
+ ...outputData,
67
+ error: {
68
+ code: error.code || BaseErrorCode.INTERNAL_ERROR,
69
+ message: outputData.message,
70
+ details: errorDetails,
71
+ },
72
+ };
73
+ const content = [
74
+ { type: "text", text: JSON.stringify(errorPayload) },
75
+ ];
76
+ return { content, isError: true };
77
+ }
78
+ const responseContent = [
79
+ { type: "text", text: JSON.stringify(outputData) },
80
+ ];
81
+ return { content: responseContent, isError: false };
82
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @fileoverview Shared type definitions for the getPubMedArticleConnections tool logic.
3
+ * @module src/mcp-server/tools/getPubMedArticleConnections/logic/types
4
+ */
5
+ import type { GetPubMedArticleConnectionsInput } from "../registration.js";
6
+ export interface RelatedArticle {
7
+ pmid: string;
8
+ title?: string;
9
+ authors?: string;
10
+ score?: number;
11
+ linkUrl: string;
12
+ }
13
+ export interface CitationOutput {
14
+ ris?: string;
15
+ bibtex?: string;
16
+ apa_string?: string;
17
+ mla_string?: string;
18
+ }
19
+ export interface ToolOutputData {
20
+ sourcePmid: string;
21
+ relationshipType: GetPubMedArticleConnectionsInput["relationshipType"];
22
+ relatedArticles: RelatedArticle[];
23
+ citations: CitationOutput;
24
+ retrievedCount: number;
25
+ eUtilityUrl?: string;
26
+ message?: string;
27
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Shared type definitions for the getPubMedArticleConnections tool logic.
3
+ * @module src/mcp-server/tools/getPubMedArticleConnections/logic/types
4
+ */
5
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Core logic for the 'get_pubmed_article_connections' MCP tool.
3
+ * This file now re-exports the main handler from the './logic/index.js' module.
4
+ * @module src/mcp-server/tools/getPubMedArticleConnections/logic
5
+ */
6
+ export { handleGetPubMedArticleConnections } from "./logic/index.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Core logic for the 'get_pubmed_article_connections' MCP tool.
3
+ * This file now re-exports the main handler from the './logic/index.js' module.
4
+ * @module src/mcp-server/tools/getPubMedArticleConnections/logic
5
+ */
6
+ export { handleGetPubMedArticleConnections } from "./logic/index.js";