Skip to main content
Glama

ClinicalTrials.gov MCP Server

clinicaltrials-find-eligible-studies.tool.ts•14.8 kB
/** * @fileoverview Complete, declarative definition for the 'clinicaltrials_find_eligible_studies' tool. * Matches patient demographics and medical profiles to eligible clinical trials. * * @module src/mcp-server/tools/definitions/clinicaltrials-find-eligible-studies.tool */ import type { ContentBlock } from '@modelcontextprotocol/sdk/types.js'; import { container } from 'tsyringe'; import { z } from 'zod'; import { ClinicalTrialsProvider } from '@/container/tokens.js'; import type { SdkContext, ToolAnnotations, ToolDefinition, } from '@/mcp-server/tools/utils/toolDefinition.js'; import { withToolAuth } from '@/mcp-server/transports/auth/lib/withAuth.js'; import type { IClinicalTrialsProvider } from '@/services/clinical-trials-gov/core/IClinicalTrialsProvider.js'; import type { Study } from '@/services/clinical-trials-gov/types.js'; import { logger, type RequestContext } from '@/utils/index.js'; import { checkAgeEligibility } from '../utils/ageParser.js'; import { checkHealthyVolunteerEligibility, checkSexEligibility, } from '../utils/eligibilityCheckers.js'; import { extractContactInfo, extractRelevantLocations, extractStudyDetails, } from '../utils/studyExtractors.js'; import { calculateMatchScore, rankStudies } from '../utils/studyRanking.js'; /** --------------------------------------------------------- */ /** Programmatic tool name (must be unique). */ const TOOL_NAME = 'clinicaltrials_find_eligible_studies'; /** --------------------------------------------------------- */ /** Human-readable title used by UIs. */ const TOOL_TITLE = 'Find Eligible Clinical Trials'; /** --------------------------------------------------------- */ /** * LLM-facing description of the tool. */ const TOOL_DESCRIPTION = 'Matches patient demographics and medical profile to eligible clinical trials. Filters by age, sex, conditions, location, and healthy volunteer status. Returns ranked list of matching studies with eligibility explanations.'; /** --------------------------------------------------------- */ /** UI/behavior hints for clients. */ const TOOL_ANNOTATIONS: ToolAnnotations = { readOnlyHint: true, idempotentHint: true, openWorldHint: true, // Accesses external ClinicalTrials.gov API }; /** --------------------------------------------------------- */ // // Schemas (input and output) // -------------------------- const PatientLocationSchema = z .object({ country: z.string().describe('Country (e.g., "United States")'), state: z.string().optional().describe('State or province'), city: z.string().optional().describe('City'), postalCode: z.string().optional().describe('Postal code'), }) .describe('Patient location for geographic filtering.'); const InputSchema = z .object({ age: z.number().int().min(0).max(120).describe('Patient age in years.'), sex: z .enum(['All', 'Female', 'Male']) .describe('Biological sex of the patient.'), conditions: z .array(z.string()) .min(1) .describe( 'List of medical conditions or diagnoses (e.g., ["Type 2 Diabetes", "Hypertension"]).', ), location: PatientLocationSchema, healthyVolunteer: z .boolean() .default(false) .describe('Whether the patient is a healthy volunteer.'), maxResults: z .number() .int() .min(1) .max(50) .default(10) .describe('Maximum number of matching studies to return.'), recruitingOnly: z .boolean() .default(true) .describe('Only include actively recruiting studies.'), }) .describe('Input parameters for finding eligible clinical trial studies.'); const EligibilityHighlightsSchema = z .object({ ageRange: z.string().optional().describe('Age range for the study'), sex: z.string().optional().describe('Sex requirement'), healthyVolunteers: z .boolean() .optional() .describe('Whether healthy volunteers are accepted'), criteriaSnippet: z .string() .optional() .describe('Excerpt from eligibility criteria'), }) .describe('Eligibility highlights for the study.'); const StudyLocationSchema = z .object({ facility: z.string().optional().describe('Facility name'), city: z.string().optional().describe('City'), state: z.string().optional().describe('State or province'), country: z.string().optional().describe('Country'), distance: z.number().optional().describe('Distance in miles'), }) .describe('Study location information.'); const StudyContactSchema = z .object({ name: z.string().optional().describe('Contact person name'), phone: z.string().optional().describe('Contact phone number'), email: z.string().optional().describe('Contact email address'), }) .describe('Study contact information.'); const StudyDetailsSchema = z .object({ phase: z.array(z.string()).optional().describe('Trial phases'), status: z.string().describe('Overall study status'), enrollmentCount: z.number().optional().describe('Planned enrollment count'), sponsor: z.string().optional().describe('Lead sponsor name'), }) .describe('Study details for ranking and display.'); const EligibleStudySchema = z .object({ nctId: z.string().describe('The NCT identifier'), title: z.string().describe('Study title'), briefSummary: z.string().optional().describe('Brief study summary'), matchScore: z .number() .min(0) .max(100) .describe('Confidence score (0-100) for eligibility match'), matchReasons: z .array(z.string()) .describe( 'Reasons why this study matches (e.g., "Age within range", "Accepts females")', ), eligibilityHighlights: EligibilityHighlightsSchema, locations: z .array(StudyLocationSchema) .describe('Relevant study locations'), contact: StudyContactSchema.optional().describe( 'Study contact information', ), studyDetails: StudyDetailsSchema, }) .describe('An eligible clinical trial study with match details.'); const OutputSchema = z .object({ eligibleStudies: z .array(EligibleStudySchema) .describe('Array of eligible studies, ranked by relevance'), totalMatches: z.number().describe('Total number of eligible studies found'), searchCriteria: z .object({ conditions: z.array(z.string()).describe('Searched conditions'), location: z.string().describe('Patient location summary'), ageRange: z.string().describe('Patient demographic summary'), }) .describe('Summary of search criteria used'), }) .describe('Response containing eligible clinical trial studies.'); type FindEligibleStudiesInput = z.infer<typeof InputSchema>; type EligibleStudy = z.infer<typeof EligibleStudySchema>; type FindEligibleStudiesOutput = z.infer<typeof OutputSchema>; // // Helper functions // -------------------------- /** * Filters studies based on eligibility criteria. */ function filterByEligibility( studies: Study[], input: FindEligibleStudiesInput, appContext: RequestContext, ): EligibleStudy[] { const eligible: EligibleStudy[] = []; for (const study of studies) { const eligibility = study.protocolSection?.eligibilityModule; if (!eligibility) { logger.debug('Skipping study without eligibility module', { ...appContext, nctId: study.protocolSection?.identificationModule?.nctId, }); continue; } const matchReasons: string[] = []; const eligibilityChecks: Array<{ eligible: boolean; reason: string }> = []; // Age Check const ageCheck = checkAgeEligibility( eligibility.minimumAge, (eligibility as { maximumAge?: string }).maximumAge, input.age, ); eligibilityChecks.push(ageCheck); if (!ageCheck.eligible) continue; matchReasons.push(ageCheck.reason); // Sex Check const sexCheck = checkSexEligibility(eligibility.sex, input.sex); eligibilityChecks.push(sexCheck); if (!sexCheck.eligible) continue; matchReasons.push(sexCheck.reason); // Healthy Volunteers Check const hvCheck = checkHealthyVolunteerEligibility( eligibility.healthyVolunteers, input.healthyVolunteer, ); eligibilityChecks.push(hvCheck); if (!hvCheck.eligible) continue; matchReasons.push(hvCheck.reason); // Calculate match score based on checks passed const matchScore = calculateMatchScore(eligibilityChecks); // Extract study information const nctId = study.protocolSection?.identificationModule?.nctId ?? 'Unknown'; const title = study.protocolSection?.identificationModule?.briefTitle ?? 'No title'; const briefSummary = study.protocolSection?.descriptionModule?.briefSummary; const locations = extractRelevantLocations(study, input.location); // If no locations match the patient's location, skip this study if (locations.length === 0) { continue; } const contact = extractContactInfo(study); const studyDetails = extractStudyDetails(study); eligible.push({ nctId, title, briefSummary, matchScore, matchReasons, eligibilityHighlights: { ageRange: `${eligibility.minimumAge ?? 'N/A'} - ${(eligibility as { maximumAge?: string }).maximumAge ?? 'N/A'}`, sex: eligibility.sex ?? 'All', healthyVolunteers: eligibility.healthyVolunteers, criteriaSnippet: eligibility.eligibilityCriteria?.substring(0, 300), }, locations, contact, studyDetails, }); } return eligible; } // // Pure business logic (no try/catch; throw McpError on failure) // ------------------------------------------------------------- /** * Finds clinical studies that match a patient's eligibility profile. */ async function findEligibleStudiesLogic( input: FindEligibleStudiesInput, appContext: RequestContext, _sdkContext: SdkContext, ): Promise<FindEligibleStudiesOutput> { logger.debug('Executing findEligibleStudiesLogic', { ...appContext, toolInput: input, }); const provider = container.resolve<IClinicalTrialsProvider>( ClinicalTrialsProvider, ); // Build search query const conditionQuery = input.conditions.join(' OR '); // Build filter for recruiting status const filter = input.recruitingOnly ? 'STATUS:Recruiting OR STATUS:"Not yet recruiting"' : undefined; // Fetch initial studies const searchParams = { query: conditionQuery, ...(filter ? { filter } : {}), pageSize: 100, // Fetch more for filtering }; logger.info('Searching for studies with criteria', { ...appContext, searchParams, }); const pagedStudies = await provider.listStudies(searchParams, appContext); logger.info(`Found ${pagedStudies.studies?.length ?? 0} studies to filter`, { ...appContext, totalCount: pagedStudies.totalCount, }); // Filter by eligibility const eligibleStudies = filterByEligibility( pagedStudies.studies ?? [], input, appContext, ); logger.info(`${eligibleStudies.length} studies passed eligibility checks`, { ...appContext, }); // Rank by relevance const rankedStudies = rankStudies(eligibleStudies); // Limit results const finalStudies = rankedStudies.slice(0, input.maxResults); logger.info( `Returning ${finalStudies.length} eligible studies (top ${input.maxResults})`, { ...appContext, totalEligible: eligibleStudies.length, }, ); return { eligibleStudies: finalStudies, totalMatches: eligibleStudies.length, searchCriteria: { conditions: input.conditions, location: input.location.city ?? input.location.state ?? input.location.country, ageRange: `${input.age} years old, ${input.sex}`, }, }; } /** * Formats the eligible studies as markdown with summaries and details. */ function responseFormatter(result: FindEligibleStudiesOutput): ContentBlock[] { const { eligibleStudies, totalMatches, searchCriteria } = result; const summary = [ `# Eligible Clinical Trials`, ``, `Found **${totalMatches}** matching studies for:`, `- **Conditions:** ${searchCriteria.conditions.join(', ')}`, `- **Location:** ${searchCriteria.location}`, `- **Patient:** ${searchCriteria.ageRange}`, ``, `Showing top ${eligibleStudies.length} ${eligibleStudies.length === 1 ? 'result' : 'results'}:`, ``, `---`, ``, ]; const studyDetails = eligibleStudies.map((study, idx) => { const locationList = study.locations .slice(0, 3) .map( (loc) => `- ${loc.facility ?? 'Unknown facility'} - ${loc.city ?? 'N/A'}, ${loc.state ?? 'N/A'}${loc.distance ? ` (${loc.distance} mi)` : ''}`, ) .join('\n'); const moreLocations = study.locations.length > 3 ? `- ...and ${study.locations.length - 3} more locations` : ''; return [ `## ${idx + 1}. ${study.title}`, `**NCT ID:** ${study.nctId}`, ``, `**Match Score:** ${study.matchScore}/100`, ``, `**Why You Match:**`, ...study.matchReasons.map((r) => `- ${r}`), ``, `**Eligibility Summary:**`, `- Age Range: ${study.eligibilityHighlights.ageRange}`, `- Sex: ${study.eligibilityHighlights.sex}`, `- Healthy Volunteers: ${study.eligibilityHighlights.healthyVolunteers ? 'Yes' : 'No'}`, ``, study.briefSummary ? `**Study Summary:**\n${study.briefSummary}\n` : '', `**Study Details:**`, `- Phase: ${study.studyDetails.phase?.join(', ') ?? 'N/A'}`, `- Status: ${study.studyDetails.status}`, `- Sponsor: ${study.studyDetails.sponsor ?? 'N/A'}`, study.studyDetails.enrollmentCount ? `- Target Enrollment: ${study.studyDetails.enrollmentCount}` : '', ``, `**Nearby Locations (${study.locations.length}):**`, locationList, moreLocations, ``, study.contact ? `**Contact:** ${study.contact.name ?? 'N/A'}${study.contact.phone ? ` | ${study.contact.phone}` : ''}${study.contact.email ? ` | ${study.contact.email}` : ''}` : '', ``, `---`, ``, ] .filter(Boolean) .join('\n'); }); return [ { type: 'text', text: summary.join('\n') + studyDetails.join(''), }, ]; } /** * The complete tool definition for finding eligible clinical trial studies. */ export const findEligibleStudiesTool: ToolDefinition< typeof InputSchema, typeof OutputSchema > = { name: TOOL_NAME, title: TOOL_TITLE, description: TOOL_DESCRIPTION, inputSchema: InputSchema, outputSchema: OutputSchema, annotations: TOOL_ANNOTATIONS, logic: withToolAuth(['tool:clinicaltrials:read'], findEligibleStudiesLogic), responseFormatter, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/clinicaltrialsgov-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server