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.

97 lines 3.81 kB
/** * @fileoverview Shared formatting helpers for tool format() functions. * @module mcp-server/tools/utils/format-helpers */ /** Truncate a string, appending ellipsis when trimmed. */ function truncate(value, max) { return value.length > max ? `${value.slice(0, max)}…` : value; } /** Strip common structural suffixes and split camelCase for readability. */ function humanizeSegment(segment) { return segment .replace(/Module$/, '') .replace(/Struct$/, '') .replace(/Info$/, '') .replace(/([a-z])([A-Z])/g, '$1 $2') .replace(/^./, (c) => c.toUpperCase()); } /** Structural path segments skipped when building labels. */ const STRUCTURAL = new Set(['protocolSection', 'resultsSection', 'derivedSection']); /** Build a human-readable label from path segments. */ function labelFromPath(segments) { const meaningful = segments.filter((s) => !s.startsWith('[') && !STRUCTURAL.has(s)); return meaningful.slice(-2).map(humanizeSegment).join(' > '); } /** Recursively collect primitive leaf values from a nested object. */ function collectLeaves(obj, segments, out) { if (obj == null || obj === '') return; if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') { out.push({ segments, value: String(obj) }); return; } if (Array.isArray(obj)) { if (obj.length === 0) return; if (obj.every((v) => typeof v !== 'object' || v === null)) { out.push({ segments, value: obj.join(', ') }); return; } // No per-array cap — the outer `maxLines` budget in formatRemainingStudyFields // already bounds total output. Capping here silently hid data from callers. for (let i = 0; i < obj.length; i++) { collectLeaves(obj[i], [...segments, `[${i}]`], out); } return; } if (typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { collectLeaves(value, [...segments, key], out); } } } /** Check if a leaf's path starts with any rendered prefix. */ function isRendered(segments, renderedPrefixes) { const path = segments.filter((s) => !s.startsWith('[')).join('.'); for (const prefix of renderedPrefixes) { if (path === prefix || path.startsWith(`${prefix}.`)) return true; } return false; } /** * Render study fields not already covered by the primary formatter. * `renderedPrefixes` uses dot-notation paths matching the study structure * (e.g., `"protocolSection.identificationModule.nctId"`). * * Returns indented lines suitable for appending after a study's primary output. */ export function formatRemainingStudyFields(study, renderedPrefixes, { maxLines = 8, maxValueLen = 200 } = {}) { const leaves = []; collectLeaves(study, [], leaves); const remaining = leaves.filter((leaf) => !isRendered(leaf.segments, renderedPrefixes)); if (remaining.length === 0) return []; // Count only cap-dropped fields as "uncovered" — dedup-dropped leaves aren't // truncation, just consolidation. Pre-fix logic counted both, lying about // truncation whenever multiple array entries shared a label. const seen = new Set(); const lines = []; let dropped = 0; for (const leaf of remaining) { const label = labelFromPath(leaf.segments); if (!label || seen.has(label)) continue; if (lines.length >= maxLines) { dropped++; continue; } seen.add(label); lines.push(` ${label}: ${truncate(leaf.value, maxValueLen)}`); } if (dropped > 0) { lines.push(` … and ${dropped} more fields`); } return lines; } //# sourceMappingURL=format-helpers.js.map