find-series
Searches DICOM series within a study using a Study Instance UID and optional filters. Returns series sorted by date, newest first, without retrieving image data.
Instructions
Searches DICOM series within a single study. Returns series sorted by series date, newest first. Does not retrieve instances or image data. Requires a Study Instance UID from find-studies.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| studyInstanceUid | Yes | DICOM Study Instance UID (e.g., 1.2.840.113619.2.55.3). Obtain from find-studies. | |
| query | Yes | Space-separated DICOM key=value search filters. Keys are DICOM keyword names or 8-digit hex tags (e.g., Modality=MR SeriesDescription=CHEST). Special keys: limit=N, offset=N. Pass an empty string to return all series. |
Implementation Reference
- src/tools/searchSeries.js:24-61 (handler)Core handler function that performs the DICOMweb QIDO-RS query for series, given a StudyInstanceUID and query string. Builds URL parameters, executes the HTTP request, maps DICOM JSON to camelCase objects, and sorts results by series date/time descending.
export async function searchSeries(studyInstanceUid, queryString, env = process.env) { // Build URL parameters and headers from the query string and environment variables const { urlParams, headers } = buildQuery(queryString, env); // Perform the HTTP request to the DICOMweb server's QIDO endpoint const res = await makeQuery( urlJoin( env.DICOMWEB_HOST, `/studies/${encodeURIComponent(studyInstanceUid)}/series?${urlParams}` ), { headers, signal: buildSignal(env), } ); if (!res.ok) { throw new Error( `Search series request failed with HTTP status ${res.status} [uri: ${scrubUrl(res.url)}]` ); } // Parse the JSON response const items = await res.json(); if (!items || !Array.isArray(items) || items.length === 0) { return []; } // Map DICOM JSON items const series = items .map(mapDicomItem) .sort((a, b) => `${b.seriesDate ?? ''}${b.seriesTime ?? ''}`.localeCompare( `${a.seriesDate ?? ''}${a.seriesTime ?? ''}` ) ); return series; } - src/index.js:115-158 (registration)MCP tool registration for 'find-series' with schema (studyInstanceUid, query) and handler that delegates to searchSeries() and formats results.
server.tool( 'find-series', 'Searches DICOM series within a single study. Returns series sorted by series date, newest first. Does not retrieve instances or image data. Requires a Study Instance UID from find-studies.', { studyInstanceUid: studyUidSchema, query: z .string() .max(1000, 'Query string must not exceed 1000 characters') .describe( 'Space-separated DICOM key=value search filters. Keys are DICOM keyword names or 8-digit hex tags (e.g., Modality=MR SeriesDescription=CHEST). Special keys: limit=N, offset=N. Pass an empty string to return all series.' ), }, async ({ studyInstanceUid, query }) => { let textResult = 'No series found matching the search criteria.'; try { // Log the search criteria server.sendLoggingMessage({ level: 'info', data: `Searching series for studyInstanceUid: ${studyInstanceUid} with query: ${query}`, }); // Perform the search using the provided parameters const series = await searchSeries(studyInstanceUid, query, process.env); // Log the search results server.sendLoggingMessage({ level: 'info', data: `Found ${series.length} series matching the search criteria.`, }); // Format the search results into a human-readable text format if (series.length > 0) { textResult = formatResults(series, 'Series'); } } catch (error) { const err = `Error searching series: ${error.message}`; server.sendLoggingMessage({ level: 'error', data: err }); return errorContent(err); } return textContent(textResult); } ); - src/index.js:118-125 (schema)Zod schema for find-series parameters: studyInstanceUid (validated DICOM UID) and query (max 1000 chars freeform string).
{ studyInstanceUid: studyUidSchema, query: z .string() .max(1000, 'Query string must not exceed 1000 characters') .describe( 'Space-separated DICOM key=value search filters. Keys are DICOM keyword names or 8-digit hex tags (e.g., Modality=MR SeriesDescription=CHEST). Special keys: limit=N, offset=N. Pass an empty string to return all series.' ), - src/utils/query.js:52-75 (helper)Builds QIDO-RS URL parameters and auth headers from a freeform query string. Used by searchSeries to construct the DICOMweb request.
export function buildQuery(queryString, env = process.env) { const parsed = parseQuery(queryString); const urlParams = new URLSearchParams(); // Attribute filters for (const attr of parsed.queryAttributes.values()) { // Prefer the keyword name when available (more readable URLs); fall back to hex tag urlParams.set(attr.name ?? attr.tag, attr.rawValue); } // Standard QIDO parameters if (parsed.fuzzyMatching) { urlParams.set('fuzzymatching', 'true'); } urlParams.set('limit', String(parsed.limit)); if (parsed.offset > 0) { urlParams.set('offset', String(parsed.offset)); } if (parsed.includeFields.length > 0) { urlParams.set('includefield', parsed.includeFields.join(',')); } return { urlParams, headers: buildAuthHeaders(env), errors: parsed.errors }; } - src/utils/queryParser.js:91-163 (helper)Parses freeform DICOM query strings (space-separated key=value pairs) into structured query parameters. Handles DICOM keywords, hex tags, includefield, fuzzymatching, limit, offset.
export function parseQuery(queryString) { const result = { queryAttributes: new Map(), includeFields: [], fuzzyMatching: false, limit: 50, offset: 0, errors: [], }; if (!queryString?.trim()) { return result; } // Split on whitespace, respecting single- and double-quoted values const tokens = queryString.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []; for (const token of tokens) { const eqIdx = token.indexOf('='); if (eqIdx === -1) { result.errors.push(`Token '${token}' has no '=', skipping`); continue; } const rawKey = token.substring(0, eqIdx).trim(); const rawValue = token.substring(eqIdx + 1).replace(/^['"]|['"]$/g, ''); const keyLower = rawKey.toLowerCase(); if (keyLower === 'includefield') { for (const part of rawValue.split(',')) { const info = lookupTag(part.trim()); if (!info) { result.errors.push( `includefield: unknown DICOM keyword or tag '${part.trim()}', skipping` ); } else { result.includeFields.push(info.tag); } } } else if (keyLower === 'fuzzymatching') { result.fuzzyMatching = rawValue.toLowerCase() !== 'false'; } else if (keyLower === 'limit') { const n = parseInt(rawValue, 10); if (!isNaN(n)) { result.limit = n; } else { result.errors.push(`limit: '${rawValue}' is not a valid integer, skipping`); } } else if (keyLower === 'offset') { const n = parseInt(rawValue, 10); if (!isNaN(n)) { result.offset = n; } else { result.errors.push(`offset: '${rawValue}' is not a valid integer, skipping`); } } else { const info = lookupTag(rawKey); if (!info) { result.errors.push(`Unknown DICOM keyword or tag '${rawKey}', skipping`); continue; } // Last write wins for duplicate keys (matches DICOMweb query semantics) result.queryAttributes.set(info.tag, { tag: info.tag, name: info.name, rawKey, rawValue, }); } } return result; }