@cyanheads/pubmed-mcp-server 1.2.3 → 1.3.0

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 (95) hide show
  1. package/README.md +2 -2
  2. package/dist/config/index.d.ts +13 -52
  3. package/dist/config/index.js +51 -222
  4. package/dist/mcp-server/server.d.ts +0 -5
  5. package/dist/mcp-server/server.js +18 -34
  6. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +8 -8
  7. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +6 -5
  8. package/dist/mcp-server/tools/generatePubMedChart/logic.js +19 -19
  9. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +65 -240
  10. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +3 -3
  11. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +10 -11
  12. package/dist/mcp-server/transports/auth/authFactory.d.ts +10 -0
  13. package/dist/mcp-server/transports/auth/authFactory.js +41 -0
  14. package/dist/mcp-server/transports/auth/authMiddleware.d.ts +19 -0
  15. package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
  16. package/dist/mcp-server/transports/auth/index.d.ts +8 -5
  17. package/dist/mcp-server/transports/auth/index.js +6 -4
  18. package/dist/mcp-server/transports/auth/{core → lib}/authTypes.d.ts +0 -5
  19. package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
  20. package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
  21. package/dist/mcp-server/transports/auth/strategies/authStrategy.d.ts +17 -0
  22. package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
  23. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts +7 -0
  24. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +112 -0
  25. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts +7 -0
  26. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +101 -0
  27. package/dist/mcp-server/transports/core/baseTransportManager.d.ts +17 -0
  28. package/dist/mcp-server/transports/core/baseTransportManager.js +18 -0
  29. package/dist/mcp-server/transports/core/honoNodeBridge.d.ts +23 -0
  30. package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
  31. package/dist/mcp-server/transports/core/statefulTransportManager.d.ts +31 -0
  32. package/dist/mcp-server/transports/core/statefulTransportManager.js +233 -0
  33. package/dist/mcp-server/transports/core/statelessTransportManager.d.ts +20 -0
  34. package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
  35. package/dist/mcp-server/transports/core/transportTypes.d.ts +68 -0
  36. package/dist/mcp-server/transports/core/transportTypes.js +5 -0
  37. package/dist/mcp-server/transports/{httpErrorHandler.d.ts → http/httpErrorHandler.d.ts} +4 -9
  38. package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
  39. package/dist/mcp-server/transports/http/httpTransport.d.ts +22 -0
  40. package/dist/mcp-server/transports/http/httpTransport.js +251 -0
  41. package/dist/mcp-server/transports/http/httpTypes.d.ts +16 -0
  42. package/dist/mcp-server/transports/http/httpTypes.js +5 -0
  43. package/dist/mcp-server/transports/http/index.d.ts +7 -0
  44. package/dist/mcp-server/transports/http/index.js +6 -0
  45. package/dist/mcp-server/transports/http/mcpTransportMiddleware.d.ts +25 -0
  46. package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
  47. package/dist/mcp-server/transports/stdio/index.d.ts +5 -0
  48. package/dist/mcp-server/transports/stdio/index.js +5 -0
  49. package/dist/mcp-server/transports/{stdioTransport.d.ts → stdio/stdioTransport.d.ts} +2 -2
  50. package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
  51. package/dist/services/NCBI/{ncbiConstants.d.ts → core/ncbiConstants.d.ts} +1 -1
  52. package/dist/services/NCBI/{ncbiConstants.js → core/ncbiConstants.js} +1 -1
  53. package/dist/services/NCBI/{ncbiCoreApiClient.d.ts → core/ncbiCoreApiClient.d.ts} +3 -3
  54. package/dist/services/NCBI/core/ncbiCoreApiClient.js +117 -0
  55. package/dist/services/NCBI/{ncbiRequestQueueManager.d.ts → core/ncbiRequestQueueManager.d.ts} +2 -2
  56. package/dist/services/NCBI/{ncbiRequestQueueManager.js → core/ncbiRequestQueueManager.js} +3 -3
  57. package/dist/services/NCBI/{ncbiResponseHandler.d.ts → core/ncbiResponseHandler.d.ts} +2 -2
  58. package/dist/services/NCBI/{ncbiResponseHandler.js → core/ncbiResponseHandler.js} +3 -3
  59. package/dist/services/NCBI/{ncbiService.d.ts → core/ncbiService.d.ts} +5 -4
  60. package/dist/services/NCBI/{ncbiService.js → core/ncbiService.js} +15 -3
  61. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.d.ts +1 -1
  62. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.js +1 -1
  63. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.d.ts +1 -1
  64. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.js +1 -1
  65. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.d.ts +1 -1
  66. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.js +5 -1
  67. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.d.ts +1 -1
  68. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.js +1 -1
  69. package/dist/types-global/errors.d.ts +2 -0
  70. package/dist/types-global/errors.js +2 -0
  71. package/dist/types-global/pubmedXml.d.ts +15 -0
  72. package/dist/utils/internal/errorHandler.js +1 -1
  73. package/dist/utils/internal/logger.d.ts +13 -1
  74. package/dist/utils/internal/logger.js +43 -9
  75. package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
  76. package/dist/utils/network/fetchWithTimeout.js +59 -0
  77. package/dist/utils/network/index.d.ts +6 -0
  78. package/dist/utils/network/index.js +5 -0
  79. package/dist/utils/scheduling/index.d.ts +6 -0
  80. package/dist/utils/scheduling/index.js +6 -0
  81. package/dist/utils/scheduling/scheduler.d.ts +72 -0
  82. package/dist/utils/scheduling/scheduler.js +150 -0
  83. package/dist/utils/security/sanitization.js +35 -18
  84. package/package.json +17 -13
  85. package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
  86. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
  87. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
  88. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
  89. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
  90. package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
  91. package/dist/mcp-server/transports/httpTransport.js +0 -208
  92. package/dist/services/NCBI/ncbiCoreApiClient.js +0 -113
  93. /package/dist/mcp-server/transports/auth/{core → lib}/authContext.d.ts +0 -0
  94. /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
  95. /package/dist/mcp-server/transports/auth/{core → lib}/authUtils.d.ts +0 -0
@@ -22,17 +22,17 @@ export declare const FetchPubMedContentInputSchema: z.ZodEffects<z.ZodObject<{
22
22
  includeMeshTerms: boolean;
23
23
  includeGrantInfo: boolean;
24
24
  outputFormat: "json" | "raw_text";
25
- pmids?: string[] | undefined;
26
25
  queryKey?: string | undefined;
27
26
  webEnv?: string | undefined;
28
- retstart?: number | undefined;
29
27
  retmax?: number | undefined;
30
- }, {
28
+ retstart?: number | undefined;
31
29
  pmids?: string[] | undefined;
30
+ }, {
32
31
  queryKey?: string | undefined;
33
32
  webEnv?: string | undefined;
34
- retstart?: number | undefined;
35
33
  retmax?: number | undefined;
34
+ retstart?: number | undefined;
35
+ pmids?: string[] | undefined;
36
36
  detailLevel?: "abstract_plus" | "full_xml" | "medline_text" | "citation_data" | undefined;
37
37
  includeMeshTerms?: boolean | undefined;
38
38
  includeGrantInfo?: boolean | undefined;
@@ -42,17 +42,17 @@ export declare const FetchPubMedContentInputSchema: z.ZodEffects<z.ZodObject<{
42
42
  includeMeshTerms: boolean;
43
43
  includeGrantInfo: boolean;
44
44
  outputFormat: "json" | "raw_text";
45
- pmids?: string[] | undefined;
46
45
  queryKey?: string | undefined;
47
46
  webEnv?: string | undefined;
48
- retstart?: number | undefined;
49
47
  retmax?: number | undefined;
50
- }, {
48
+ retstart?: number | undefined;
51
49
  pmids?: string[] | undefined;
50
+ }, {
52
51
  queryKey?: string | undefined;
53
52
  webEnv?: string | undefined;
54
- retstart?: number | undefined;
55
53
  retmax?: number | undefined;
54
+ retstart?: number | undefined;
55
+ pmids?: string[] | undefined;
56
56
  detailLevel?: "abstract_plus" | "full_xml" | "medline_text" | "citation_data" | undefined;
57
57
  includeMeshTerms?: boolean | undefined;
58
58
  includeGrantInfo?: boolean | undefined;
@@ -6,10 +6,10 @@
6
6
  * @module src/mcp-server/tools/fetchPubMedContent/logic
7
7
  */
8
8
  import { z } from "zod";
9
- import { getNcbiService } from "../../../services/NCBI/ncbiService.js";
9
+ import { getNcbiService } from "../../../services/NCBI/core/ncbiService.js";
10
10
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
11
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";
12
+ import { ensureArray, extractAbstractText, extractArticleDates, extractAuthors, extractDoi, extractGrants, extractJournalInfo, extractKeywords, extractMeshTerms, extractPmid, extractPublicationTypes, getText, } from "../../../services/NCBI/parsing/index.js";
13
13
  export const FetchPubMedContentInputSchema = z
14
14
  .object({
15
15
  pmids: z
@@ -242,13 +242,14 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
242
242
  }
243
243
  }
244
244
  else if (input.detailLevel === "full_xml") {
245
+ const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
246
+ articlesCount = articlesXml.length;
245
247
  if (input.outputFormat === "raw_text") {
248
+ // Note: Raw XML output is requested, but we still parse to get an accurate count.
249
+ // This is a trade-off for robustness over performance in this specific case.
246
250
  finalOutputText = String(eFetchResponseData);
247
- articlesCount = (finalOutputText.match(/<PubmedArticle>/g) || []).length;
248
251
  }
249
252
  else {
250
- const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
251
- articlesCount = articlesXml.length;
252
253
  const foundPmidsInXml = new Set();
253
254
  const articlesPayload = articlesXml.map((articleXml) => {
254
255
  const pmid = extractPmid(articleXml.MedlineCitation) || "unknown_pmid";
@@ -1,9 +1,3 @@
1
- /**
2
- * @fileoverview Core logic for the generate_pubmed_chart tool.
3
- * Generates charts from parameterized input by creating Chart.js configurations
4
- * and rendering them on the server using chartjs-node-canvas.
5
- * @module src/mcp-server/tools/generatePubMedChart/logic
6
- */
7
1
  import { ChartJSNodeCanvas } from "chartjs-node-canvas";
8
2
  import { z } from "zod";
9
3
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
@@ -96,8 +90,8 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
96
90
  const groupedData = groupDataBySeries(dataValues, xField, yField, seriesField);
97
91
  datasets = Array.from(groupedData.entries()).map(([seriesName, data]) => ({
98
92
  label: seriesName,
99
- data: labels.map(label => {
100
- const point = data.find(p => p.x === label);
93
+ data: labels.map((label) => {
94
+ const point = data.find((p) => p.x === label);
101
95
  return point ? point.y : null;
102
96
  }),
103
97
  // You can add backgroundColor, borderColor etc. here for styling
@@ -107,41 +101,45 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
107
101
  datasets = [
108
102
  {
109
103
  label: yField,
110
- data: labels.map(label => {
111
- const item = dataValues.find(d => d[xField] === label);
104
+ data: labels.map((label) => {
105
+ const item = dataValues.find((d) => d[xField] === label);
112
106
  return item ? item[yField] : null;
113
107
  }),
114
108
  },
115
109
  ];
116
110
  }
117
111
  // For scatter and bubble charts, the data format is different
118
- if (chartType === 'scatter' || chartType === 'bubble') {
112
+ if (chartType === "scatter" || chartType === "bubble") {
119
113
  if (seriesField) {
120
114
  const groupedData = groupDataBySeries(dataValues, xField, yField, seriesField);
121
115
  datasets = Array.from(groupedData.entries()).map(([seriesName, data]) => ({
122
116
  label: seriesName,
123
- data: data.map(point => ({
117
+ data: data.map((point) => ({
124
118
  x: point.x,
125
119
  y: point.y,
126
- r: chartType === 'bubble' && sizeField ? dataValues.find(d => d[xField] === point.x)[sizeField] : undefined
120
+ r: chartType === "bubble" && sizeField
121
+ ? dataValues.find((d) => d[xField] === point.x)[sizeField]
122
+ : undefined,
127
123
  })),
128
124
  }));
129
125
  }
130
126
  else {
131
- datasets = [{
127
+ datasets = [
128
+ {
132
129
  label: yField,
133
- data: dataValues.map(item => ({
130
+ data: dataValues.map((item) => ({
134
131
  x: item[xField],
135
132
  y: item[yField],
136
- r: chartType === 'bubble' && sizeField ? item[sizeField] : undefined
133
+ r: chartType === "bubble" && sizeField ? item[sizeField] : undefined,
137
134
  })),
138
- }];
135
+ },
136
+ ];
139
137
  }
140
138
  }
141
139
  const configuration = {
142
140
  type: chartType,
143
141
  data: {
144
- labels: (chartType !== 'scatter' && chartType !== 'bubble') ? labels : undefined,
142
+ labels: chartType !== "scatter" && chartType !== "bubble" ? labels : undefined,
145
143
  datasets: datasets,
146
144
  },
147
145
  options: {
@@ -151,7 +149,9 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
151
149
  text: title,
152
150
  },
153
151
  },
154
- scales: chartType === "pie" || chartType === "doughnut" || chartType === "polarArea"
152
+ scales: chartType === "pie" ||
153
+ chartType === "doughnut" ||
154
+ chartType === "polarArea"
155
155
  ? undefined
156
156
  : {
157
157
  x: {
@@ -3,16 +3,16 @@
3
3
  * Fetches article details using EFetch and formats them into various citation styles.
4
4
  * @module src/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter
5
5
  */
6
- import { getNcbiService } from "../../../../services/NCBI/ncbiService.js";
6
+ import Cite from "citation-js";
7
+ import { getNcbiService } from "../../../../services/NCBI/core/ncbiService.js";
7
8
  import { logger, requestContextService, } from "../../../../utils/index.js";
8
- import { extractAuthors, extractDoi, extractJournalInfo, extractPmid, getText, } from "../../../../utils/parsing/ncbi-parsing/index.js";
9
+ import { extractAuthors, extractDoi, extractJournalInfo, extractPmid, getText, } from "../../../../services/NCBI/parsing/index.js";
9
10
  // Main handler for citation formats
10
11
  export async function handleCitationFormats(input, outputData, context) {
11
12
  const eFetchParams = {
12
13
  db: "pubmed",
13
14
  id: input.sourcePmid,
14
15
  retmode: "xml",
15
- // Omitting rettype to hopefully get the fullest XML record by default
16
16
  };
17
17
  const eFetchBaseUrl = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
18
18
  const searchParamsString = new URLSearchParams(eFetchParams).toString();
@@ -26,264 +26,89 @@ export async function handleCitationFormats(input, outputData, context) {
26
26
  return;
27
27
  }
28
28
  const article = eFetchResult.PubmedArticleSet.PubmedArticle[0];
29
+ const csl = pubmedArticleToCsl(article, context);
30
+ const cite = new Cite(csl);
29
31
  if (input.citationStyles?.includes("ris")) {
30
- outputData.citations.ris = formatAsRIS(article, context);
32
+ outputData.citations.ris = cite.format("ris");
31
33
  }
32
34
  if (input.citationStyles?.includes("bibtex")) {
33
- outputData.citations.bibtex = formatAsBibTeX(article, context);
35
+ outputData.citations.bibtex = cite.format("bibtex");
34
36
  }
35
37
  if (input.citationStyles?.includes("apa_string")) {
36
- outputData.citations.apa_string = formatAsAPA(article, context);
38
+ outputData.citations.apa_string = cite.format("bibliography", {
39
+ format: "text",
40
+ template: "apa",
41
+ });
37
42
  }
38
43
  if (input.citationStyles?.includes("mla_string")) {
39
- outputData.citations.mla_string = formatAsMLA(article, context);
44
+ outputData.citations.mla_string = cite.format("bibliography", {
45
+ format: "text",
46
+ template: "mla",
47
+ });
40
48
  }
41
49
  outputData.retrievedCount = 1;
42
50
  }
43
- // --- Citation Formatting Helper Functions ---
44
- function formatAsRIS(article, context) {
51
+ /**
52
+ * Converts an XML PubMed Article object to a CSL-JSON object.
53
+ * @param article The PubMed article in XML format.
54
+ * @param context The request context for logging.
55
+ * @returns A CSL-JSON object compatible with citation-js.
56
+ */
57
+ function pubmedArticleToCsl(article, context) {
45
58
  const medlineCitation = article.MedlineCitation;
46
59
  const articleDetails = medlineCitation?.Article;
47
60
  const pmid = extractPmid(medlineCitation);
48
- logger.debug("Formatting RIS for article", requestContextService.createRequestContext({ ...context, pmid }));
49
- if (!articleDetails)
50
- return "TY - ERROR\nTI - Article details not found\nER - \n";
51
- const authors = extractAuthors(articleDetails.AuthorList);
52
- const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
53
- const title = getText(articleDetails.ArticleTitle);
54
- const doi = extractDoi(articleDetails);
55
- let risString = "TY - JOUR\n";
56
- authors.forEach((author) => {
57
- if (author.lastName && author.firstName) {
58
- risString += `AU - ${author.lastName}, ${author.firstName}\n`;
59
- }
60
- else if (author.lastName) {
61
- // Handle collective name or incomplete author
62
- risString += `AU - ${author.lastName}\n`;
63
- }
61
+ const cslContext = requestContextService.createRequestContext({
62
+ ...context,
63
+ pmid,
64
64
  });
65
- risString += `TI - ${title || "N/A"}\n`;
66
- risString += `JO - ${journalInfo?.title || "N/A"}\n`;
67
- risString += `VL - ${journalInfo?.volume || ""}\n`;
68
- risString += `IS - ${journalInfo?.issue || ""}\n`;
69
- if (journalInfo?.pages) {
70
- const pages = journalInfo.pages.split("-");
71
- risString += `SP - ${pages[0] || ""}\n`;
72
- if (pages.length > 1)
73
- risString += `EP - ${pages[1] || ""}\n`;
65
+ logger.debug("Converting PubMed XML to CSL-JSON", cslContext);
66
+ if (!articleDetails) {
67
+ logger.warning("Article details not found for CSL conversion", cslContext);
68
+ return { id: pmid || "unknown", title: "Article details not found" };
74
69
  }
75
- risString += `PY - ${journalInfo?.publicationDate?.year || ""}\n`;
76
- if (doi)
77
- risString += `DO - ${doi}\n`;
78
- if (pmid)
79
- risString += `UR - https://pubmed.ncbi.nlm.nih.gov/${pmid}\n`;
80
- risString += "ER - \n";
81
- return risString;
82
- }
83
- function formatAsBibTeX(article, context) {
84
- const medlineCitation = article.MedlineCitation;
85
- const articleDetails = medlineCitation?.Article;
86
- const pmid = extractPmid(medlineCitation);
87
- logger.debug("Formatting BibTeX for article", requestContextService.createRequestContext({ ...context, pmid }));
88
- if (!articleDetails)
89
- return `@article{ERROR_ArticleNotFound${pmid || ""},\n title = {Article details not found}\n}\n`;
90
- const parsedAuthors = extractAuthors(articleDetails.AuthorList);
91
- const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
92
- const title = getText(articleDetails.ArticleTitle) || "N/A";
93
- const doi = extractDoi(articleDetails);
94
- const authorsBibtex = parsedAuthors
95
- .map((author) => {
96
- if (author.lastName && author.firstName)
97
- return `${author.lastName}, ${author.firstName}`;
98
- if (author.lastName)
99
- return `{${author.lastName}}`; // For collective names or single names
100
- return "";
101
- })
102
- .filter(Boolean)
103
- .join(" and ");
104
- const year = journalInfo?.publicationDate?.year || "ND";
105
- const firstAuthorLastName = parsedAuthors[0]?.lastName?.replace(/\s+/g, "") || "Unknown";
106
- const bibKey = `${firstAuthorLastName}${year}`;
107
- let bibtexString = `@article{${bibKey},\n`;
108
- if (authorsBibtex)
109
- bibtexString += ` author = {${authorsBibtex}},\n`;
110
- bibtexString += ` title = {${title}},\n`;
111
- bibtexString += ` journal = {${journalInfo?.title || "N/A"}},\n`;
112
- if (journalInfo?.publicationDate?.year)
113
- bibtexString += ` year = {${journalInfo.publicationDate.year}},\n`;
114
- if (journalInfo?.volume)
115
- bibtexString += ` volume = {${journalInfo.volume}},\n`;
116
- if (journalInfo?.issue)
117
- bibtexString += ` number = {${journalInfo.issue}},\n`;
118
- if (journalInfo?.pages)
119
- bibtexString += ` pages = {${journalInfo.pages.replace("-", "--")}},\n`;
120
- if (journalInfo?.publicationDate?.month)
121
- bibtexString += ` month = {${journalInfo.publicationDate.month.toLowerCase()}},\n`;
122
- if (doi)
123
- bibtexString += ` doi = {${doi}},\n`;
124
- if (pmid)
125
- bibtexString += ` pmid = {${pmid}}\n`;
126
- bibtexString += `}\n`;
127
- return bibtexString;
128
- }
129
- function formatAsAPA(article, context) {
130
- const medlineCitation = article.MedlineCitation;
131
- const articleDetails = medlineCitation?.Article;
132
- const pmid = extractPmid(medlineCitation);
133
- logger.debug("Formatting APA for article", requestContextService.createRequestContext({ ...context, pmid }));
134
- if (!articleDetails)
135
- return "Article details not found.";
136
- const parsedAuthors = extractAuthors(articleDetails.AuthorList);
137
- const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
138
- const titleText = getText(articleDetails.ArticleTitle) || "N/A";
139
- const doi = extractDoi(articleDetails);
140
- let authorsString = "N/A";
141
- if (parsedAuthors.length > 0) {
142
- if (parsedAuthors.length <= 20) {
143
- authorsString = parsedAuthors
144
- .map((author) => {
145
- if (author.lastName && author.firstName) {
146
- const initials = author.firstName
147
- .split(/\s+|-/)
148
- .map((namePart) => namePart.charAt(0).toUpperCase() + ".")
149
- .join("");
150
- return `${author.lastName}, ${initials}`;
151
- }
152
- if (author.lastName)
153
- return author.lastName; // Collective name
154
- return "";
155
- })
156
- .filter(Boolean)
157
- .join(", ");
158
- if (parsedAuthors.length > 1) {
159
- const lastCommaIndex = authorsString.lastIndexOf(", ");
160
- if (lastCommaIndex !== -1) {
161
- authorsString =
162
- authorsString.substring(0, lastCommaIndex) +
163
- " & " +
164
- authorsString.substring(lastCommaIndex + 2);
165
- }
166
- }
167
- }
168
- else {
169
- const first19 = parsedAuthors
170
- .slice(0, 19)
171
- .map((author) => {
172
- if (author.lastName && author.firstName) {
173
- const initials = author.firstName
174
- .split(/\s+|-/)
175
- .map((namePart) => namePart.charAt(0).toUpperCase() + ".")
176
- .join("");
177
- return `${author.lastName}, ${initials}`;
178
- }
179
- if (author.lastName)
180
- return author.lastName;
181
- return "";
182
- })
183
- .filter(Boolean)
184
- .join(", ");
185
- const lastAuthor = parsedAuthors[parsedAuthors.length - 1];
186
- let lastAuthorString = "";
187
- if (lastAuthor.lastName && lastAuthor.firstName) {
188
- const initials = lastAuthor.firstName
189
- .split(/\s+|-/)
190
- .map((namePart) => namePart.charAt(0).toUpperCase() + ".")
191
- .join("");
192
- lastAuthorString = `${lastAuthor.lastName}, ${initials}`;
193
- }
194
- else if (lastAuthor.lastName) {
195
- lastAuthorString = lastAuthor.lastName;
196
- }
197
- authorsString = `${first19}, ..., ${lastAuthorString}`;
198
- }
199
- }
200
- const year = journalInfo?.publicationDate?.year || "n.d.";
201
- const apaTitle = titleText.charAt(0).toUpperCase() + titleText.slice(1); // APA typically sentence case for article titles.
202
- const journal = journalInfo?.title || "N/A";
203
- const volume = journalInfo?.volume || "";
204
- const issue = journalInfo?.issue ? `(${journalInfo.issue})` : "";
205
- const pages = journalInfo?.pages || "";
206
- const doiLink = doi ? ` https://doi.org/${doi}` : "";
207
- let apaString = `${authorsString}. (${year}). ${apaTitle}. ${journal}`;
208
- if (volume)
209
- apaString += `, ${volume}`;
210
- if (issue)
211
- apaString += issue;
212
- if (pages)
213
- apaString += `, ${pages}`;
214
- apaString += `.${doiLink}`;
215
- return apaString;
216
- }
217
- function formatAsMLA(article, context) {
218
- const medlineCitation = article.MedlineCitation;
219
- const articleDetails = medlineCitation?.Article;
220
- const pmid = extractPmid(medlineCitation);
221
- logger.debug("Formatting MLA for article", requestContextService.createRequestContext({ ...context, pmid }));
222
- if (!articleDetails)
223
- return "Article details not found.";
224
- const parsedAuthors = extractAuthors(articleDetails.AuthorList);
70
+ const authors = extractAuthors(articleDetails.AuthorList);
225
71
  const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation);
226
- const titleText = getText(articleDetails.ArticleTitle);
72
+ const title = getText(articleDetails.ArticleTitle);
227
73
  const doi = extractDoi(articleDetails);
228
- let authorsString = "N/A";
229
- if (parsedAuthors.length > 0) {
230
- if (parsedAuthors.length <= 2) {
231
- authorsString = parsedAuthors
232
- .map((author, index) => {
233
- if (author.lastName && author.firstName) {
234
- return index === 0
235
- ? `${author.lastName}, ${author.firstName}`
236
- : `${author.firstName} ${author.lastName}`;
74
+ const cslAuthors = authors.map((author) => author.collectiveName
75
+ ? { literal: author.collectiveName }
76
+ : { family: author.lastName, given: author.firstName });
77
+ const dateParts = [];
78
+ if (journalInfo?.publicationDate?.year) {
79
+ dateParts.push(parseInt(journalInfo.publicationDate.year, 10));
80
+ if (journalInfo.publicationDate.month) {
81
+ // Convert month name/number to number
82
+ const monthNumber = new Date(`${journalInfo.publicationDate.month} 1, 2000`).getMonth();
83
+ if (!isNaN(monthNumber)) {
84
+ dateParts.push(monthNumber + 1);
85
+ if (journalInfo.publicationDate.day) {
86
+ dateParts.push(parseInt(journalInfo.publicationDate.day, 10));
237
87
  }
238
- if (author.lastName)
239
- return author.lastName;
240
- return "";
241
- })
242
- .filter(Boolean)
243
- .join(" and ");
244
- }
245
- else {
246
- const firstAuthor = parsedAuthors[0];
247
- if (firstAuthor.lastName && firstAuthor.firstName) {
248
- authorsString = `${firstAuthor.lastName}, ${firstAuthor.firstName}, et al`;
249
- }
250
- else if (firstAuthor.lastName) {
251
- authorsString = `${firstAuthor.lastName}, et al`;
252
88
  }
253
89
  }
254
90
  }
255
- const title = titleText ? `"${titleText}."` : "N/A.";
256
- const journal = journalInfo?.title || "N/A";
257
- let publicationDateString = journalInfo?.publicationDate?.year || "";
258
- if (journalInfo?.publicationDate?.month && journalInfo.publicationDate.year) {
259
- const month = journalInfo.publicationDate.month.substring(0, 3) + "."; // Abbreviate month
260
- publicationDateString = `${month} ${journalInfo.publicationDate.year}`;
261
- if (journalInfo.publicationDate.day) {
262
- publicationDateString = `${journalInfo.publicationDate.day} ${month} ${journalInfo.publicationDate.year}`;
91
+ const cslData = {
92
+ id: pmid,
93
+ type: "article-journal",
94
+ title: title,
95
+ author: cslAuthors,
96
+ issued: {
97
+ "date-parts": [dateParts],
98
+ },
99
+ "container-title": journalInfo?.title,
100
+ volume: journalInfo?.volume,
101
+ issue: journalInfo?.issue,
102
+ page: journalInfo?.pages,
103
+ DOI: doi,
104
+ PMID: pmid,
105
+ URL: pmid ? `https://pubmed.ncbi.nlm.nih.gov/${pmid}` : undefined,
106
+ };
107
+ // Clean up any undefined/null properties
108
+ for (const key in cslData) {
109
+ if (cslData[key] === undefined || cslData[key] === null) {
110
+ delete cslData[key];
263
111
  }
264
112
  }
265
- else if (journalInfo?.publicationDate?.medlineDate) {
266
- publicationDateString = journalInfo.publicationDate.medlineDate;
267
- }
268
- const volume = journalInfo?.volume ? `vol. ${journalInfo.volume}` : "";
269
- const issue = journalInfo?.issue ? `no. ${journalInfo.issue}` : "";
270
- const pages = journalInfo?.pages
271
- ? `pp. ${journalInfo.pages.replace("-", "–")}`
272
- : ""; // en-dash for MLA
273
- const accessUrl = doi
274
- ? `doi:${doi}`
275
- : pmid
276
- ? `https://pubmed.ncbi.nlm.nih.gov/${pmid}`
277
- : "";
278
- let mlaString = `${authorsString}. ${title} ${journal}`;
279
- if (volume)
280
- mlaString += `, ${volume}`;
281
- if (issue)
282
- mlaString += `, ${issue}`;
283
- if (publicationDateString)
284
- mlaString += `, ${publicationDateString}`;
285
- if (pages)
286
- mlaString += `, ${pages}`;
287
- mlaString += `. PubMed Central, ${accessUrl}.`; // Assuming PubMed Central or just PubMed
288
- return mlaString;
113
+ return cslData;
289
114
  }
@@ -3,10 +3,10 @@
3
3
  * for the getPubMedArticleConnections tool.
4
4
  * @module src/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler
5
5
  */
6
- import { getNcbiService } from "../../../../services/NCBI/ncbiService.js";
6
+ import { getNcbiService } from "../../../../services/NCBI/core/ncbiService.js";
7
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
8
+ import { extractBriefSummaries } from "../../../../services/NCBI/parsing/index.js";
9
+ import { ensureArray } from "../../../../services/NCBI/parsing/xmlGenericHelpers.js"; // Added import
10
10
  export async function handleELinkRelationships(input, outputData, context) {
11
11
  const eLinkParams = {
12
12
  dbfrom: "pubmed",
@@ -5,10 +5,10 @@
5
5
  * @module src/mcp-server/tools/searchPubMedArticles/logic
6
6
  */
7
7
  import { z } from "zod";
8
- import { getNcbiService } from "../../../services/NCBI/ncbiService.js";
8
+ import { getNcbiService } from "../../../services/NCBI/core/ncbiService.js";
9
9
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
10
10
  import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
11
- import { extractBriefSummaries } from "../../../utils/parsing/ncbi-parsing/index.js";
11
+ import { extractBriefSummaries } from "../../../services/NCBI/parsing/index.js";
12
12
  import { sanitization } from "../../../utils/security/sanitization.js";
13
13
  export const SearchPubMedArticlesInputSchema = z.object({
14
14
  queryTerm: z
@@ -99,7 +99,7 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
99
99
  sort: input.sortBy,
100
100
  usehistory: currentFetchBriefSummaries > 0 ? "y" : "n",
101
101
  };
102
- const eSearchResponse = await ncbiService.eSearch(eSearchParams, toolLogicContext);
102
+ const esResult = await ncbiService.eSearch(eSearchParams, toolLogicContext);
103
103
  const eSearchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi";
104
104
  const eSearchQueryStringParams = {};
105
105
  for (const key in eSearchParams) {
@@ -109,15 +109,14 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
109
109
  }
110
110
  const eSearchQueryString = new URLSearchParams(eSearchQueryStringParams).toString();
111
111
  const eSearchUrl = `${eSearchBase}?${eSearchQueryString}`;
112
- if (!eSearchResponse || !eSearchResponse.eSearchResult) {
112
+ if (!esResult) {
113
113
  throw new McpError(BaseErrorCode.NCBI_PARSING_ERROR, "Invalid or empty ESearch response from NCBI.", {
114
114
  ...toolLogicContext,
115
- responsePreview: sanitizeInputForLogging(JSON.stringify(eSearchResponse).substring(0, 200)),
115
+ responsePreview: sanitizeInputForLogging(JSON.stringify(esResult).substring(0, 200)),
116
116
  });
117
117
  }
118
- const esResult = eSearchResponse.eSearchResult;
119
- const pmids = esResult.IdList?.Id || [];
120
- const totalFound = parseInt(esResult.Count || "0", 10);
118
+ const pmids = esResult.idList || [];
119
+ const totalFound = esResult.count || 0;
121
120
  const retrievedPmidCount = pmids.length;
122
121
  let briefSummaries = [];
123
122
  let eSummaryUrl;
@@ -127,9 +126,9 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
127
126
  version: "2.0",
128
127
  retmode: "xml",
129
128
  };
130
- if (esResult.WebEnv && esResult.QueryKey) {
131
- eSummaryParams.WebEnv = esResult.WebEnv;
132
- eSummaryParams.query_key = esResult.QueryKey;
129
+ if (esResult.webEnv && esResult.queryKey) {
130
+ eSummaryParams.WebEnv = esResult.webEnv;
131
+ eSummaryParams.query_key = esResult.queryKey;
133
132
  eSummaryParams.retmax = currentFetchBriefSummaries;
134
133
  }
135
134
  else {
@@ -0,0 +1,10 @@
1
+ import { AuthStrategy } from "./strategies/authStrategy.js";
2
+ /**
3
+ * Creates and returns an authentication strategy instance based on the
4
+ * application's configuration (`config.mcpAuthMode`).
5
+ *
6
+ * @returns An instance of a class that implements the `AuthStrategy` interface,
7
+ * or `null` if authentication is disabled (`none`).
8
+ * @throws {Error} If the auth mode is unknown or misconfigured.
9
+ */
10
+ export declare function createAuthStrategy(): AuthStrategy | null;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @fileoverview Factory for creating an authentication strategy based on configuration.
3
+ * This module centralizes the logic for selecting and instantiating the correct
4
+ * authentication strategy, promoting loose coupling and easy extensibility.
5
+ * @module src/mcp-server/transports/auth/authFactory
6
+ */
7
+ import { config } from "../../../config/index.js";
8
+ import { logger, requestContextService } from "../../../utils/index.js";
9
+ import { JwtStrategy } from "./strategies/jwtStrategy.js";
10
+ import { OauthStrategy } from "./strategies/oauthStrategy.js";
11
+ /**
12
+ * Creates and returns an authentication strategy instance based on the
13
+ * application's configuration (`config.mcpAuthMode`).
14
+ *
15
+ * @returns An instance of a class that implements the `AuthStrategy` interface,
16
+ * or `null` if authentication is disabled (`none`).
17
+ * @throws {Error} If the auth mode is unknown or misconfigured.
18
+ */
19
+ export function createAuthStrategy() {
20
+ const context = requestContextService.createRequestContext({
21
+ operation: "createAuthStrategy",
22
+ authMode: config.mcpAuthMode,
23
+ });
24
+ logger.info("Creating authentication strategy...", context);
25
+ switch (config.mcpAuthMode) {
26
+ case "jwt":
27
+ logger.debug("Instantiating JWT authentication strategy.", context);
28
+ return new JwtStrategy();
29
+ case "oauth":
30
+ logger.debug("Instantiating OAuth authentication strategy.", context);
31
+ return new OauthStrategy();
32
+ case "none":
33
+ logger.info("Authentication is disabled ('none' mode).", context);
34
+ return null; // No authentication
35
+ default:
36
+ // This ensures that if a new auth mode is added to the config type
37
+ // but not to this factory, we get a compile-time or runtime error.
38
+ logger.error(`Unknown authentication mode: ${config.mcpAuthMode}`, context);
39
+ throw new Error(`Unknown authentication mode: ${config.mcpAuthMode}`);
40
+ }
41
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @fileoverview Defines a unified Hono middleware for authentication.
3
+ * This middleware is strategy-agnostic. It extracts a Bearer token,
4
+ * delegates verification to the provided authentication strategy, and
5
+ * populates the async-local storage context with the resulting auth info.
6
+ * @module src/mcp-server/transports/auth/authMiddleware
7
+ */
8
+ import type { HttpBindings } from "@hono/node-server";
9
+ import type { Context, Next } from "hono";
10
+ import type { AuthStrategy } from "./strategies/authStrategy.js";
11
+ /**
12
+ * Creates a Hono middleware function that enforces authentication using a given strategy.
13
+ *
14
+ * @param strategy - An instance of a class that implements the `AuthStrategy` interface.
15
+ * @returns A Hono middleware function.
16
+ */
17
+ export declare function createAuthMiddleware(strategy: AuthStrategy): (c: Context<{
18
+ Bindings: HttpBindings;
19
+ }>, next: Next) => Promise<void>;