llm-tools/mcps/dicom_mcp/CLAUDE.md
Gregory Gauthier 83ec950df7 first commit
2026-04-08 12:11:04 +01:00

9.2 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

A Model Context Protocol (MCP) server that provides DICOM medical imaging QA tools to Claude. Built with FastMCP and pydicom, it exposes 17 read-only async tools for analyzing DICOM files, with a focus on body composition analysis, Dixon sequence validation, pixel analysis, Philips private tag resolution, UID comparison, segmentation verification, and T1 mapping (MOLLI/NOLLI) analysis.

Commands

# Install dependencies
poetry install --with dev

# Run all tests
poetry run pytest -v --tb=short

# Run a single test class
poetry run pytest test_dicom_mcp.py::TestToolRegistration -v

# Run a single test
poetry run pytest test_dicom_mcp.py::TestToolRegistration::test_all_tools_registered -v

# Run the MCP server directly
poetry run python -m dicom_mcp

# Run with PII filtering enabled
DICOM_MCP_PII_FILTER=true poetry run python -m dicom_mcp

Architecture

The project is structured as a Python package (dicom_mcp/) with a backward-compatible shim at the root (dicom_mcp.py). All tests live in test_dicom_mcp.py.

Package Structure

dicom_mcp/
    __init__.py         # Re-exports all public symbols for backward compat
    __main__.py         # Entry point: python -m dicom_mcp
    server.py           # mcp = FastMCP("dicom_mcp") + run()
    config.py           # Env-var-driven config (PII_FILTER_ENABLED, MAX_FILES)
    constants.py        # Enums (ResponseFormat, SequenceType), COMMON_TAGS, VALID_TAG_GROUPS
    pii.py              # PII tag set + redaction functions
    helpers/
        __init__.py     # Re-exports all helper functions
        tags.py         # _safe_get_tag, _format_tag_value, _resolve_tag, _validate_tag_groups, _format_markdown_table
        sequence.py     # _identify_sequence_type, _is_dixon_sequence, _get_dixon_image_types
        files.py        # _is_dicom_file, _find_dicom_files
        philips.py      # _resolve_philips_private_tag, _list_philips_private_creators
        pixels.py       # _get_pixel_array, _extract_roi, _compute_stats, _apply_windowing
        filters.py      # _parse_filter, _apply_filter
        tree.py         # _build_tree_text, _build_tree_json, _format_tree_value
    tools/
        __init__.py     # Imports all tool modules to trigger @mcp.tool() registration
        discovery.py    # dicom_list_files, dicom_find_dixon_series
        metadata.py     # dicom_get_metadata, dicom_compare_headers
        query.py        # dicom_query, dicom_summarize_directory
        validation.py   # dicom_validate_sequence, dicom_analyze_series
        search.py       # dicom_search
        philips.py      # dicom_query_philips_private
        pixels.py       # dicom_read_pixels, dicom_compute_snr, dicom_render_image
        tree.py         # dicom_dump_tree
        uid_comparison.py # dicom_compare_uids
        segmentation.py # dicom_verify_segmentations
        ti_analysis.py  # dicom_analyze_ti
dicom_mcp.py            # Thin shim so `python dicom_mcp.py` still works

Import Chain (No Circular Dependencies)

  • config.py and constants.py are leaf modules (no internal imports)
  • server.py only imports FastMCP externally — owns the mcp instance
  • helpers/*.py import from constants.py, config.py, pydicom/numpy (never server.py)
  • tools/*.py import mcp from server.py, plus helpers and constants
  • tools/__init__.py imports all tool modules (triggers @mcp.tool() registration)
  • server.run() does a deferred import of dicom_mcp.tools before starting
  • __init__.py re-exports everything so import dicom_mcp still works

Tool structure: Each tool is an async function that takes a directory/file path, performs DICOM analysis using pydicom, and returns formatted results (markdown or JSON). All tools are read-only and annotated with ToolAnnotations(readOnlyHint=True). All blocking I/O is wrapped in asyncio.to_thread() to prevent event loop blocking.

The 17 tools:

  • dicom_list_files — Recursively find DICOM files, optionally filtered by sequence type; supports count_only mode
  • dicom_get_metadata — Extract DICOM header info from a single file using tag groups; supports Philips private tags via philips_private_tags parameter
  • dicom_compare_headers — Compare headers across 2-10 files side-by-side
  • dicom_find_dixon_series — Identify Dixon sequences and detect image types (water/fat/in-phase/out-phase)
  • dicom_validate_sequence — Validate sequence parameters (TR, TE, flip angle) against expected values
  • dicom_analyze_series — Comprehensive series analysis checking parameter consistency and completeness
  • dicom_summarize_directory — High-level overview of directory contents with field-level summaries
  • dicom_query — Query arbitrary DICOM tags across a directory with optional grouping
  • dicom_search — Search DICOM files using filter syntax (text, numeric, presence operators)
  • dicom_query_philips_private — Query Philips private DICOM tags by DD number and element offset; can list all Private Creator tags or resolve specific private elements
  • dicom_read_pixels — Extract pixel statistics with optional ROI and histogram
  • dicom_compute_snr — Compute signal-to-noise ratio from two ROIs
  • dicom_render_image — Render DICOM to PNG with windowing and ROI overlays
  • dicom_dump_tree — Full hierarchical dump of DICOM structure including nested sequences; configurable depth and private tag visibility
  • dicom_compare_uids — Compare UID sets (e.g. SeriesInstanceUID) between two DICOM directories; supports any tag keyword or hex code
  • dicom_verify_segmentations — Validate that segmentation DICOM files reference valid source images via SourceImageSequence
  • dicom_analyze_ti — Extract and validate inversion times from MOLLI/T1 mapping sequences across vendors; handles Philips private TI tags automatically

Key constants:

  • MAX_FILES — Safety limit for directory scans (default 1000, configurable via DICOM_MCP_MAX_FILES env var)
  • COMMON_TAGS — Dictionary of 9 tag groups (patient_info, study_info, series_info, image_info, acquisition, manufacturer, equipment, geometry, pixel_data) mapping to DICOM tag tuples
  • VALID_TAG_GROUPS — Sorted list of valid tag group names

PII Filtering

Patient-identifying tags can be redacted from tool output via an environment variable:

  • Enable: DICOM_MCP_PII_FILTER=true (accepts true, 1, yes, case-insensitive)
  • Disable: unset or any other value (default)

Redacted tags (patient tags only): PatientName (0010,0010), PatientID (0010,0020), PatientBirthDate (0010,0030), PatientSex (0010,0040).

Affected tools: dicom_get_metadata, dicom_compare_headers, dicom_summarize_directory, dicom_query. All other tools do not expose patient data and are unaffected.

Redaction is applied at the output formatting level via redact_if_pii() in pii.py. Internal logic (sequence identification, grouping) uses raw values.

Behavioural Constraints

This MCP is a data inspection tool, not a clinical decision support system. When using these tools, keep responses strictly factual and descriptive:

  • Report what is observed in the DICOM data (tag values, pixel statistics, parameters, counts)
  • Describe technical characteristics (acquisition settings, sequence types, vendor differences)
  • Do not suggest clinical utility, diagnostic applications, or workflow suitability
  • Do not interpret findings in a clinical or diagnostic context
  • Do not assess data quality relative to specific clinical use cases
  • Do not recommend clinical actions based on the data

Present data as-is. Qualified professionals draw the conclusions.

These constraints are enforced at protocol level via FastMCP(instructions=...) in server.py, which sends them to any MCP client at connection time. See docs/GUIDELINES.md for the full policy and regulatory context.

Key Patterns

  • All tools use ResponseFormat enum (MARKDOWN/JSON) for output formatting
  • SequenceType enum covers: DIXON, T1_MAPPING, MULTI_ECHO_GRE, SPIN_ECHO_IR, T1, T2, FLAIR, DWI, LOCALIZER, UNKNOWN
  • DICOM files are validated by checking for the 128-byte preamble + "DICM" magic bytes in _is_dicom_file()
  • Custom DICOM tags can be specified in hex format (GGGG,EEEE)
  • Philips private tags are resolved dynamically per-file via _resolve_philips_private_tag() — block assignments vary across scanners
  • All synchronous I/O (pydicom.dcmread, file globbing) is wrapped in asyncio.to_thread() to keep the event loop responsive
  • _find_dicom_files() returns tuple[list[tuple[Path, Dataset]], bool] — pre-read datasets + truncation flag — to avoid double-reading files
  • The server imports FastMCP from mcp.server.fastmcp (not directly from fastmcp)

Documentation

Additional documentation lives in the docs/ directory: