153 lines
9.2 KiB
Markdown
153 lines
9.2 KiB
Markdown
|
|
# 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
|