Content
# dicom-mcp
An [MCP](https://modelcontextprotocol.io) server, written in Rust, that lets a
person read **their own** DICOM medical imaging through Claude — list their
studies, look at individual images, and build a PDF to bring to their doctor or
specialist.
It is deliberately narrow and privacy-first:
- **Read-only.** It never writes to, moves, or deletes a source DICOM file.
Exports (PNG/PDF) are written only to the server's own output folder (or a
configured path).
- **CT, MR, and X-ray only** (`CT`, `MR`, and the projection-radiography family
`CR`/`DX`/`XA`/`RF`). Other modalities are filtered out at scan time.
- **Anonymized.** Image metadata returned to the model is an allow-listed,
non-identifying set. Patient names, IDs, birth dates, accession numbers,
institutions, and DICOM UIDs are never read or surfaced.
- **Not medical advice.** The server helps *surface* and *organize* a person's
imaging and prepare questions. Diagnosis belongs with a clinician.
> ⚕️ This tool empowers individuals to access and understand their own imaging.
> It is not a diagnostic device and does not replace professional radiological
> interpretation.
## How it works
DICOM studies — whether exported from a portal or burned on a disc — are
indexed by a `DICOMDIR` file (a file literally named `DICOMDIR`, no extension).
The server is pointed at the folder the imaging lives in; it scans that folder and
**all subfolders** for every `DICOMDIR` (including a disc copied to disk), parses
the `PATIENT → STUDY → SERIES → IMAGE` hierarchy, keeps the CT/MR/XR series, and
reports the studies it found:
> **StudyName · date · # series · total images**
## Tools
| Tool | What it does |
|------|--------------|
| `scan_folder` | Recursively scan a folder for DICOMDIRs and load CT/MR/XR studies. Replies with the studies found. |
| `list_studies` | List the studies currently loaded (persists across restarts). |
| `get_study` | Series breakdown for one study (modality, number, name, image count) plus any attached reports. |
| `get_report` | Retrieve a study's report: a Structured Report rendered to text inline, or an encapsulated PDF written to the output folder. |
| `get_image_info` | Anonymized technical metadata for one image (see below). |
| `view_image` | Inline 8-bit, window-leveled **preview** so the image can be looked at and discussed. |
| `export_image_png` | **Lossless** PNG export — raw stored values at native bit depth by default (or window-leveled 16-bit). |
| `generate_pdf` | Build a PDF in `adjacency` or `grid` layout to share with a clinician. |
| `clear_studies` | Drop the loaded set (does not touch source files). Requires `confirm: true`. |
| `build_info` | Version, git build/commit, **UTC compile time**, target, rustc. |
### What image metadata is returned
Only technical, non-identifying attributes:
- **Position:** image *N / M*, series, series name, modality.
- **Dimensions / size / type:** rows × columns, samples per pixel, bits
allocated/stored, signed/unsigned, photometric interpretation, frames,
uncompressed stored byte size, and a one-line summary.
- **Slice geometry:** slice thickness, slice location, spacing between slices,
pixel spacing, slice position (z), kVp.
- **Acquisition date-time** and **scanner make/model.**
- **Intensity mapping:** rescale slope/intercept and window center/width — so a
clinician can interpret hypo/hyper-intensities.
### Reports
Studies often ship the report alongside the images, as a DICOM **Structured
Report** (SR) or an **encapsulated PDF**. These live in `SR`/`DOC` series that
the CT/MR/XR filter drops, so they're captured separately and listed per study
(`report_count` / the `reports` array in `get_study`). `get_report` renders an
SR to readable text inline (radiation-dose reports, key-object selections,
findings — whatever the SR contains, walked from its content tree) or writes an
encapsulated PDF to the output folder. A report is the patient's **own**
document and may name them, so — unlike image metadata — it is returned as-is,
not anonymized.
### Lossless export & fidelity
Medical interpretation depends on bit depth (hypo/hyper-intensities live in the
stored pixel values), so export is built around fidelity:
- `export_image_png` defaults to the **raw stored pixel values at native bit
depth**, with no modality LUT / VOI window / rescale applied — 16-bit
grayscale PNG with **maximum** (`Compression::High`) lossless compression.
Signed pixels are shifted into the unsigned range by a recorded
`applied_offset` (subtract it to recover the original signed value). Pass
`windowed: true` for a display-leveled 16-bit image instead.
- The **PDF generator embeds images losslessly** at native resolution as 16-bit
grayscale and merely *scales* them by the PDF transform to fit each cell — no
resampling. Streams use lossless **FlateDecode** (no DCT/JPEG, no downscaling),
so embedded pixels stay bit-exact while keeping the PDF compact. (Images are
window-leveled for visibility.)
- `view_image` returns an 8-bit, downscaled preview purely so the model can see
the picture — it is explicitly *not* a faithful copy.
### PDF layouts
- **`adjacency`** — the selected image centered, with its series neighbours in a
cross: **left** = previous slice, **right** = next slice, **top** =
two-back, **bottom** = two-forward. A neighbour past the end of the series is
drawn as a grey placeholder of the same cell size.
- **`grid`** — a contact sheet of the whole series (or an inclusive `from..to`
range) in `cols` columns (default 4), each image captioned
`YYYY-MM MOD SeriesName Image##`.
## Building
```sh
cargo build --release
```
Requires Rust ≥ 1.85 (edition 2024) and a **C compiler** (`cc`/MSVC) — JPEG 2000
support compiles a bundled copy of the C OpenJPEG library (`openjpeg-sys`). No
separate GDCM/OpenJPEG/`cmake` install is needed.
Decoders: uncompressed, RLE, and baseline/extended JPEG via pure-Rust `native`;
**JPEG 2000** (`1.2.840.10008.1.2.4.90/.91`, very common in PACS exports) via C
OpenJPEG. JPEG-LS and JPEG XL are not enabled. The `transfer_syntax` field in
`get_image_info` reports what each image uses. See
[KNOWN_ISSUES.md](KNOWN_ISSUES.md) for why the pure-Rust JPEG 2000 decoder is
deliberately avoided (it aborts the process) and how to build without the C
dependency.
## Running
The server speaks JSON-RPC over **stdio** (stdout is reserved for the protocol).
```sh
dicom-mcp serve
```
Flags:
- `--ephemeral` — don't persist loaded studies between runs.
- `--state-file <path>` — override the state file location.
- `--allowed-root <dir>` — restrict `scan_folder` to paths under this root
(repeatable). Default: unrestricted.
- `dicom-mcp version` — print build info and exit.
### Claude Desktop config
```json
{
"mcpServers": {
"dicom": {
"command": "/path/to/dicom-mcp",
"args": ["serve"]
}
}
}
```
## Files & logging
Everything the server writes lives under the OS-standard per-user data
directory (never inside the imaging folders):
- Windows `…\AppData\Local\dicom-mcp\`
- macOS `~/Library/Application Support/dicom-mcp/`
- Linux `~/.local/share/dicom-mcp/`
| Path | Contents |
|------|----------|
| `state.json` | The set of loaded studies (metadata only — never pixel data). |
| `logs/dicom-mcp.log` | Rolling daily runtime log (also mirrored to stderr). |
| `error.log` | Scrubbed JSONL records for unexpected errors (version + kind only — no paths or image data). |
| `out/` | Exported PNGs and generated PDFs. |
## Privacy
This software reads private medical data. By design it only ever surfaces the
anonymized technical metadata listed above, never patient identifiers or UIDs,
and writes nothing back to the imaging. The scrubbed `error.log` exists so it
can be *voluntarily* attached to a bug report; review it first.
## License
MIT — see [LICENSE](LICENSE).