# 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

```bash
# 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](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:

- **[docs/USAGE.md](docs/USAGE.md)** — Detailed tool reference, parameter guide, and QA workflow examples
- **[docs/TODO.md](docs/TODO.md)** — Planned improvements and known issues
- **[docs/CAPABILITIES.md](docs/CAPABILITIES.md)** — Plain-English summary of all 17 tool capabilities
- **[docs/GUIDELINES.md](docs/GUIDELINES.md)** — Behavioural constraints and regulatory context
