clinicaltrialsgov-mcp-server
Version:
ClinicalTrials.gov Model Context Protocol (MCP) Server that provides a suite of tools for interacting with the official ClinicalTrials.gov v2 API. Enables AI agents and LLMs to programmatically search, retrieve, and analyze clinical trial data.
141 lines (140 loc) • 5.69 kB
JavaScript
/**
* @fileoverview Defines the core logic, schemas, and types for the `clinicaltrials_get_study` tool.
* @module src/mcp-server/tools/getStudy/logic
*/
import { z } from "zod";
import { ClinicalTrialsGovService, StudySchema, } from "../../../services/clinical-trials-gov/index.js";
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
import { cleanStudy } from "../../../utils/clinicaltrials/jsonCleaner.js";
import { logger } from "../../../utils/index.js";
/**
* Zod schema for the input of the `clinicaltrials_get_study` tool.
*/
export const GetStudyInputSchema = z.object({
nctIds: z
.union([
z.string().regex(/^[Nn][Cc][Tt]\d{8}$/),
z
.array(z.string().regex(/^[Nn][Cc][Tt]\d{8}$/))
.min(1)
.max(5),
])
.describe("A single NCT ID (e.g., 'NCT12345678') or an array of up to 5 NCT IDs to fetch. Each ID must be 8 digits."),
markupFormat: z
.enum(["markdown", "legacy"])
.default("markdown")
.optional()
.describe("Format for rich text fields. 'markdown' provides formatted text, while 'legacy' provides the original. Defaults to 'markdown'."),
fields: z
.array(z.string())
.optional()
.describe("A list of specific top-level fields to return (e.g., ['protocolSection', 'derivedSection']). If omitted, all fields are returned."),
summaryOnly: z
.boolean()
.default(false)
.optional()
.describe("If true, returns a concise summary of each study. If false (default), returns the complete study data."),
});
/**
* Zod schema for a summarized study, containing only essential fields for a concise overview.
*/
export const StudySummarySchema = z.object({
nctId: z.string().optional(),
title: z.string().optional(),
briefSummary: z.string().optional(),
overallStatus: z.string().optional(),
conditions: z.array(z.string()).optional(),
interventions: z
.array(z.object({ name: z.string().optional(), type: z.string().optional() }))
.optional(),
leadSponsor: z.string().optional(),
});
/**
* Zod schema for the output of the `clinicaltrials_get_study` tool.
*/
export const GetStudyOutputSchema = z.object({
studies: z.array(z.union([StudySchema, StudySummarySchema])),
errors: z
.array(z.object({
nctId: z.string(),
error: z.string(),
}))
.optional(),
});
/**
* Extracts a concise summary from a full study object.
* @param study - The full study object.
* @returns A `StudySummary` object.
*/
function createStudySummary(study) {
return {
nctId: study.protocolSection?.identificationModule?.nctId,
title: study.protocolSection?.identificationModule?.officialTitle,
briefSummary: study.protocolSection?.descriptionModule?.briefSummary,
overallStatus: study.protocolSection?.statusModule?.overallStatus,
conditions: study.protocolSection?.conditionsModule?.conditions,
interventions: study.protocolSection?.armsInterventionsModule?.interventions?.map((i) => ({
name: i.name,
type: i.type,
})),
leadSponsor: study.protocolSection?.sponsorCollaboratorsModule?.leadSponsor?.name,
};
}
/**
* Fetches one or more clinical studies from ClinicalTrials.gov by their NCT IDs.
* Returns either complete study data or concise summaries for each.
*
* @param params - The validated input parameters, defining which studies to fetch and in what format.
* @param context - The request context for logging and tracing.
* @returns A promise resolving to an object containing an array of studies or summaries.
* @throws {McpError} Throws if a study is not found or if the API request fails.
*/
export async function getStudyLogic(params, context) {
const nctIds = Array.isArray(params.nctIds) ? params.nctIds : [params.nctIds];
const markupFormat = params.markupFormat ?? 'markdown';
logger.debug(`Executing getStudyLogic for NCT IDs: ${nctIds.join(", ")}`, {
...context,
toolInput: params,
});
const service = ClinicalTrialsGovService.getInstance();
const studies = [];
const errors = [];
const studyPromises = nctIds.map(async (nctId) => {
try {
const study = await service.fetchStudy(nctId, context, {
fields: params.fields,
markupFormat: markupFormat,
});
if (!study) {
// This case might be redundant if fetchStudy throws a 404, but it's a safe fallback.
throw new McpError(BaseErrorCode.NOT_FOUND, `Study with NCT ID '${nctId}' not found.`);
}
const cleanedStudy = cleanStudy(study);
logger.info(`Successfully fetched study ${nctId}`, { ...context });
if (params.summaryOnly) {
logger.debug(`Creating summary for study ${nctId}`, { ...context });
studies.push(createStudySummary(cleanedStudy));
}
else {
studies.push(cleanedStudy);
}
}
catch (error) {
const errorMessage = error instanceof McpError
? error.message
: "An unexpected error occurred";
logger.warning(`Failed to fetch study ${nctId}: ${errorMessage}`, {
...context,
nctId,
error,
});
errors.push({ nctId, error: errorMessage });
}
});
await Promise.all(studyPromises);
const result = { studies };
if (errors.length > 0) {
result.errors = errors;
}
return result;
}