Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ intermediate-findings/
playwright-report/
test-results/
__pycache__/
*.pyc
*.pyc
/examples/dicom-viewer-mcp-app/dicom
3 changes: 3 additions & 0 deletions examples/dicom-viewer-mcp-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
.DS_Store
21 changes: 21 additions & 0 deletions examples/dicom-viewer-mcp-app/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Thales Matheus Mendonça Santos

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
180 changes: 180 additions & 0 deletions examples/dicom-viewer-mcp-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# DICOM Viewer MCP App

A prototype MCP (Model Context Protocol) App that displays DICOM medical images directly in Claude Desktop. Built using the [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps).

![DICOM Viewer Screenshot](screenshot.png)

## MCP Client Configuration

Add to your MCP client configuration (stdio transport):

```json
{
"mcpServers": {
"dicom-viewer": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-dicom-viewer",
"--stdio"
]
}
}
}
```

### Local Development

To test local modifications, use this configuration (replace `~/code/ext-apps` with your clone path):

```json
{
"mcpServers": {
"dicom-viewer": {
"command": "bash",
"args": [
"-c",
"cd ~/code/ext-apps/examples/dicom-viewer-mcp-app && npm run build >&2 && node dist/index.js --stdio"
]
}
}
}
```

## Features

- View DICOM medical images directly within Claude Desktop
- **Series navigation** - browse through multiple slices with slider, buttons, or scroll wheel
- **Keyboard shortcuts** - arrow keys for navigation, Home/End for first/last slice
- Pan and zoom controls for image inspection
- Displays image metadata (dimensions, bit depth, instance number)
- Server-side rendering to JPEG using `dicom-parser` and `sharp`
- Handles DICOM window/level and rescale slope/intercept
- Automatic sorting by Instance Number or Slice Location

## How It Works

1. The MCP server scans the `./dicom/` folder for all `.dcm` files
2. Parses each DICOM file using `dicom-parser`
3. Extracts slice metadata and sorts by Instance Number or Slice Location
4. Sends a compact HTML UI resource to the host (no embedded images)
5. The client requests slices on-demand via the `get-dicom-slice` tool
6. The server converts each slice to JPEG with proper windowing and returns it

This approach avoids CSP (Content Security Policy) restrictions in Claude Desktop by doing all DICOM processing server-side.

## Prerequisites

- Node.js 18+
- Claude Desktop with MCP support

## Installation

```bash
# From the ext-apps repository root
cd examples/dicom-viewer-mcp-app

# Install dependencies (if not already done at repo root)
npm install

# Build the project
npm run build
```

## Usage

### 1. Add your DICOM files

Place your DICOM files (`.dcm`) in the `./dicom/` folder:

```
./dicom/
├── IM-0001-0001.dcm
├── IM-0001-0002.dcm
├── IM-0001-0003.dcm
└── ...
```

### 2. Configure Claude Desktop

Add the server to your Claude Desktop configuration using the MCP Client Configuration section above.

**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`

### 3. Restart Claude Desktop

After updating the configuration, fully quit and restart Claude Desktop.

### 4. View DICOM images

Ask Claude to "show me a DICOM study" or "use the view-dicom tool".

## Controls

| Action | Control |
| -------------------------- | ------------------------------------ |
| Navigate slices | Scroll wheel, slider, or ◀/▶ buttons |
| Navigate slices (keyboard) | Arrow keys (←/→ or ↑/↓) |
| First/Last slice | Home / End keys |
| Zoom | Ctrl + Scroll wheel, or +/− buttons |
| Pan | Click and drag |
| Reset view | Reset button |

## Development

```bash
# Build and run in development mode
npm start

# Or run with hot reload
npm run dev
```

## Project Structure

```
dicom-viewer-mcp-app/
├── dicom/ # Place DICOM files here (a single series)
│ └── .gitkeep
├── src/
│ ├── mcp-app.tsx # React client component
│ ├── mcp-app.module.css # Styles
│ └── global.css
├── server.ts # MCP server with DICOM processing
├── main.ts # Entry point (stdio/HTTP transport)
├── mcp-app.html # HTML template
├── package.json
└── README.md
```

## Technical Details

### DICOM Processing

The server handles:

- 8-bit and 16-bit pixel data (signed and unsigned)
- MONOCHROME1 and MONOCHROME2 photometric interpretations
- Rescale slope and intercept transformations
- Window center and window width adjustments
- Automatic slice sorting by Instance Number or Slice Location

### Supported DICOM Transfer Syntaxes

Currently supports uncompressed DICOM files (Explicit/Implicit VR Little Endian). Compressed transfer syntaxes (JPEG, JPEG 2000, etc.) are not yet supported.

## Limitations

- Uncompressed DICOM only (no JPEG/JPEG2000 compression)
- Slice images are fetched on demand (first load per slice can be slow)
- Single series at a time

## Author

**Thales Matheus** - [GitHub](https://github.com/ThalesMMS)

## License

MIT
Empty file.
93 changes: 93 additions & 0 deletions examples/dicom-viewer-mcp-app/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Entry point for running the MCP server.
* Run with: npx @modelcontextprotocol/server-dicom-viewer
* Or: node dist/index.js [--stdio]
*/

import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";

/**
* Starts an MCP server with Streamable HTTP transport in stateless mode.
*
* @param createServer - Factory function that creates a new McpServer instance per request.
*/
export async function startStreamableHTTPServer(
createServer: () => McpServer,
): Promise<void> {
const port = parseInt(process.env.PORT ?? "3001", 10);

const app = createMcpExpressApp({ host: "0.0.0.0" });
app.use(cors());

app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});

res.on("close", () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});

try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("MCP error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});

const httpServer = app.listen(port, (err) => {
if (err) {
console.error("Failed to start server:", err);
process.exit(1);
}
console.log(`MCP server listening on http://localhost:${port}/mcp`);
});

const shutdown = () => {
console.log("\nShutting down...");
httpServer.close(() => process.exit(0));
};

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}

/**
* Starts an MCP server with stdio transport.
*
* @param createServer - Factory function that creates a new McpServer instance.
*/
export async function startStdioServer(
createServer: () => McpServer,
): Promise<void> {
await createServer().connect(new StdioServerTransport());
}

async function main() {
if (process.argv.includes("--stdio")) {
await startStdioServer(createServer);
} else {
await startStreamableHTTPServer(createServer);
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
14 changes: 14 additions & 0 deletions examples/dicom-viewer-mcp-app/mcp-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>DICOM Viewer</title>
<link rel="stylesheet" href="/src/global.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/mcp-app.tsx"></script>
</body>
</html>
58 changes: 58 additions & 0 deletions examples/dicom-viewer-mcp-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@modelcontextprotocol/server-dicom-viewer",
"version": "1.0.0",
"type": "module",
"description": "A prototype MCP App that displays DICOM medical images in Claude Desktop",
"repository": {
"type": "git",
"url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/dicom-viewer-mcp-app"
},
"license": "MIT",
"main": "dist/server.js",
"files": [
"dist"
],
"scripts": {
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun --watch main.ts",
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"dicom-parser": "^1.8.21",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"sharp": "^0.33.5",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "22.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
},
"types": "dist/server.d.ts",
"exports": {
".": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
}
},
"bin": {
"mcp-server-dicom-viewer": "dist/index.js"
}
}
Binary file added examples/dicom-viewer-mcp-app/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading