UNPKG

clinicaltrialsgov-mcp-server

Version:

Search ClinicalTrials.gov trials, retrieve study details and results, and match patients to eligible trials via MCP. STDIO or Streamable HTTP.

128 lines 6.53 kB
/** * @fileoverview Discover valid field values with study counts from ClinicalTrials.gov. * @module mcp-server/tools/definitions/get-field-values.tool */ import { tool, z } from '@cyanheads/mcp-ts-core'; import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors'; import { getClinicalTrialsService } from '../../../services/clinical-trials/clinical-trials-service.js'; import { toArray } from '../utils/query-helpers.js'; import { RECOVERY_HINTS } from '../utils/recovery-hints.js'; export const getFieldValues = tool('clinicaltrials_get_field_values', { description: `Discover valid values for ClinicalTrials.gov fields with study counts per value. Use to explore available filter options before building a search — e.g., valid OverallStatus, Phase, InterventionType, StudyType, or LeadSponsorClass values.`, annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true, }, errors: [ { reason: 'field_invalid', code: JsonRpcErrorCode.ValidationError, when: 'A requested field name is not a valid PascalCase piece name.', recovery: RECOVERY_HINTS.field_invalid, }, { reason: 'rate_limited', code: JsonRpcErrorCode.RateLimited, when: 'ClinicalTrials.gov returned 429 after retry budget exhausted.', recovery: RECOVERY_HINTS.rate_limited, retryable: true, }, ], input: z.object({ fields: z .union([ z.string().describe('A single PascalCase field name.'), z .array(z.string()) .min(1) .describe('Multiple PascalCase field names (at least one required).'), ]) .describe(`PascalCase field name(s) to get value statistics for. Examples: OverallStatus, Phase, StudyType, Sex, LeadSponsorClass. Use clinicaltrials_get_field_definitions with a query to find more field names.`), }), output: z.object({ fieldStats: z .array(z .object({ field: z.string().describe('Full dot-notation field path.'), piece: z.string().describe('PascalCase piece name.'), type: z.string().describe('Field data type (ENUM, BOOLEAN, STRING, DATE, etc.).'), missingStudiesCount: z .number() .optional() .describe('Number of studies where this field is absent.'), multiValued: z .boolean() .optional() .describe('True when the field is array-typed (a study can carry several values, e.g. Phase, Condition), so the per-value studiesCount buckets sum above the study total. Use to avoid computing a percentage against the corpus.'), // ENUM / STRING fields uniqueValuesCount: z.number().optional().describe('Number of distinct values.'), topValues: z .array(z .object({ value: z.string().describe('Field value.'), studiesCount: z.number().describe('Number of studies with this value.'), }) .describe('A value and its study count.')) .optional() .describe('Values ranked by frequency (capped at 250 by the API). Present for ENUM/STRING fields. When multiValued is true, studiesCount sums can exceed the study total.'), // BOOLEAN fields trueCount: z .number() .optional() .describe('Studies where field is true. Present for BOOLEAN fields.'), falseCount: z .number() .optional() .describe('Studies where field is false. Present for BOOLEAN fields.'), }) .describe('Statistics for a single requested field.')) .describe('One entry per requested field: canonical path, PascalCase piece name, data type, missing/unique counts, and top values with study counts (or trueCount/falseCount for BOOLEAN fields).'), }), async handler(input, ctx) { const fields = toArray(input.fields); if (fields.length === 0) { throw ctx.fail('field_invalid', 'Provide at least one PascalCase field name (e.g. "OverallStatus").', { ...ctx.recoveryFor('field_invalid') }); } const service = getClinicalTrialsService(); const stats = await service.getFieldValues(fields, ctx); ctx.log.info('Field values fetched', { fieldCount: stats.length }); return { fieldStats: stats }; }, format: (result) => { const lines = []; for (const stat of result.fieldStats) { const header = stat.type === 'BOOLEAN' ? `**${stat.piece}** — ${stat.field} (boolean):` : `**${stat.piece}** — ${stat.field} (${stat.type}, ${stat.uniqueValuesCount ?? '?'} unique values):`; lines.push(header); if (stat.trueCount != null) lines.push(` true: ${stat.trueCount} studies`); if (stat.falseCount != null) lines.push(` false: ${stat.falseCount} studies`); const topValues = stat.topValues ?? []; if (stat.type !== 'BOOLEAN') { if (topValues.length === 0) { lines.push(' No recorded values for this field.'); } else { const shown = topValues.slice(0, 15); for (const tv of shown) { lines.push(` ${tv.value}: ${tv.studiesCount} studies`); } if (topValues.length > shown.length) { const remainder = topValues.length - shown.length; const unique = stat.uniqueValuesCount; lines.push(` … and ${remainder} more${unique != null ? ` (of ${unique} unique; topValues capped at 250)` : ''}`); } } } if (stat.missingStudiesCount != null && stat.missingStudiesCount > 0) lines.push(` (missing in ${stat.missingStudiesCount} studies)`); if (stat.multiValued) lines.push(' (multi-valued field — counts may exceed the study total; a study can carry several values)'); } return [{ type: 'text', text: lines.join('\n') }]; }, }); //# sourceMappingURL=get-field-values.tool.js.map