@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.
- package/LICENSE +201 -0
- package/README.md +174 -0
- package/dist/config/index.d.ts +91 -0
- package/dist/config/index.js +324 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +190 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +79 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +82 -0
- package/dist/mcp-server/resources/echoResource/index.d.ts +13 -0
- package/dist/mcp-server/resources/echoResource/index.js +13 -0
- package/dist/mcp-server/resources/echoResource/registration.d.ts +30 -0
- package/dist/mcp-server/resources/echoResource/registration.js +168 -0
- package/dist/mcp-server/server.d.ts +28 -0
- package/dist/mcp-server/server.js +159 -0
- package/dist/mcp-server/tools/fetchPubMedContent/index.d.ts +6 -0
- package/dist/mcp-server/tools/fetchPubMedContent/index.js +6 -0
- package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +63 -0
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +518 -0
- package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +10 -0
- package/dist/mcp-server/tools/fetchPubMedContent/registration.js +37 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/index.d.ts +6 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/index.js +6 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +9 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +288 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +9 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +208 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +15 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +82 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +27 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.js +5 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +6 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +6 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +36 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +76 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/index.d.ts +6 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/index.js +6 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.d.ts +7 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.js +9 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.d.ts +111 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +149 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.d.ts +106 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.js +7 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.d.ts +10 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +251 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +12 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +55 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +10 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +36 -0
- package/dist/mcp-server/tools/searchPubMedArticles/index.d.ts +6 -0
- package/dist/mcp-server/tools/searchPubMedArticles/index.js +6 -0
- package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +61 -0
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +229 -0
- package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +10 -0
- package/dist/mcp-server/tools/searchPubMedArticles/registration.js +38 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +29 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.js +174 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
- package/dist/mcp-server/transports/httpTransport.js +463 -0
- package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
- package/dist/mcp-server/transports/stdioTransport.js +63 -0
- package/dist/services/NCBI/ncbiConstants.d.ts +22 -0
- package/dist/services/NCBI/ncbiConstants.js +5 -0
- package/dist/services/NCBI/ncbiCoreApiClient.d.ts +24 -0
- package/dist/services/NCBI/ncbiCoreApiClient.js +118 -0
- package/dist/services/NCBI/ncbiRequestQueueManager.d.ts +36 -0
- package/dist/services/NCBI/ncbiRequestQueueManager.js +96 -0
- package/dist/services/NCBI/ncbiResponseHandler.d.ts +22 -0
- package/dist/services/NCBI/ncbiResponseHandler.js +192 -0
- package/dist/services/NCBI/ncbiService.d.ts +24 -0
- package/dist/services/NCBI/ncbiService.js +57 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.js +7 -0
- package/dist/services/llm-providers/index.d.ts +7 -0
- package/dist/services/llm-providers/index.js +7 -0
- package/dist/services/llm-providers/llmFactory.d.ts +69 -0
- package/dist/services/llm-providers/llmFactory.js +132 -0
- package/dist/services/llm-providers/openRouter/index.d.ts +6 -0
- package/dist/services/llm-providers/openRouter/index.js +7 -0
- package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +99 -0
- package/dist/services/llm-providers/openRouter/openRouterProvider.js +329 -0
- package/dist/types-global/errors.d.ts +119 -0
- package/dist/types-global/errors.js +119 -0
- package/dist/types-global/pubmedXml.d.ts +350 -0
- package/dist/types-global/pubmedXml.js +7 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.js +18 -0
- package/dist/utils/internal/errorHandler.d.ts +176 -0
- package/dist/utils/internal/errorHandler.js +335 -0
- package/dist/utils/internal/index.d.ts +9 -0
- package/dist/utils/internal/index.js +9 -0
- package/dist/utils/internal/logger.d.ts +141 -0
- package/dist/utils/internal/logger.js +426 -0
- package/dist/utils/internal/requestContext.d.ts +83 -0
- package/dist/utils/internal/requestContext.js +72 -0
- package/dist/utils/metrics/index.d.ts +7 -0
- package/dist/utils/metrics/index.js +7 -0
- package/dist/utils/metrics/tokenCounter.d.ts +35 -0
- package/dist/utils/metrics/tokenCounter.js +110 -0
- package/dist/utils/parsing/dateParser.d.ts +73 -0
- package/dist/utils/parsing/dateParser.js +107 -0
- package/dist/utils/parsing/index.d.ts +8 -0
- package/dist/utils/parsing/index.js +8 -0
- package/dist/utils/parsing/jsonParser.d.ts +82 -0
- package/dist/utils/parsing/jsonParser.js +126 -0
- package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.d.ts +32 -0
- package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.js +295 -0
- package/dist/utils/parsing/ncbi-parsing/index.d.ts +8 -0
- package/dist/utils/parsing/ncbi-parsing/index.js +8 -0
- package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.d.ts +70 -0
- package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.js +229 -0
- package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.d.ts +31 -0
- package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.js +76 -0
- package/dist/utils/security/idGenerator.d.ts +134 -0
- package/dist/utils/security/idGenerator.js +198 -0
- package/dist/utils/security/index.d.ts +9 -0
- package/dist/utils/security/index.js +9 -0
- package/dist/utils/security/rateLimiter.d.ts +108 -0
- package/dist/utils/security/rateLimiter.js +179 -0
- package/dist/utils/security/sanitization.d.ts +173 -0
- package/dist/utils/security/sanitization.js +439 -0
- package/package.json +97 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core client for making HTTP requests to NCBI E-utilities.
|
|
3
|
+
* Handles request construction, API key injection, retries, and basic error handling.
|
|
4
|
+
* @module src/services/NCBI/ncbiCoreApiClient
|
|
5
|
+
*/
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
import { config } from "../../config/index.js";
|
|
8
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
9
|
+
import { logger, requestContextService, sanitizeInputForLogging, } from "../../utils/index.js";
|
|
10
|
+
import { NCBI_EUTILS_BASE_URL, } from "./ncbiConstants.js";
|
|
11
|
+
export class NcbiCoreApiClient {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.axiosInstance = axios.create({
|
|
14
|
+
timeout: 30000, // 30 seconds timeout for NCBI requests
|
|
15
|
+
});
|
|
16
|
+
logger.debug("NcbiCoreApiClient initialized", requestContextService.createRequestContext({
|
|
17
|
+
service: "NcbiCoreApiClient",
|
|
18
|
+
ncbiBaseUrl: NCBI_EUTILS_BASE_URL,
|
|
19
|
+
maxRetries: config.ncbiMaxRetries,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Makes an HTTP request to the specified NCBI E-utility endpoint.
|
|
24
|
+
* Handles parameter assembly, API key injection, GET/POST selection, and retries.
|
|
25
|
+
* @param endpoint The E-utility endpoint (e.g., "esearch", "efetch").
|
|
26
|
+
* @param params The parameters for the E-utility.
|
|
27
|
+
* @param context The request context for logging.
|
|
28
|
+
* @param options Options for the request, like retmode and whether to use POST.
|
|
29
|
+
* @param retries The current retry attempt number.
|
|
30
|
+
* @returns A Promise resolving to the raw AxiosResponse.
|
|
31
|
+
* @throws {McpError} If the request fails after all retries or an unexpected error occurs.
|
|
32
|
+
*/
|
|
33
|
+
async makeRequest(endpoint, params, context, options = {}, retries = 0) {
|
|
34
|
+
const rawParams = {
|
|
35
|
+
tool: config.ncbiToolIdentifier,
|
|
36
|
+
email: config.ncbiAdminEmail,
|
|
37
|
+
api_key: config.ncbiApiKey,
|
|
38
|
+
...params,
|
|
39
|
+
};
|
|
40
|
+
// Filter out undefined/null values and convert others to string for URLSearchParams/request body
|
|
41
|
+
const finalParams = {};
|
|
42
|
+
for (const key in rawParams) {
|
|
43
|
+
if (Object.prototype.hasOwnProperty.call(rawParams, key)) {
|
|
44
|
+
const value = rawParams[key];
|
|
45
|
+
if (value !== undefined && value !== null) {
|
|
46
|
+
finalParams[key] = String(value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const requestConfig = {
|
|
51
|
+
method: options.usePost ? "POST" : "GET",
|
|
52
|
+
url: `${NCBI_EUTILS_BASE_URL}/${endpoint}.fcgi`,
|
|
53
|
+
};
|
|
54
|
+
if (options.usePost) {
|
|
55
|
+
requestConfig.data = new URLSearchParams(finalParams).toString();
|
|
56
|
+
requestConfig.headers = {
|
|
57
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
requestConfig.params = finalParams;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
logger.debug(`Making NCBI HTTP request: ${requestConfig.method} ${requestConfig.url}`, requestContextService.createRequestContext({
|
|
65
|
+
...context,
|
|
66
|
+
operation: "NCBI_HttpRequest",
|
|
67
|
+
endpoint,
|
|
68
|
+
method: requestConfig.method,
|
|
69
|
+
requestParams: sanitizeInputForLogging(finalParams),
|
|
70
|
+
attempt: retries + 1,
|
|
71
|
+
}));
|
|
72
|
+
const response = await this.axiosInstance(requestConfig);
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (retries < config.ncbiMaxRetries) {
|
|
77
|
+
const retryDelay = Math.pow(2, retries) * 200; // Increased base delay for retries
|
|
78
|
+
logger.warning(`NCBI request to ${endpoint} failed. Retrying (${retries + 1}/${config.ncbiMaxRetries}) in ${retryDelay}ms...`, requestContextService.createRequestContext({
|
|
79
|
+
...context,
|
|
80
|
+
operation: "NCBI_HttpRequestRetry",
|
|
81
|
+
endpoint,
|
|
82
|
+
error: error.message,
|
|
83
|
+
retryCount: retries + 1,
|
|
84
|
+
maxRetries: config.ncbiMaxRetries,
|
|
85
|
+
delay: retryDelay,
|
|
86
|
+
}));
|
|
87
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
88
|
+
return this.makeRequest(endpoint, params, context, options, retries + 1);
|
|
89
|
+
}
|
|
90
|
+
if (axios.isAxiosError(error)) {
|
|
91
|
+
logger.error(`Axios error during NCBI request to ${endpoint} after ${retries} retries`, error, requestContextService.createRequestContext({
|
|
92
|
+
...context,
|
|
93
|
+
operation: "NCBI_AxiosError",
|
|
94
|
+
endpoint,
|
|
95
|
+
status: error.response?.status,
|
|
96
|
+
responseData: sanitizeInputForLogging(error.response?.data),
|
|
97
|
+
}));
|
|
98
|
+
throw new McpError(BaseErrorCode.NCBI_SERVICE_UNAVAILABLE, `NCBI request failed: ${error.message}`, {
|
|
99
|
+
endpoint,
|
|
100
|
+
status: error.response?.status,
|
|
101
|
+
details: error.response?.data
|
|
102
|
+
? String(error.response.data).substring(0, 500)
|
|
103
|
+
: undefined,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// If it's already an McpError, rethrow it (could be from a previous stage if this function is used more broadly)
|
|
107
|
+
if (error instanceof McpError)
|
|
108
|
+
throw error;
|
|
109
|
+
logger.error(`Unexpected error during NCBI request to ${endpoint} after ${retries} retries`, error, requestContextService.createRequestContext({
|
|
110
|
+
...context,
|
|
111
|
+
operation: "NCBI_UnexpectedError",
|
|
112
|
+
endpoint,
|
|
113
|
+
errorMessage: error.message,
|
|
114
|
+
}));
|
|
115
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Unexpected error communicating with NCBI: ${error.message}`, { endpoint });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Manages a queue for NCBI E-utility requests to ensure compliance with rate limits.
|
|
3
|
+
* @module src/services/NCBI/ncbiRequestQueueManager
|
|
4
|
+
*/
|
|
5
|
+
import { RequestContext } from "../../utils/index.js";
|
|
6
|
+
import { NcbiRequestParams } from "./ncbiConstants.js";
|
|
7
|
+
/**
|
|
8
|
+
* Interface for a queued NCBI request.
|
|
9
|
+
*/
|
|
10
|
+
export interface QueuedRequest {
|
|
11
|
+
resolve: (value: any) => void;
|
|
12
|
+
reject: (reason?: any) => void;
|
|
13
|
+
task: () => Promise<any>;
|
|
14
|
+
context: RequestContext;
|
|
15
|
+
endpoint: string;
|
|
16
|
+
params: NcbiRequestParams;
|
|
17
|
+
}
|
|
18
|
+
export declare class NcbiRequestQueueManager {
|
|
19
|
+
private requestQueue;
|
|
20
|
+
private isProcessingQueue;
|
|
21
|
+
private lastRequestTime;
|
|
22
|
+
constructor();
|
|
23
|
+
/**
|
|
24
|
+
* Processes the request queue, ensuring delays between requests to respect NCBI rate limits.
|
|
25
|
+
*/
|
|
26
|
+
private processQueue;
|
|
27
|
+
/**
|
|
28
|
+
* Enqueues a task (an NCBI API call) to be processed.
|
|
29
|
+
* @param task A function that returns a Promise resolving to the API call result.
|
|
30
|
+
* @param context The request context for logging and correlation.
|
|
31
|
+
* @param endpoint The NCBI endpoint being called (e.g., "esearch", "efetch").
|
|
32
|
+
* @param params The parameters for the NCBI request.
|
|
33
|
+
* @returns A Promise that resolves or rejects with the result of the task.
|
|
34
|
+
*/
|
|
35
|
+
enqueueRequest<T>(task: () => Promise<T>, context: RequestContext, endpoint: string, params: NcbiRequestParams): Promise<T>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Manages a queue for NCBI E-utility requests to ensure compliance with rate limits.
|
|
3
|
+
* @module src/services/NCBI/ncbiRequestQueueManager
|
|
4
|
+
*/
|
|
5
|
+
import { config } from "../../config/index.js";
|
|
6
|
+
import { logger, requestContextService, sanitizeInputForLogging, } from "../../utils/index.js";
|
|
7
|
+
export class NcbiRequestQueueManager {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.requestQueue = [];
|
|
10
|
+
this.isProcessingQueue = false;
|
|
11
|
+
this.lastRequestTime = 0;
|
|
12
|
+
logger.debug("NcbiRequestQueueManager initialized", requestContextService.createRequestContext({
|
|
13
|
+
service: "NcbiRequestQueueManager",
|
|
14
|
+
requestDelay: config.ncbiRequestDelayMs,
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Processes the request queue, ensuring delays between requests to respect NCBI rate limits.
|
|
19
|
+
*/
|
|
20
|
+
async processQueue() {
|
|
21
|
+
if (this.isProcessingQueue || this.requestQueue.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.isProcessingQueue = true;
|
|
25
|
+
const requestItem = this.requestQueue.shift();
|
|
26
|
+
if (!requestItem) {
|
|
27
|
+
this.isProcessingQueue = false;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const { resolve, reject, task, context, endpoint, params } = requestItem;
|
|
31
|
+
try {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
34
|
+
const delayNeeded = config.ncbiRequestDelayMs - timeSinceLastRequest;
|
|
35
|
+
if (delayNeeded > 0) {
|
|
36
|
+
logger.debug(`Delaying NCBI request by ${delayNeeded}ms to respect rate limit.`, requestContextService.createRequestContext({
|
|
37
|
+
...context,
|
|
38
|
+
operation: "NCBI_RateLimitDelay",
|
|
39
|
+
delayNeeded,
|
|
40
|
+
endpoint,
|
|
41
|
+
}));
|
|
42
|
+
await new Promise((r) => setTimeout(r, delayNeeded));
|
|
43
|
+
}
|
|
44
|
+
this.lastRequestTime = Date.now();
|
|
45
|
+
logger.info(`Executing NCBI request via queue: ${endpoint}`, requestContextService.createRequestContext({
|
|
46
|
+
...context,
|
|
47
|
+
operation: "NCBI_ExecuteFromQueue",
|
|
48
|
+
endpoint,
|
|
49
|
+
params: sanitizeInputForLogging(params),
|
|
50
|
+
}));
|
|
51
|
+
const result = await task();
|
|
52
|
+
resolve(result);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
logger.error("Error processing NCBI request from queue", error instanceof Error ? error : new Error(String(error)), requestContextService.createRequestContext({
|
|
56
|
+
...context,
|
|
57
|
+
operation: "NCBI_QueueError",
|
|
58
|
+
endpoint,
|
|
59
|
+
params: sanitizeInputForLogging(params),
|
|
60
|
+
errorMessage: error?.message,
|
|
61
|
+
}));
|
|
62
|
+
reject(error);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
this.isProcessingQueue = false;
|
|
66
|
+
if (this.requestQueue.length > 0) {
|
|
67
|
+
// Ensure processQueue is called without awaiting it here to prevent deep stacks
|
|
68
|
+
Promise.resolve().then(() => this.processQueue());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Enqueues a task (an NCBI API call) to be processed.
|
|
74
|
+
* @param task A function that returns a Promise resolving to the API call result.
|
|
75
|
+
* @param context The request context for logging and correlation.
|
|
76
|
+
* @param endpoint The NCBI endpoint being called (e.g., "esearch", "efetch").
|
|
77
|
+
* @param params The parameters for the NCBI request.
|
|
78
|
+
* @returns A Promise that resolves or rejects with the result of the task.
|
|
79
|
+
*/
|
|
80
|
+
enqueueRequest(task, context, endpoint, params) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
this.requestQueue.push({
|
|
83
|
+
resolve,
|
|
84
|
+
reject,
|
|
85
|
+
task,
|
|
86
|
+
context,
|
|
87
|
+
endpoint,
|
|
88
|
+
params,
|
|
89
|
+
});
|
|
90
|
+
if (!this.isProcessingQueue) {
|
|
91
|
+
// Ensure processQueue is called without awaiting it here
|
|
92
|
+
Promise.resolve().then(() => this.processQueue());
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Handles parsing of NCBI E-utility responses and NCBI-specific error extraction.
|
|
3
|
+
* @module src/services/NCBI/ncbiResponseHandler
|
|
4
|
+
*/
|
|
5
|
+
import { AxiosResponse } from "axios";
|
|
6
|
+
import { RequestContext } from "../../utils/index.js";
|
|
7
|
+
import { NcbiRequestOptions } from "./ncbiConstants.js";
|
|
8
|
+
export declare class NcbiResponseHandler {
|
|
9
|
+
private xmlParser;
|
|
10
|
+
constructor();
|
|
11
|
+
private extractNcbiErrorMessages;
|
|
12
|
+
/**
|
|
13
|
+
* Parses the raw AxiosResponse data based on retmode and checks for NCBI-specific errors.
|
|
14
|
+
* @param response The raw AxiosResponse from an NCBI E-utility call.
|
|
15
|
+
* @param endpoint The E-utility endpoint for context.
|
|
16
|
+
* @param context The request context for logging.
|
|
17
|
+
* @param options The original request options, particularly `retmode`.
|
|
18
|
+
* @returns The parsed data (object for XML/JSON, string for text).
|
|
19
|
+
* @throws {McpError} If parsing fails or NCBI reports an error in the response body.
|
|
20
|
+
*/
|
|
21
|
+
parseAndHandleResponse<T = any>(response: AxiosResponse, endpoint: string, context: RequestContext, options: NcbiRequestOptions): T;
|
|
22
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Handles parsing of NCBI E-utility responses and NCBI-specific error extraction.
|
|
3
|
+
* @module src/services/NCBI/ncbiResponseHandler
|
|
4
|
+
*/
|
|
5
|
+
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
6
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
7
|
+
import { logger, requestContextService, sanitizeInputForLogging, } from "../../utils/index.js";
|
|
8
|
+
export class NcbiResponseHandler {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.xmlParser = new XMLParser({
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
attributeNamePrefix: "@_",
|
|
13
|
+
parseTagValue: true, // auto-convert numbers, booleans if possible
|
|
14
|
+
isArray: (name, jpath, isLeafNode, isAttribute) => {
|
|
15
|
+
// Common NCBI list tags - expand as needed
|
|
16
|
+
const arrayTags = [
|
|
17
|
+
"IdList.Id",
|
|
18
|
+
"eSearchResult.IdList.Id",
|
|
19
|
+
"PubmedArticleSet.PubmedArticle",
|
|
20
|
+
"PubmedArticleSet.DeleteCitation.PMID",
|
|
21
|
+
"AuthorList.Author",
|
|
22
|
+
"MeshHeadingList.MeshHeading",
|
|
23
|
+
"GrantList.Grant",
|
|
24
|
+
"KeywordList.Keyword",
|
|
25
|
+
"PublicationTypeList.PublicationType",
|
|
26
|
+
"LinkSet.LinkSetDb.Link",
|
|
27
|
+
"Link.Id",
|
|
28
|
+
"DbInfo.FieldList.Field",
|
|
29
|
+
"DbInfo.LinkList.Link",
|
|
30
|
+
"DocSum.Item", // For ESummary v2.0 JSON-like XML
|
|
31
|
+
];
|
|
32
|
+
return arrayTags.includes(jpath);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
logger.debug("NcbiResponseHandler initialized", requestContextService.createRequestContext({
|
|
36
|
+
service: "NcbiResponseHandler",
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
extractNcbiErrorMessages(parsedXml) {
|
|
40
|
+
const messages = [];
|
|
41
|
+
// Order matters for specificity if multiple error types could exist
|
|
42
|
+
const errorPaths = [
|
|
43
|
+
"eLinkResult.ERROR",
|
|
44
|
+
"eSummaryResult.ERROR",
|
|
45
|
+
"eSearchResult.ErrorList.PhraseNotFound",
|
|
46
|
+
"eSearchResult.ErrorList.FieldNotFound",
|
|
47
|
+
"PubmedArticleSet.ErrorList.CannotRetrievePMID", // More specific error
|
|
48
|
+
"ERROR", // Generic top-level error
|
|
49
|
+
];
|
|
50
|
+
for (const path of errorPaths) {
|
|
51
|
+
let errorSource = parsedXml;
|
|
52
|
+
const parts = path.split(".");
|
|
53
|
+
for (const part of parts) {
|
|
54
|
+
if (errorSource &&
|
|
55
|
+
typeof errorSource === "object" &&
|
|
56
|
+
part in errorSource) {
|
|
57
|
+
errorSource = errorSource[part];
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
errorSource = undefined;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (errorSource) {
|
|
65
|
+
const items = Array.isArray(errorSource) ? errorSource : [errorSource];
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
if (typeof item === "string") {
|
|
68
|
+
messages.push(item);
|
|
69
|
+
}
|
|
70
|
+
else if (item && typeof item["#text"] === "string") {
|
|
71
|
+
messages.push(item["#text"]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Handle warnings if no primary errors found
|
|
77
|
+
if (messages.length === 0 && parsedXml.eSearchResult?.WarningList) {
|
|
78
|
+
const warningPaths = [
|
|
79
|
+
"eSearchResult.WarningList.QuotedPhraseNotFound",
|
|
80
|
+
"eSearchResult.WarningList.OutputMessage",
|
|
81
|
+
];
|
|
82
|
+
for (const path of warningPaths) {
|
|
83
|
+
let warningSource = parsedXml;
|
|
84
|
+
const parts = path.split(".");
|
|
85
|
+
for (const part of parts) {
|
|
86
|
+
if (warningSource &&
|
|
87
|
+
typeof warningSource === "object" &&
|
|
88
|
+
part in warningSource) {
|
|
89
|
+
warningSource = warningSource[part];
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
warningSource = undefined;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (warningSource) {
|
|
97
|
+
const items = Array.isArray(warningSource)
|
|
98
|
+
? warningSource
|
|
99
|
+
: [warningSource];
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
if (typeof item === "string") {
|
|
102
|
+
messages.push(`Warning: ${item}`);
|
|
103
|
+
}
|
|
104
|
+
else if (item && typeof item["#text"] === "string") {
|
|
105
|
+
messages.push(`Warning: ${item["#text"]}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return messages.length > 0
|
|
112
|
+
? messages
|
|
113
|
+
: ["Unknown NCBI API error structure."];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parses the raw AxiosResponse data based on retmode and checks for NCBI-specific errors.
|
|
117
|
+
* @param response The raw AxiosResponse from an NCBI E-utility call.
|
|
118
|
+
* @param endpoint The E-utility endpoint for context.
|
|
119
|
+
* @param context The request context for logging.
|
|
120
|
+
* @param options The original request options, particularly `retmode`.
|
|
121
|
+
* @returns The parsed data (object for XML/JSON, string for text).
|
|
122
|
+
* @throws {McpError} If parsing fails or NCBI reports an error in the response body.
|
|
123
|
+
*/
|
|
124
|
+
parseAndHandleResponse(response, endpoint, context, options) {
|
|
125
|
+
const responseData = response.data;
|
|
126
|
+
const operationContext = requestContextService.createRequestContext({
|
|
127
|
+
...context,
|
|
128
|
+
operation: "NCBI_ParseResponse",
|
|
129
|
+
endpoint,
|
|
130
|
+
retmode: options.retmode,
|
|
131
|
+
});
|
|
132
|
+
if (options.retmode === "text") {
|
|
133
|
+
logger.debug("Received text response from NCBI.", operationContext);
|
|
134
|
+
return responseData;
|
|
135
|
+
}
|
|
136
|
+
if (options.retmode === "xml") {
|
|
137
|
+
logger.debug("Attempting to parse XML response from NCBI.", operationContext);
|
|
138
|
+
if (typeof responseData !== "string" ||
|
|
139
|
+
XMLValidator.validate(responseData) !== true) {
|
|
140
|
+
logger.error("Invalid or non-string XML response from NCBI", new Error("Invalid XML structure"), {
|
|
141
|
+
...operationContext,
|
|
142
|
+
responseSnippet: String(responseData).substring(0, 500),
|
|
143
|
+
});
|
|
144
|
+
throw new McpError(BaseErrorCode.NCBI_PARSING_ERROR, "Received invalid XML from NCBI.", { endpoint, responseSnippet: String(responseData).substring(0, 200) });
|
|
145
|
+
}
|
|
146
|
+
// Always parse for error checking, even if returning raw XML
|
|
147
|
+
const parsedXml = this.xmlParser.parse(responseData);
|
|
148
|
+
// Check for error indicators within the parsed XML structure
|
|
149
|
+
if (parsedXml.eSearchResult?.ErrorList ||
|
|
150
|
+
parsedXml.eLinkResult?.ERROR ||
|
|
151
|
+
parsedXml.eSummaryResult?.ERROR ||
|
|
152
|
+
parsedXml.PubmedArticleSet?.ErrorList || // Check for ErrorList specifically
|
|
153
|
+
parsedXml.ERROR // Generic top-level error
|
|
154
|
+
) {
|
|
155
|
+
const errorMessages = this.extractNcbiErrorMessages(parsedXml);
|
|
156
|
+
logger.error("NCBI API returned an error in XML response", new Error(errorMessages.join("; ")), {
|
|
157
|
+
...operationContext,
|
|
158
|
+
errors: errorMessages,
|
|
159
|
+
parsedXml: sanitizeInputForLogging(parsedXml), // Log the parsed structure for error diagnosis
|
|
160
|
+
});
|
|
161
|
+
throw new McpError(BaseErrorCode.NCBI_API_ERROR, `NCBI API Error: ${errorMessages.join("; ")}`, { endpoint, ncbiErrors: errorMessages });
|
|
162
|
+
}
|
|
163
|
+
// If raw XML is requested and no errors were found, return the original string
|
|
164
|
+
if (options.returnRawXml) {
|
|
165
|
+
logger.debug("Successfully validated XML response. Returning raw XML string as requested.", operationContext);
|
|
166
|
+
return responseData; // responseData is the raw XML string
|
|
167
|
+
}
|
|
168
|
+
logger.debug("Successfully parsed XML response. Returning parsed object.", operationContext);
|
|
169
|
+
return parsedXml; // Return the parsed object by default
|
|
170
|
+
}
|
|
171
|
+
if (options.retmode === "json") {
|
|
172
|
+
logger.debug("Handling JSON response from NCBI.", operationContext);
|
|
173
|
+
// Assuming responseData is already parsed by Axios if Content-Type was application/json
|
|
174
|
+
if (typeof responseData === "object" &&
|
|
175
|
+
responseData !== null &&
|
|
176
|
+
responseData.error) {
|
|
177
|
+
const errorMessage = String(responseData.error);
|
|
178
|
+
logger.error("NCBI API returned an error in JSON response", new Error(errorMessage), {
|
|
179
|
+
...operationContext,
|
|
180
|
+
error: errorMessage,
|
|
181
|
+
responseData: sanitizeInputForLogging(responseData),
|
|
182
|
+
});
|
|
183
|
+
throw new McpError(BaseErrorCode.NCBI_API_ERROR, `NCBI API Error: ${errorMessage}`, { endpoint, ncbiError: errorMessage });
|
|
184
|
+
}
|
|
185
|
+
logger.debug("Successfully processed JSON response.", operationContext);
|
|
186
|
+
return responseData;
|
|
187
|
+
}
|
|
188
|
+
// Fallback for unknown retmode or if retmode is undefined
|
|
189
|
+
logger.warning(`Response received with unspecified or unhandled retmode: ${options.retmode}. Returning raw data.`, operationContext);
|
|
190
|
+
return responseData;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Service for interacting with NCBI E-utilities.
|
|
3
|
+
* This module centralizes all communication with NCBI's E-utility APIs,
|
|
4
|
+
* handling request construction, API key management, rate limiting,
|
|
5
|
+
* retries, and parsing of XML/JSON responses. It aims to provide a robust
|
|
6
|
+
* and compliant interface for other parts of the pubmed-mcp-server to
|
|
7
|
+
* access PubMed data.
|
|
8
|
+
* @module src/services/NCBI/ncbiService
|
|
9
|
+
*/
|
|
10
|
+
import { RequestContext } from "../../utils/index.js";
|
|
11
|
+
import { NcbiRequestParams, NcbiRequestOptions } from "./ncbiConstants.js";
|
|
12
|
+
export declare class NcbiService {
|
|
13
|
+
private queueManager;
|
|
14
|
+
private apiClient;
|
|
15
|
+
private responseHandler;
|
|
16
|
+
constructor();
|
|
17
|
+
private performNcbiRequest;
|
|
18
|
+
eSearch(params: NcbiRequestParams, context: RequestContext): Promise<any>;
|
|
19
|
+
eSummary(params: NcbiRequestParams, context: RequestContext): Promise<any>;
|
|
20
|
+
eFetch(params: NcbiRequestParams, context: RequestContext, options?: NcbiRequestOptions): Promise<any>;
|
|
21
|
+
eLink(params: NcbiRequestParams, context: RequestContext): Promise<any>;
|
|
22
|
+
eInfo(params: NcbiRequestParams, context: RequestContext): Promise<any>;
|
|
23
|
+
}
|
|
24
|
+
export declare const ncbiService: NcbiService;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Service for interacting with NCBI E-utilities.
|
|
3
|
+
* This module centralizes all communication with NCBI's E-utility APIs,
|
|
4
|
+
* handling request construction, API key management, rate limiting,
|
|
5
|
+
* retries, and parsing of XML/JSON responses. It aims to provide a robust
|
|
6
|
+
* and compliant interface for other parts of the pubmed-mcp-server to
|
|
7
|
+
* access PubMed data.
|
|
8
|
+
* @module src/services/NCBI/ncbiService
|
|
9
|
+
*/
|
|
10
|
+
import { logger, requestContextService, } from "../../utils/index.js";
|
|
11
|
+
import { NcbiCoreApiClient } from "./ncbiCoreApiClient.js";
|
|
12
|
+
import { NcbiRequestQueueManager } from "./ncbiRequestQueueManager.js";
|
|
13
|
+
import { NcbiResponseHandler } from "./ncbiResponseHandler.js";
|
|
14
|
+
export class NcbiService {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.queueManager = new NcbiRequestQueueManager();
|
|
17
|
+
this.apiClient = new NcbiCoreApiClient();
|
|
18
|
+
this.responseHandler = new NcbiResponseHandler();
|
|
19
|
+
logger.debug("NcbiService initialized with new modular architecture", requestContextService.createRequestContext({
|
|
20
|
+
service: "NcbiService",
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
async performNcbiRequest(endpoint, params, context, options = {}) {
|
|
24
|
+
const task = async () => {
|
|
25
|
+
const rawResponse = await this.apiClient.makeRequest(endpoint, params, context, options);
|
|
26
|
+
return this.responseHandler.parseAndHandleResponse(rawResponse, endpoint, context, options);
|
|
27
|
+
};
|
|
28
|
+
return this.queueManager.enqueueRequest(task, context, endpoint, params);
|
|
29
|
+
}
|
|
30
|
+
async eSearch(params, context) {
|
|
31
|
+
return this.performNcbiRequest("esearch", params, context, {
|
|
32
|
+
retmode: "xml",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async eSummary(params, context) {
|
|
36
|
+
// Determine retmode based on params, default to xml
|
|
37
|
+
const retmode = params.version === "2.0" && params.retmode === "json" ? "json" : "xml";
|
|
38
|
+
return this.performNcbiRequest("esummary", params, context, { retmode });
|
|
39
|
+
}
|
|
40
|
+
async eFetch(params, context, options = { retmode: "xml" }) {
|
|
41
|
+
// Determine if POST should be used based on number of IDs
|
|
42
|
+
const usePost = typeof params.id === "string" && params.id.split(",").length > 200;
|
|
43
|
+
const fetchOptions = { ...options, usePost };
|
|
44
|
+
return this.performNcbiRequest("efetch", params, context, fetchOptions);
|
|
45
|
+
}
|
|
46
|
+
async eLink(params, context) {
|
|
47
|
+
return this.performNcbiRequest("elink", params, context, {
|
|
48
|
+
retmode: "xml",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async eInfo(params, context) {
|
|
52
|
+
return this.performNcbiRequest("einfo", params, context, {
|
|
53
|
+
retmode: "xml",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export const ncbiService = new NcbiService();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for LLM provider services.
|
|
3
|
+
* This file re-exports all services related to different Large Language Model providers,
|
|
4
|
+
* making them easily accessible from a single import path.
|
|
5
|
+
* @module src/services/llm-providers/index
|
|
6
|
+
*/
|
|
7
|
+
export * from "./openRouter";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for LLM provider services.
|
|
3
|
+
* This file re-exports all services related to different Large Language Model providers,
|
|
4
|
+
* making them easily accessible from a single import path.
|
|
5
|
+
* @module src/services/llm-providers/index
|
|
6
|
+
*/
|
|
7
|
+
export * from "./openRouter"; // Changed to export from the new barrel file
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Factory for creating LLM client instances.
|
|
3
|
+
* Provides a centralized way to instantiate clients for different LLM providers
|
|
4
|
+
* like OpenRouter and Google Gemini, handling API key configuration and
|
|
5
|
+
* basic client setup.
|
|
6
|
+
* @module src/services/llm-providers/llmFactory
|
|
7
|
+
*/
|
|
8
|
+
import { GoogleGenAI } from "@google/genai";
|
|
9
|
+
import OpenAI from "openai";
|
|
10
|
+
import { RequestContext } from "../../utils/index.js";
|
|
11
|
+
/**
|
|
12
|
+
* Defines the supported LLM providers.
|
|
13
|
+
*/
|
|
14
|
+
export type LlmProviderType = "openrouter" | "gemini";
|
|
15
|
+
/**
|
|
16
|
+
* Options for configuring the OpenRouter client.
|
|
17
|
+
*/
|
|
18
|
+
export interface OpenRouterClientOptions {
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
baseURL?: string;
|
|
21
|
+
siteUrl?: string;
|
|
22
|
+
siteName?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Options for configuring the Gemini client using @google/genai.
|
|
26
|
+
* The factory will return a GoogleGenAI instance.
|
|
27
|
+
* Vertex AI specific options are included here.
|
|
28
|
+
*/
|
|
29
|
+
export interface GeminiClientOptions {
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
useVertexAi?: boolean;
|
|
32
|
+
project?: string;
|
|
33
|
+
location?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Union type for all LLM client options.
|
|
37
|
+
*/
|
|
38
|
+
export type LlmClientOptions = OpenRouterClientOptions | GeminiClientOptions;
|
|
39
|
+
/**
|
|
40
|
+
* LLM Factory class to create and configure LLM clients.
|
|
41
|
+
*/
|
|
42
|
+
declare class LlmFactory {
|
|
43
|
+
/**
|
|
44
|
+
* Creates and returns an LLM client instance for the specified provider.
|
|
45
|
+
*
|
|
46
|
+
* @param provider - The LLM provider to create a client for.
|
|
47
|
+
* @param context - The request context for logging.
|
|
48
|
+
* @param options - Optional provider-specific configuration options.
|
|
49
|
+
* @returns A Promise resolving to an instance of OpenAI (for OpenRouter)
|
|
50
|
+
* or GoogleGenAI (for Gemini).
|
|
51
|
+
* @throws {McpError} If the provider is unsupported or API key/config is missing.
|
|
52
|
+
*/
|
|
53
|
+
getLlmClient(provider: LlmProviderType, context: RequestContext, options?: LlmClientOptions): Promise<OpenAI | GoogleGenAI>;
|
|
54
|
+
/**
|
|
55
|
+
* Creates an OpenAI client configured for OpenRouter.
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
private createOpenRouterClient;
|
|
59
|
+
/**
|
|
60
|
+
* Creates a GoogleGenAI client for Gemini, supporting standard API key or Vertex AI.
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
private createGeminiClient;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Singleton instance of the LlmFactory.
|
|
67
|
+
*/
|
|
68
|
+
export declare const llmFactory: LlmFactory;
|
|
69
|
+
export {};
|