Skip to main content
Glama

ClinicalTrials.gov MCP Server

clinicaltrials-analyze-trends.tool.ts11 kB
/** * @fileoverview Complete, declarative definition for the 'clinicaltrials_analyze_trends' tool. * Performs statistical analysis on clinical trial data by fetching matching studies and aggregating metrics. * * @module src/mcp-server/tools/definitions/clinicaltrials-analyze-trends.tool */ import type { ContentBlock } from '@modelcontextprotocol/sdk/types.js'; import { container } from 'tsyringe'; import { z } from 'zod'; import { AppConfig, 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 { JsonRpcErrorCode, McpError } from '@/types-global/errors.js'; import { logger, type RequestContext } from '@/utils/index.js'; /** --------------------------------------------------------- */ /** Programmatic tool name (must be unique). */ const TOOL_NAME = 'clinicaltrials_analyze_trends'; /** --------------------------------------------------------- */ /** Human-readable title used by UIs. */ const TOOL_TITLE = 'Analyze Clinical Trial Trends'; /** --------------------------------------------------------- */ /** * LLM-facing description of the tool. */ const TOOL_DESCRIPTION = 'Performs statistical analysis on clinical trial studies matching search criteria. Aggregates data by status, country, sponsor type, phase, year, or month. May fetch up to 5000 studies.'; /** --------------------------------------------------------- */ /** UI/behavior hints for clients. */ const TOOL_ANNOTATIONS: ToolAnnotations = { readOnlyHint: true, idempotentHint: true, openWorldHint: true, // Accesses external ClinicalTrials.gov API }; /** --------------------------------------------------------- */ /** API call delay to avoid rate limiting */ const API_CALL_DELAY_MS = 250; /** * A simple promise-based delay function. */ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // // Schemas (input and output) // -------------------------- /** * Defines the types of analysis that can be performed. */ const AnalysisTypeSchema = z.enum([ 'countByStatus', 'countByCountry', 'countBySponsorType', 'countByPhase', 'countByYear', 'countByMonth', ]); const InputSchema = z .object({ query: z .string() .optional() .describe( 'General search query for conditions, interventions, sponsors, or other terms.', ), filter: z .string() .optional() .describe( 'Advanced filter expression using the ClinicalTrials.gov filter syntax.', ), analysisType: z .union([ AnalysisTypeSchema.describe('A single analysis type to perform.'), z .array(AnalysisTypeSchema) .min(1) .describe('An array of analysis types to perform.'), ]) .describe( 'Specify one or more analysis types: countByStatus, countByCountry, countBySponsorType, countByPhase, countByYear, or countByMonth.', ), }) .describe('Input parameters for analyzing clinical trial trends.'); const AnalysisResultSchema = z .object({ analysisType: AnalysisTypeSchema.describe( 'The type of analysis performed.', ), totalStudies: z .number() .int() .describe('Total number of studies included in this analysis.'), results: z .record(z.number()) .describe( 'Aggregated counts by category (e.g., status, country, phase).', ), }) .describe('Result of a single analysis type.'); const OutputSchema = z .object({ analysis: z .array(AnalysisResultSchema) .describe('Array of analysis results, one per requested analysis type.'), }) .describe('Trend analysis results for clinical trial studies.'); type AnalyzeTrendsInput = z.infer<typeof InputSchema>; type AnalysisResult = z.infer<typeof AnalysisResultSchema>; type AnalyzeTrendsOutput = z.infer<typeof OutputSchema>; // // Pure business logic (no try/catch; throw McpError on failure) // ------------------------------------------------------------- /** * Fetches all studies for a given query, handling pagination. * Throws if the total count exceeds the configured limit. */ async function fetchAllStudies( query: string | undefined, filter: string | undefined, appContext: RequestContext, ): Promise<Study[]> { const config = container.resolve< ReturnType<typeof import('@/config/index.js').parseConfig> >(AppConfig); const provider = container.resolve<IClinicalTrialsProvider>( ClinicalTrialsProvider, ); const maxStudies = config.maxStudiesForAnalysis; logger.debug('Fetching all studies for analysis...', { ...appContext }); // First, make one call to check the total number of studies const initialResponse = await provider.listStudies( { ...(query && { query }), ...(filter && { filter }), pageSize: 1, }, appContext, ); const totalStudies = initialResponse.totalCount ?? 0; if (totalStudies > maxStudies) { throw new McpError( JsonRpcErrorCode.ValidationError, `The query returned ${totalStudies} studies, which exceeds the limit of ${maxStudies} for analysis. Please provide a more specific query.`, { totalStudies, limit: maxStudies }, ); } if (totalStudies === 0) { return []; } // If within limits, proceed to fetch all studies let allStudies: Study[] = []; let pageToken: string | undefined = undefined; let hasMore = true; while (hasMore) { const pagedStudies = await provider.listStudies( { ...(query && { query }), ...(filter && { filter }), ...(pageToken && { pageToken }), pageSize: 1000, }, appContext, ); if (pagedStudies.studies) { allStudies = allStudies.concat(pagedStudies.studies); } pageToken = pagedStudies.nextPageToken; hasMore = !!pageToken && allStudies.length < totalStudies; if (hasMore) { await delay(API_CALL_DELAY_MS); } } logger.info(`Fetched a total of ${allStudies.length} studies for analysis.`, { ...appContext, }); return allStudies; } /** * Performs a statistical analysis on a set of clinical trials matching the given criteria. */ async function analyzeTrendsLogic( input: AnalyzeTrendsInput, appContext: RequestContext, _sdkContext: SdkContext, ): Promise<AnalyzeTrendsOutput> { logger.debug('Executing analyzeTrendsLogic', { ...appContext, toolInput: input, }); const allStudies = await fetchAllStudies( input.query, input.filter, appContext, ); const analysisTypes = Array.isArray(input.analysisType) ? input.analysisType : [input.analysisType]; const finalResults: AnalysisResult[] = []; for (const type of analysisTypes) { const results: Record<string, number> = {}; for (const study of allStudies) { let key: string | undefined; switch (type) { case 'countByStatus': key = study.protocolSection?.statusModule?.overallStatus ?? 'Unknown'; break; case 'countByCountry': study.protocolSection?.contactsLocationsModule?.locations?.forEach( (loc) => { const country = loc.country ?? 'Unknown'; results[country] = (results[country] ?? 0) + 1; }, ); continue; case 'countBySponsorType': key = study.protocolSection?.sponsorCollaboratorsModule?.leadSponsor ?.class ?? 'Unknown'; break; case 'countByPhase': { const phases = study.protocolSection?.designModule?.phases ?? [ 'Unknown', ]; phases.forEach((phase) => { const phaseKey = phase ?? 'Unknown'; results[phaseKey] = (results[phaseKey] ?? 0) + 1; }); continue; } case 'countByYear': { const startDate = study.protocolSection?.statusModule?.startDateStruct?.date; if (startDate) { const year = startDate.substring(0, 4); // Extract YYYY from date results[year] = (results[year] ?? 0) + 1; } else { results['Unknown'] = (results['Unknown'] ?? 0) + 1; } continue; } case 'countByMonth': { const startDate = study.protocolSection?.statusModule?.startDateStruct?.date; if (startDate && startDate.length >= 7) { const yearMonth = startDate.substring(0, 7); // Extract YYYY-MM from date results[yearMonth] = (results[yearMonth] ?? 0) + 1; } else { results['Unknown'] = (results['Unknown'] ?? 0) + 1; } continue; } } if (key) { results[key] = (results[key] ?? 0) + 1; } } finalResults.push({ analysisType: type, totalStudies: allStudies.length, results, }); } logger.info('Successfully completed trend analysis', { ...appContext, analysisCount: finalResults.length, }); return { analysis: finalResults }; } /** * Formats a concise human-readable summary. */ function responseFormatter(result: AnalyzeTrendsOutput): ContentBlock[] { const analysisCount = result.analysis.length; const summaries = result.analysis.map((analysis) => { const topEntries = Object.entries(analysis.results) .sort((a, b) => b[1] - a[1]) .slice(0, 10); const categoryList = topEntries .map(([category, count]) => { const percentage = ((count / analysis.totalStudies) * 100).toFixed(1); return ` • ${category}: ${count} (${percentage}%)`; }) .join('\n'); const more = Object.keys(analysis.results).length > 10 ? ` ...and ${Object.keys(analysis.results).length - 10} more` : ''; return [ `Analysis: ${analysis.analysisType}`, `Total Studies: ${analysis.totalStudies}`, 'Top Categories:', categoryList, more, ] .filter(Boolean) .join('\n'); }); return [ { type: 'text', text: `Completed ${analysisCount} ${analysisCount === 1 ? 'analysis' : 'analyses'}\n\n${summaries.join('\n\n---\n\n')}`, }, ]; } /** * The complete tool definition for analyzing clinical trial trends. */ export const analyzeTrendsTool: 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'], analyzeTrendsLogic), 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