@cyanheads/pubmed-mcp-server
Version:
A Model Context Protocol (MCP) server enabling AI agents to intelligently search, retrieve, and analyze biomedical literature from PubMed via NCBI E-utilities. Built on the mcp-ts-template for robust, production-ready performance.
114 lines (113 loc) • 5.48 kB
JavaScript
/**
* @fileoverview Core client for making HTTP requests to NCBI E-utilities.
* Handles request construction, API key injection, retries, and basic error handling.
* @module src/services/NCBI/ncbiCoreApiClient
*/
import axios from "axios";
import { config } from "../../config/index.js";
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
import { logger, requestContextService, sanitizeInputForLogging, } from "../../utils/index.js";
import { NCBI_EUTILS_BASE_URL, } from "./ncbiConstants.js";
export class NcbiCoreApiClient {
constructor() {
this.axiosInstance = axios.create({
timeout: 30000, // 30 seconds timeout for NCBI requests
});
}
/**
* Makes an HTTP request to the specified NCBI E-utility endpoint.
* Handles parameter assembly, API key injection, GET/POST selection, and retries.
* @param endpoint The E-utility endpoint (e.g., "esearch", "efetch").
* @param params The parameters for the E-utility.
* @param context The request context for logging.
* @param options Options for the request, like retmode and whether to use POST.
* @param retries The current retry attempt number.
* @returns A Promise resolving to the raw AxiosResponse.
* @throws {McpError} If the request fails after all retries or an unexpected error occurs.
*/
async makeRequest(endpoint, params, context, options = {}, retries = 0) {
const rawParams = {
tool: config.ncbiToolIdentifier,
email: config.ncbiAdminEmail,
api_key: config.ncbiApiKey,
...params,
};
// Filter out undefined/null values and convert others to string for URLSearchParams/request body
const finalParams = {};
for (const key in rawParams) {
if (Object.prototype.hasOwnProperty.call(rawParams, key)) {
const value = rawParams[key];
if (value !== undefined && value !== null) {
finalParams[key] = String(value);
}
}
}
const requestConfig = {
method: options.usePost ? "POST" : "GET",
url: `${NCBI_EUTILS_BASE_URL}/${endpoint}.fcgi`,
};
if (options.usePost) {
requestConfig.data = new URLSearchParams(finalParams).toString();
requestConfig.headers = {
"Content-Type": "application/x-www-form-urlencoded",
};
}
else {
requestConfig.params = finalParams;
}
try {
logger.debug(`Making NCBI HTTP request: ${requestConfig.method} ${requestConfig.url}`, requestContextService.createRequestContext({
...context,
operation: "NCBI_HttpRequest",
endpoint,
method: requestConfig.method,
requestParams: sanitizeInputForLogging(finalParams),
attempt: retries + 1,
}));
const response = await this.axiosInstance(requestConfig);
return response;
}
catch (error) {
if (retries < config.ncbiMaxRetries) {
const retryDelay = Math.pow(2, retries) * 200; // Increased base delay for retries
logger.warning(`NCBI request to ${endpoint} failed. Retrying (${retries + 1}/${config.ncbiMaxRetries}) in ${retryDelay}ms...`, requestContextService.createRequestContext({
...context,
operation: "NCBI_HttpRequestRetry",
endpoint,
error: error.message,
retryCount: retries + 1,
maxRetries: config.ncbiMaxRetries,
delay: retryDelay,
}));
await new Promise((r) => setTimeout(r, retryDelay));
return this.makeRequest(endpoint, params, context, options, retries + 1);
}
if (axios.isAxiosError(error)) {
logger.error(`Axios error during NCBI request to ${endpoint} after ${retries} retries`, error, requestContextService.createRequestContext({
...context,
operation: "NCBI_AxiosError",
endpoint,
status: error.response?.status,
responseData: sanitizeInputForLogging(error.response?.data),
}));
throw new McpError(BaseErrorCode.NCBI_SERVICE_UNAVAILABLE, `NCBI request failed: ${error.message}`, {
endpoint,
status: error.response?.status,
details: error.response?.data
? String(error.response.data).substring(0, 500)
: undefined,
});
}
// If it's already an McpError, rethrow it (could be from a previous stage if this function is used more broadly)
if (error instanceof McpError)
throw error;
logger.error(`Unexpected error during NCBI request to ${endpoint} after ${retries} retries`, error, requestContextService.createRequestContext({
...context,
operation: "NCBI_UnexpectedError",
endpoint,
errorMessage: error.message,
}));
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Unexpected error communicating with NCBI: ${error.message}`, { endpoint });
}
}
}