Skip to main content
Glama
PantelisGeorgiadis

DICOMweb MCP Server

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

TableJSON Schema
NameRequiredDescriptionDefault
studyInstanceUidYesDICOM Study Instance UID (e.g., 1.2.840.113619.2.55.3). Obtain from find-studies.
queryYesSpace-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

  • 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);
      }
    );
  • 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.'
        ),
  • 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 };
    }
  • 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;
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations exist, so the description carries the full burden. It discloses sorting order, scope (single study), and non-retrieval of image data. However, it does not mention pagination, error behavior, or rate limits, which are gaps for a query tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, each adding distinct information: purpose, sorting order, and constraint. No redundancy, and front-loaded with the main action.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given two simple parameters and no output schema, the description covers usage constraints, input provenance (from find-studies), and what not to expect. It is adequate but could mention output structure (e.g., list of series UIDs).

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, and the description adds meaning by explaining the query parameter's special keys (limit, offset) and the empty string behavior. This provides value beyond the schema's basic descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it searches DICOM series within a single study, returns sorted results, and specifies it does not retrieve instances or image data. This distinguishes it from siblings like find-studies and find-instances.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly requires a Study Instance UID from find-studies, implying the usage context after that tool. It also clarifies it does not retrieve instances or images, guiding the agent to other tools for those tasks, though it does not name them explicitly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PantelisGeorgiadis/dicomweb-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server