@cyanheads/pubmed-mcp-server 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +174 -0
  3. package/dist/config/index.d.ts +91 -0
  4. package/dist/config/index.js +324 -0
  5. package/dist/index.d.ts +24 -0
  6. package/dist/index.js +190 -0
  7. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +79 -0
  8. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +82 -0
  9. package/dist/mcp-server/resources/echoResource/index.d.ts +13 -0
  10. package/dist/mcp-server/resources/echoResource/index.js +13 -0
  11. package/dist/mcp-server/resources/echoResource/registration.d.ts +30 -0
  12. package/dist/mcp-server/resources/echoResource/registration.js +168 -0
  13. package/dist/mcp-server/server.d.ts +28 -0
  14. package/dist/mcp-server/server.js +159 -0
  15. package/dist/mcp-server/tools/fetchPubMedContent/index.d.ts +6 -0
  16. package/dist/mcp-server/tools/fetchPubMedContent/index.js +6 -0
  17. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +63 -0
  18. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +518 -0
  19. package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +10 -0
  20. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +37 -0
  21. package/dist/mcp-server/tools/getPubMedArticleConnections/index.d.ts +6 -0
  22. package/dist/mcp-server/tools/getPubMedArticleConnections/index.js +6 -0
  23. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +9 -0
  24. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +288 -0
  25. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +9 -0
  26. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +208 -0
  27. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +15 -0
  28. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +82 -0
  29. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +27 -0
  30. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.js +5 -0
  31. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +6 -0
  32. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +6 -0
  33. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +36 -0
  34. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +76 -0
  35. package/dist/mcp-server/tools/pubmedResearchAgent/index.d.ts +6 -0
  36. package/dist/mcp-server/tools/pubmedResearchAgent/index.js +6 -0
  37. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.d.ts +7 -0
  38. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.js +9 -0
  39. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.d.ts +111 -0
  40. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +149 -0
  41. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.d.ts +106 -0
  42. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.js +7 -0
  43. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.d.ts +10 -0
  44. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +251 -0
  45. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +12 -0
  46. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +55 -0
  47. package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +10 -0
  48. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +36 -0
  49. package/dist/mcp-server/tools/searchPubMedArticles/index.d.ts +6 -0
  50. package/dist/mcp-server/tools/searchPubMedArticles/index.js +6 -0
  51. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +61 -0
  52. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +229 -0
  53. package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +10 -0
  54. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +38 -0
  55. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +29 -0
  56. package/dist/mcp-server/transports/authentication/authMiddleware.js +174 -0
  57. package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
  58. package/dist/mcp-server/transports/httpTransport.js +463 -0
  59. package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
  60. package/dist/mcp-server/transports/stdioTransport.js +63 -0
  61. package/dist/services/NCBI/ncbiConstants.d.ts +22 -0
  62. package/dist/services/NCBI/ncbiConstants.js +5 -0
  63. package/dist/services/NCBI/ncbiCoreApiClient.d.ts +24 -0
  64. package/dist/services/NCBI/ncbiCoreApiClient.js +118 -0
  65. package/dist/services/NCBI/ncbiRequestQueueManager.d.ts +36 -0
  66. package/dist/services/NCBI/ncbiRequestQueueManager.js +96 -0
  67. package/dist/services/NCBI/ncbiResponseHandler.d.ts +22 -0
  68. package/dist/services/NCBI/ncbiResponseHandler.js +192 -0
  69. package/dist/services/NCBI/ncbiService.d.ts +24 -0
  70. package/dist/services/NCBI/ncbiService.js +57 -0
  71. package/dist/services/index.d.ts +7 -0
  72. package/dist/services/index.js +7 -0
  73. package/dist/services/llm-providers/index.d.ts +7 -0
  74. package/dist/services/llm-providers/index.js +7 -0
  75. package/dist/services/llm-providers/llmFactory.d.ts +69 -0
  76. package/dist/services/llm-providers/llmFactory.js +132 -0
  77. package/dist/services/llm-providers/openRouter/index.d.ts +6 -0
  78. package/dist/services/llm-providers/openRouter/index.js +7 -0
  79. package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +99 -0
  80. package/dist/services/llm-providers/openRouter/openRouterProvider.js +329 -0
  81. package/dist/types-global/errors.d.ts +119 -0
  82. package/dist/types-global/errors.js +119 -0
  83. package/dist/types-global/pubmedXml.d.ts +350 -0
  84. package/dist/types-global/pubmedXml.js +7 -0
  85. package/dist/utils/index.d.ts +10 -0
  86. package/dist/utils/index.js +18 -0
  87. package/dist/utils/internal/errorHandler.d.ts +176 -0
  88. package/dist/utils/internal/errorHandler.js +335 -0
  89. package/dist/utils/internal/index.d.ts +9 -0
  90. package/dist/utils/internal/index.js +9 -0
  91. package/dist/utils/internal/logger.d.ts +141 -0
  92. package/dist/utils/internal/logger.js +426 -0
  93. package/dist/utils/internal/requestContext.d.ts +83 -0
  94. package/dist/utils/internal/requestContext.js +72 -0
  95. package/dist/utils/metrics/index.d.ts +7 -0
  96. package/dist/utils/metrics/index.js +7 -0
  97. package/dist/utils/metrics/tokenCounter.d.ts +35 -0
  98. package/dist/utils/metrics/tokenCounter.js +110 -0
  99. package/dist/utils/parsing/dateParser.d.ts +73 -0
  100. package/dist/utils/parsing/dateParser.js +107 -0
  101. package/dist/utils/parsing/index.d.ts +8 -0
  102. package/dist/utils/parsing/index.js +8 -0
  103. package/dist/utils/parsing/jsonParser.d.ts +82 -0
  104. package/dist/utils/parsing/jsonParser.js +126 -0
  105. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.d.ts +32 -0
  106. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.js +295 -0
  107. package/dist/utils/parsing/ncbi-parsing/index.d.ts +8 -0
  108. package/dist/utils/parsing/ncbi-parsing/index.js +8 -0
  109. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.d.ts +70 -0
  110. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.js +229 -0
  111. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.d.ts +31 -0
  112. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.js +76 -0
  113. package/dist/utils/security/idGenerator.d.ts +134 -0
  114. package/dist/utils/security/idGenerator.js +198 -0
  115. package/dist/utils/security/index.d.ts +9 -0
  116. package/dist/utils/security/index.js +9 -0
  117. package/dist/utils/security/rateLimiter.d.ts +108 -0
  118. package/dist/utils/security/rateLimiter.js +179 -0
  119. package/dist/utils/security/sanitization.d.ts +173 -0
  120. package/dist/utils/security/sanitization.js +439 -0
  121. package/package.json +97 -0
@@ -0,0 +1,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 Main barrel file for all services.
3
+ * This file re-exports all service modules, providing a single entry point
4
+ * for accessing various services within the application.
5
+ * @module src/services/index
6
+ */
7
+ export * from "./llm-providers";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Main barrel file for all services.
3
+ * This file re-exports all service modules, providing a single entry point
4
+ * for accessing various services within the application.
5
+ * @module src/services/index
6
+ */
7
+ export * from "./llm-providers";
@@ -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 {};