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
JavaScript
/**
* @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