@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.
- package/README.md +2 -2
- package/dist/config/index.d.ts +13 -52
- package/dist/config/index.js +51 -222
- package/dist/mcp-server/server.d.ts +0 -5
- package/dist/mcp-server/server.js +18 -34
- package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +8 -8
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +6 -5
- package/dist/mcp-server/tools/generatePubMedChart/logic.js +19 -19
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +65 -240
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +3 -3
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +10 -11
- package/dist/mcp-server/transports/auth/authFactory.d.ts +10 -0
- package/dist/mcp-server/transports/auth/authFactory.js +41 -0
- package/dist/mcp-server/transports/auth/authMiddleware.d.ts +19 -0
- package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
- package/dist/mcp-server/transports/auth/index.d.ts +8 -5
- package/dist/mcp-server/transports/auth/index.js +6 -4
- package/dist/mcp-server/transports/auth/{core → lib}/authTypes.d.ts +0 -5
- package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
- package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
- package/dist/mcp-server/transports/auth/strategies/authStrategy.d.ts +17 -0
- package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts +7 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +112 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts +7 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +101 -0
- package/dist/mcp-server/transports/core/baseTransportManager.d.ts +17 -0
- package/dist/mcp-server/transports/core/baseTransportManager.js +18 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.d.ts +23 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.d.ts +31 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.js +233 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.d.ts +20 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
- package/dist/mcp-server/transports/core/transportTypes.d.ts +68 -0
- package/dist/mcp-server/transports/core/transportTypes.js +5 -0
- package/dist/mcp-server/transports/{httpErrorHandler.d.ts → http/httpErrorHandler.d.ts} +4 -9
- package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
- package/dist/mcp-server/transports/http/httpTransport.d.ts +22 -0
- package/dist/mcp-server/transports/http/httpTransport.js +251 -0
- package/dist/mcp-server/transports/http/httpTypes.d.ts +16 -0
- package/dist/mcp-server/transports/http/httpTypes.js +5 -0
- package/dist/mcp-server/transports/http/index.d.ts +7 -0
- package/dist/mcp-server/transports/http/index.js +6 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.d.ts +25 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
- package/dist/mcp-server/transports/stdio/index.d.ts +5 -0
- package/dist/mcp-server/transports/stdio/index.js +5 -0
- package/dist/mcp-server/transports/{stdioTransport.d.ts → stdio/stdioTransport.d.ts} +2 -2
- package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
- package/dist/services/NCBI/{ncbiConstants.d.ts → core/ncbiConstants.d.ts} +1 -1
- package/dist/services/NCBI/{ncbiConstants.js → core/ncbiConstants.js} +1 -1
- package/dist/services/NCBI/{ncbiCoreApiClient.d.ts → core/ncbiCoreApiClient.d.ts} +3 -3
- package/dist/services/NCBI/core/ncbiCoreApiClient.js +117 -0
- package/dist/services/NCBI/{ncbiRequestQueueManager.d.ts → core/ncbiRequestQueueManager.d.ts} +2 -2
- package/dist/services/NCBI/{ncbiRequestQueueManager.js → core/ncbiRequestQueueManager.js} +3 -3
- package/dist/services/NCBI/{ncbiResponseHandler.d.ts → core/ncbiResponseHandler.d.ts} +2 -2
- package/dist/services/NCBI/{ncbiResponseHandler.js → core/ncbiResponseHandler.js} +3 -3
- package/dist/services/NCBI/{ncbiService.d.ts → core/ncbiService.d.ts} +5 -4
- package/dist/services/NCBI/{ncbiService.js → core/ncbiService.js} +15 -3
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.js +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.js +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.js +5 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.js +1 -1
- package/dist/types-global/errors.d.ts +2 -0
- package/dist/types-global/errors.js +2 -0
- package/dist/types-global/pubmedXml.d.ts +15 -0
- package/dist/utils/internal/errorHandler.js +1 -1
- package/dist/utils/internal/logger.d.ts +13 -1
- package/dist/utils/internal/logger.js +43 -9
- package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
- package/dist/utils/network/fetchWithTimeout.js +59 -0
- package/dist/utils/network/index.d.ts +6 -0
- package/dist/utils/network/index.js +5 -0
- package/dist/utils/scheduling/index.d.ts +6 -0
- package/dist/utils/scheduling/index.js +6 -0
- package/dist/utils/scheduling/scheduler.d.ts +72 -0
- package/dist/utils/scheduling/scheduler.js +150 -0
- package/dist/utils/security/sanitization.js +35 -18
- package/package.json +17 -13
- package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
- package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
- package/dist/mcp-server/transports/httpTransport.js +0 -208
- package/dist/services/NCBI/ncbiCoreApiClient.js +0 -113
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.d.ts +0 -0
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
- /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 "../../../
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
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" ||
|
|
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
|
|
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 "../../../../
|
|
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 =
|
|
32
|
+
outputData.citations.ris = cite.format("ris");
|
|
31
33
|
}
|
|
32
34
|
if (input.citationStyles?.includes("bibtex")) {
|
|
33
|
-
outputData.citations.bibtex =
|
|
35
|
+
outputData.citations.bibtex = cite.format("bibtex");
|
|
34
36
|
}
|
|
35
37
|
if (input.citationStyles?.includes("apa_string")) {
|
|
36
|
-
outputData.citations.apa_string =
|
|
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 =
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
72
|
+
const title = getText(articleDetails.ArticleTitle);
|
|
227
73
|
const doi = extractDoi(articleDetails);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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 "../../../../
|
|
9
|
-
import { ensureArray } from "../../../../
|
|
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 "../../../
|
|
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
|
|
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 (!
|
|
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(
|
|
115
|
+
responsePreview: sanitizeInputForLogging(JSON.stringify(esResult).substring(0, 200)),
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
-
const
|
|
119
|
-
const
|
|
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.
|
|
131
|
-
eSummaryParams.WebEnv = esResult.
|
|
132
|
-
eSummaryParams.query_key = esResult.
|
|
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>;
|