# 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