llm-tools/mcps/dicom_mcp/CLAUDE.md

153 lines
9.2 KiB
Markdown
Raw Normal View History

2026-04-08 11:11:04 +00:00
# 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