/** * @fileoverview Shared helpers for normalizing tool inputs into API search parameters. * @module mcp-server/tools/utils/query-helpers */ /** * Normalize `string | string[]` to `string[]`. * * LLM callers intermittently serialize array arguments as JSON strings * (`'["RECRUITING","COMPLETED"]'` rather than `["RECRUITING","COMPLETED"]`). A * `[`-leading string that `JSON.parse`s to an all-string array is unwrapped to * that array. Every legitimate scalar this normalizes (status/phase enums, NCT * IDs, PascalCase field names) starts with a letter, so a `[`-leading string is * always either a stringified array or garbage — and garbage falls through to * the scalar-wrap, preserving the prior behavior. */ export declare function toArray(v: string | string[]): string[]; export declare function toArray(v: string | string[] | undefined): string[] | undefined; /** * Normalize a `statusFilter` to upstream's canonical `OverallStatus` spelling. * * `filter.overallStatus` is matched case-sensitively upstream — `recruiting` * and `Recruiting` both 400 while `RECRUITING` succeeds — so each entry is * trimmed, uppercased, and every run of whitespace, hyphens, and underscores * collapsed to one `_`. No two statuses differ only in case or separators, so * the mapping is one-to-one. Whitespace around a `,` or `|` list delimiter * (both of which upstream splits on) is dropped first, so `recruiting, completed` * becomes `RECRUITING,COMPLETED` rather than a `_COMPLETED` token the caller * never wrote. The two registry display labels that differ from their API value * by more than case and separators, `Active, not recruiting` (its comma would * split it) and `Unknown status`, are mapped to `ACTIVE_NOT_RECRUITING` and * `UNKNOWN`; neither `ACTIVE`, `NOT_RECRUITING`, nor `UNKNOWN_STATUS` is a * status, so the mapping stays one-to-one. A value that still names no status * reaches upstream in its normalized form and fails there with `enum_invalid`, * the same contract and recovery hint an unknown canonical value gets. A blank * entry becomes `''`, which `firstBlankListParam` still rejects. */ export declare function normalizeStatusFilter(v: string | string[] | undefined): string[] | undefined; /** * Split each list entry on the `,` and `|` delimiters ClinicalTrials.gov itself * uses for multi-value parameters, trim the pieces, and drop the empty ones — * `["NCTId,BriefTitle"]` becomes `["NCTId", "BriefTitle"]` and `["NCTId,"]` * becomes `["NCTId"]`. No field name or phase value contains either delimiter, * so the split is one-to-one. A piece of only format characters counts as * empty. A repeated piece is dropped after its first occurrence: it changes * nothing but the URL length, and a long enough repeat runs the request past * the length upstream accepts. An entry that splits to nothing (`""`, `" "`, `","`) * becomes `''` rather than vanishing, so `firstBlankListParam` still rejects it * instead of the list silently shrinking — or, for `[","]`, emptying and * dropping the parameter from the request. */ export declare function splitListEntries(values: string[]): string[]; export declare function splitListEntries(values: string[] | undefined): string[] | undefined; /** The six `Phase` values ClinicalTrials.gov accepts, in registry order. */ export declare const PHASE_VALUES: readonly ['NA', 'EARLY_PHASE1', 'PHASE1', 'PHASE2', 'PHASE3', 'PHASE4']; /** * Resolve a `phaseFilter` to canonical phase values. * * Each entry is split on `,` and `|` (`splitListEntries`), then matched with * case, whitespace, `-`, `_`, and quotes ignored, so every single-value * spelling upstream accepts (`phase3`, `Phase 3`, `"PHASE3"`, `Not Applicable`, * `Early Phase 1`) resolves to the one value it names. A piece that resolves to * no phase is returned as the whole entry it came from (trimmed) for * `firstInvalidPhase` to name — `RANGE[PHASE1,PHASE3]` is named as written, not * as the fragment `RANGE[PHASE1`. Entries are interpolated into * `AREA[Phase]`, so one such as `PHASE3 OR AREA[StudyType]OBSERVATIONAL` * must never reach upstream, which would answer it as an expression widening * the cohort past the phase constraint. Quotes are ignored in matching, so a * piece of only quotes, whitespace, and format characters carries no content: * it is dropped like an empty piece, and an entry left with nothing stays `''` * for `firstBlankListParam`. */ export declare function normalizePhaseFilter(v: string | string[] | undefined): string[] | undefined; /** The first entry of a normalized `phaseFilter` that is not a canonical phase. */ export declare function firstInvalidPhase(phases: string[] | undefined): string | undefined; /** * Message, recovery hint, and `literals` for a `phaseFilter` entry that names * no phase. The literals are what the caller sends back — the six values and * the list form — so content[] renders each as a code span rather than * Markdown-escaped text (`\["PHASE2","PHASE3"\]`). */ export declare function invalidPhaseRejection(value: string): { hint: string; literals: string[]; message: string; }; /** * Name the first supplied-but-blank string parameter, or `undefined` when every * supplied value carries non-whitespace. Omitted parameters are skipped — * omission keeps its documented meaning; only a value the caller actually * supplied is judged. Trimming, not a length check: `' '` is as empty as `''`, * and the upstream query builder drops both, silently widening the request to * the whole registry instead of constraining it. */ export declare function firstBlankParam(params: Record): string | undefined; /** * Name the first supplied list parameter that is empty or carries a blank * entry. Pass lists already normalized through `toArray`, so the stringified * `'[]'` form LLM callers sometimes send is judged on what it resolves to. */ export declare function firstBlankListParam(params: Record): string | undefined; /** * Actionable message for a parameter supplied with a blank value. Leads with * the fix that works on a required parameter too — omitting one of those fails * the schema and returns a bare -32602 with no reason and no recovery hint. */ export declare function blankValueMessage(param: string): string; /** * Build the AREA[] phase filter and combine it with the caller's advancedFilter. * * The caller's expression is parenthesized as a unit before the AND join. Essie * has no precedence rule that scopes a trailing `OR` back under a preceding * `AND`, so an ungrouped operand let the OR branch escape the phase constraint * entirely — a PHASE3 search answered with phase-less observational studies. * The wrap is unconditional: Essie tolerates redundant nested parentheses, and * deciding whether a caller's own leading `(` closes at the end of the * expression or midway through it would mean reimplementing the parser. * * An advancedFilter with no phase expression alongside it is returned untouched * — there is no AND boundary for its meaning to leak past. */ export declare function buildAdvancedFilter(phaseFilter?: string[], advancedFilter?: string): string | undefined; /** * Render one structured term as an upstream query operand: quoted when it * carries whitespace so it matches as a literal phrase, bare otherwise. * * An embedded `"` is stripped rather than escaped. Upstream Essie has no * working escape for a literal quote inside a quoted phrase and does not fail * on one — an unescaped `"` silently reparses into a different query that * returns a plausible-looking wrong result set, and a backslash-escaped one * matches nothing. Removing the character is the only handling that keeps the * phrase intact. */ export declare function quoteQueryTerm(term: string): string; /** * Render a location search term (the city) as an operand inside * `SEARCH[Location](…)`: bare when it is a single run of letters and digits, * otherwise quoted as `quoteLocationLiteral` quotes it. Inside the wrapper a * bare part carrying punctuation under-matches — `Winston-Salem` finds a small * fraction of the studies its quoted form finds, every one of which lists a * Winston-Salem site — and a bare `(`, `)`, `[`, or `]` is query syntax that * would close the wrapper and regroup the expression around the rest of the * part. An embedded `"` is stripped before deciding. */ export declare function quoteLocationPart(part: string): string; /** * Render a location value as one quoted literal, the form an * `AREA[LocationState]` or `AREA[LocationCountry]` operand always takes: left * bare there, a single word is read as syntax — `Missing` as the `MISSING` * operator. An embedded `"` is stripped, as in `quoteQueryTerm`. A backslash is * doubled: a single one escapes the closing quote, leaving the phrase open so a * later part's `)` regroups the expression, while upstream reads a doubled one * inside a phrase as a literal backslash. */ export declare function quoteLocationLiteral(value: string): string; /** * What a term contributes to the query `quoteQueryTerm`, `quoteLocationPart`, * or `quoteLocationLiteral` builds: its text with every `"` and every invisible * format character stripped, trimmed. Empty means the term carries no content — * it reaches upstream as no operand at all, as a bare `" "` phrase, or as a * zero-width term upstream answers with a bare `Search error` — so judge * blankness on this, not on the raw value. */ export declare function queryTermContent(term: string): string; /** * Report normalized filter values in the shape the caller sent them: a list * stays a list, and a lone scalar comes back as a scalar. */ export declare function inCallerShape(raw: string | string[] | undefined, normalized: string[]): string | string[] | undefined; //# sourceMappingURL=query-helpers.d.ts.map