Upload 14 files
Browse files- Dockerfile +12 -7
- README.md +59 -4
- app.py +43 -33
- medpy/mcp_output/README_MCP.md +153 -154
- medpy/mcp_output/mcp_plugin/adapter.py +188 -295
- medpy/mcp_output/mcp_plugin/main.py +8 -9
- medpy/mcp_output/mcp_plugin/mcp_service.py +321 -185
- medpy/mcp_output/requirements.txt +4 -6
- medpy/mcp_output/start_mcp.py +23 -18
- port.json +1 -5
- requirements.txt +5 -5
- run_docker.ps1 +5 -24
- run_docker.sh +6 -73
Dockerfile
CHANGED
|
@@ -1,18 +1,23 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
|
| 7 |
WORKDIR /app
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
COPY --chown=user . /app
|
| 13 |
ENV MCP_TRANSPORT=http
|
| 14 |
ENV MCP_PORT=7860
|
| 15 |
|
| 16 |
EXPOSE 7860
|
| 17 |
|
|
|
|
|
|
|
| 18 |
CMD ["python", "medpy/mcp_output/start_mcp.py"]
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 4 |
+
PYTHONUNBUFFERED=1
|
|
|
|
| 5 |
|
| 6 |
WORKDIR /app
|
| 7 |
|
| 8 |
+
RUN useradd -m -u 1000 appuser
|
| 9 |
+
|
| 10 |
+
COPY requirements.txt /app/requirements.txt
|
| 11 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY medpy /app/medpy
|
| 14 |
+
COPY app.py /app/app.py
|
| 15 |
|
|
|
|
| 16 |
ENV MCP_TRANSPORT=http
|
| 17 |
ENV MCP_PORT=7860
|
| 18 |
|
| 19 |
EXPOSE 7860
|
| 20 |
|
| 21 |
+
USER appuser
|
| 22 |
+
|
| 23 |
CMD ["python", "medpy/mcp_output/start_mcp.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,65 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: medpy MCP Service
|
| 3 |
+
emoji: 🔧
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# medpy MCP Service
|
| 12 |
+
|
| 13 |
+
This deployment package exposes MedPy medical-image processing capabilities through FastMCP.
|
| 14 |
+
It supports local stdio usage (Claude Desktop / CLI) and HTTP transport for Docker/HuggingFace Spaces.
|
| 15 |
+
|
| 16 |
+
## Available tools
|
| 17 |
+
|
| 18 |
+
- `health_check` — dependency and adapter status
|
| 19 |
+
- `list_modules` — list loaded MedPy modules
|
| 20 |
+
- `list_symbols` — inspect public module symbols
|
| 21 |
+
- `image_info` — inspect shape/dtype/spacing from an image
|
| 22 |
+
- `anisotropic_diffusion` — denoise and save image
|
| 23 |
+
- `otsu_segmentation` — threshold and save binary mask
|
| 24 |
+
- `compute_overlap_metrics` — Dice/Jaccard/Precision/Recall
|
| 25 |
+
- `bounding_box` — non-zero mask bounding box
|
| 26 |
+
|
| 27 |
+
## Local stdio usage
|
| 28 |
+
|
| 29 |
+
Run from the deployment root:
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
cd medpy/mcp_output
|
| 33 |
+
export MCP_TRANSPORT=stdio
|
| 34 |
+
python start_mcp.py
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Or:
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
python medpy/mcp_output/mcp_plugin/main.py
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## HTTP usage (Docker / HF Spaces)
|
| 44 |
+
|
| 45 |
+
Docker entrypoint runs:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
python medpy/mcp_output/start_mcp.py
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
with environment:
|
| 52 |
+
|
| 53 |
+
- `MCP_TRANSPORT=http`
|
| 54 |
+
- `MCP_PORT=7860`
|
| 55 |
+
|
| 56 |
+
The MCP HTTP endpoint is available at:
|
| 57 |
+
|
| 58 |
+
- `https://<host>/mcp`
|
| 59 |
+
|
| 60 |
+
## Build and run with scripts
|
| 61 |
+
|
| 62 |
+
- Bash: `./run_docker.sh`
|
| 63 |
+
- PowerShell: `./run_docker.ps1`
|
| 64 |
+
|
| 65 |
+
Both scripts read port mapping from `port.json` (fixed to `7860` for HF Spaces compatibility).
|
app.py
CHANGED
|
@@ -1,45 +1,55 @@
|
|
| 1 |
-
from
|
|
|
|
| 2 |
import os
|
| 3 |
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
sys.path.insert(0, mcp_plugin_path)
|
| 7 |
|
| 8 |
-
app = FastAPI(
|
| 9 |
-
title="Medpy MCP Service",
|
| 10 |
-
description="Auto-generated MCP service for medpy",
|
| 11 |
-
version="1.0.0"
|
| 12 |
-
)
|
| 13 |
|
| 14 |
@app.get("/")
|
| 15 |
-
def root():
|
| 16 |
return {
|
| 17 |
-
"
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
| 21 |
}
|
| 22 |
|
|
|
|
| 23 |
@app.get("/health")
|
| 24 |
-
def
|
| 25 |
-
return {"status": "healthy"
|
|
|
|
| 26 |
|
| 27 |
@app.get("/tools")
|
| 28 |
-
def
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
import os
|
| 4 |
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Dict, List
|
| 7 |
+
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
+
|
| 10 |
+
PLUGIN_DIR = Path(__file__).resolve().parent / "medpy" / "mcp_output" / "mcp_plugin"
|
| 11 |
+
if PLUGIN_DIR.exists():
|
| 12 |
+
plugin_str = str(PLUGIN_DIR)
|
| 13 |
+
if plugin_str not in sys.path:
|
| 14 |
+
sys.path.insert(0, plugin_str)
|
| 15 |
+
|
| 16 |
+
PORT = int(os.getenv("PORT", "7860"))
|
| 17 |
|
| 18 |
+
app = FastAPI(title="medpy-mcp-info", version="1.0.0")
|
|
|
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
@app.get("/")
|
| 22 |
+
def root() -> Dict[str, Any]:
|
| 23 |
return {
|
| 24 |
+
"name": "medpy MCP service",
|
| 25 |
+
"description": "Supplementary info app for local development.",
|
| 26 |
+
"mcp_entrypoint": "medpy/mcp_output/start_mcp.py",
|
| 27 |
+
"default_port": PORT,
|
| 28 |
}
|
| 29 |
|
| 30 |
+
|
| 31 |
@app.get("/health")
|
| 32 |
+
def health() -> Dict[str, str]:
|
| 33 |
+
return {"status": "healthy"}
|
| 34 |
+
|
| 35 |
|
| 36 |
@app.get("/tools")
|
| 37 |
+
def tools() -> Dict[str, List[Dict[str, Any]]]:
|
| 38 |
+
from mcp_service import create_app
|
| 39 |
+
|
| 40 |
+
mcp = create_app()
|
| 41 |
+
tools_obj = getattr(mcp, "tools", None)
|
| 42 |
+
output: List[Dict[str, Any]] = []
|
| 43 |
+
|
| 44 |
+
if isinstance(tools_obj, dict):
|
| 45 |
+
for name, tool in tools_obj.items():
|
| 46 |
+
description = getattr(tool, "description", "")
|
| 47 |
+
output.append({"name": str(name), "description": str(description)})
|
| 48 |
+
elif isinstance(tools_obj, list):
|
| 49 |
+
for tool in tools_obj:
|
| 50 |
+
name = getattr(tool, "name", getattr(tool, "__name__", "unknown"))
|
| 51 |
+
description = getattr(tool, "description", "")
|
| 52 |
+
output.append({"name": str(name), "description": str(description)})
|
| 53 |
+
|
| 54 |
+
output.sort(key=lambda x: x["name"])
|
| 55 |
+
return {"tools": output}
|
medpy/mcp_output/README_MCP.md
CHANGED
|
@@ -1,154 +1,153 @@
|
|
| 1 |
-
# MedPy MCP
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
-
|
| 25 |
-
- `
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
-
|
| 66 |
-
-
|
| 67 |
-
-
|
| 68 |
-
-
|
| 69 |
-
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
- `
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
- `medpy.graphcut`
|
|
|
|
| 1 |
+
# MedPy MCP Plugin
|
| 2 |
+
|
| 3 |
+
This MCP layer wraps core MedPy medical-image processing capabilities.
|
| 4 |
+
|
| 5 |
+
## Exposed Tools
|
| 6 |
+
|
| 7 |
+
### 1) `health_check`
|
| 8 |
+
- **Parameters**: none
|
| 9 |
+
- **Description**: Reports dependency availability and adapter module-load status.
|
| 10 |
+
- **Example**:
|
| 11 |
+
```json
|
| 12 |
+
{"tool":"health_check","arguments":{}}
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
### 2) `list_modules`
|
| 16 |
+
- **Parameters**: none
|
| 17 |
+
- **Description**: Lists successfully loaded MedPy modules and failed imports.
|
| 18 |
+
- **Example**:
|
| 19 |
+
```json
|
| 20 |
+
{"tool":"list_modules","arguments":{}}
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
### 3) `list_symbols`
|
| 24 |
+
- **Parameters**:
|
| 25 |
+
- `module_name` (string): Fully qualified module path, e.g. `medpy.filter`.
|
| 26 |
+
- **Example**:
|
| 27 |
+
```json
|
| 28 |
+
{"tool":"list_symbols","arguments":{"module_name":"medpy.metric"}}
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### 4) `image_info`
|
| 32 |
+
- **Parameters**:
|
| 33 |
+
- `image_path` (string): Path to image file or DICOM directory.
|
| 34 |
+
- **Returns**: shape, dtype, min/max/mean, voxel spacing, and offset.
|
| 35 |
+
- **Example**:
|
| 36 |
+
```json
|
| 37 |
+
{"tool":"image_info","arguments":{"image_path":"./data/brain.nii.gz"}}
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
### 5) `anisotropic_diffusion`
|
| 41 |
+
- **Parameters**:
|
| 42 |
+
- `input_path` (string)
|
| 43 |
+
- `output_path` (string)
|
| 44 |
+
- `niter` (int, default `5`)
|
| 45 |
+
- `kappa` (float, default `30.0`)
|
| 46 |
+
- `gamma` (float, default `0.1`)
|
| 47 |
+
- `option` (int, one of `1|2|3`, default `1`)
|
| 48 |
+
- `use_compression` (bool, default `false`)
|
| 49 |
+
- **Example**:
|
| 50 |
+
```json
|
| 51 |
+
{
|
| 52 |
+
"tool":"anisotropic_diffusion",
|
| 53 |
+
"arguments":{
|
| 54 |
+
"input_path":"./data/input.nii.gz",
|
| 55 |
+
"output_path":"./data/denoised.nii.gz",
|
| 56 |
+
"niter":8,
|
| 57 |
+
"kappa":40.0,
|
| 58 |
+
"gamma":0.1,
|
| 59 |
+
"option":1
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
### 6) `otsu_segmentation`
|
| 65 |
+
- **Parameters**:
|
| 66 |
+
- `input_path` (string)
|
| 67 |
+
- `output_mask_path` (string)
|
| 68 |
+
- `bins` (int, default `64`)
|
| 69 |
+
- `keep_largest_component` (bool, default `true`)
|
| 70 |
+
- `use_compression` (bool, default `false`)
|
| 71 |
+
- **Example**:
|
| 72 |
+
```json
|
| 73 |
+
{
|
| 74 |
+
"tool":"otsu_segmentation",
|
| 75 |
+
"arguments":{
|
| 76 |
+
"input_path":"./data/input.nii.gz",
|
| 77 |
+
"output_mask_path":"./data/mask.nii.gz",
|
| 78 |
+
"bins":64,
|
| 79 |
+
"keep_largest_component":true
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### 7) `compute_overlap_metrics`
|
| 85 |
+
- **Parameters**:
|
| 86 |
+
- `result_mask_path` (string)
|
| 87 |
+
- `reference_mask_path` (string)
|
| 88 |
+
- **Returns**: `dice`, `jaccard`, `precision`, `recall`.
|
| 89 |
+
- **Example**:
|
| 90 |
+
```json
|
| 91 |
+
{
|
| 92 |
+
"tool":"compute_overlap_metrics",
|
| 93 |
+
"arguments":{
|
| 94 |
+
"result_mask_path":"./data/pred.nii.gz",
|
| 95 |
+
"reference_mask_path":"./data/gt.nii.gz"
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### 8) `bounding_box`
|
| 101 |
+
- **Parameters**:
|
| 102 |
+
- `mask_path` (string)
|
| 103 |
+
- **Returns**: axis-aligned bounding box as start/stop pairs.
|
| 104 |
+
- **Example**:
|
| 105 |
+
```json
|
| 106 |
+
{"tool":"bounding_box","arguments":{"mask_path":"./data/mask.nii.gz"}}
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
## Standard Tool Response
|
| 110 |
+
All tools return:
|
| 111 |
+
|
| 112 |
+
```json
|
| 113 |
+
{
|
| 114 |
+
"success": true,
|
| 115 |
+
"result": {},
|
| 116 |
+
"error": null
|
| 117 |
+
}
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
On error:
|
| 121 |
+
|
| 122 |
+
```json
|
| 123 |
+
{
|
| 124 |
+
"success": false,
|
| 125 |
+
"result": null,
|
| 126 |
+
"error": "message"
|
| 127 |
+
}
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
## Run Locally (stdio)
|
| 131 |
+
|
| 132 |
+
From `mcp_output`:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
export MCP_TRANSPORT=stdio
|
| 136 |
+
python start_mcp.py
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
Or directly:
|
| 140 |
+
|
| 141 |
+
```bash
|
| 142 |
+
python mcp_plugin/main.py
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
## Run via HTTP transport
|
| 146 |
+
|
| 147 |
+
```bash
|
| 148 |
+
export MCP_TRANSPORT=http
|
| 149 |
+
export MCP_PORT=8000
|
| 150 |
+
python start_mcp.py
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
MCP HTTP endpoint is served by FastMCP at `/mcp`.
|
|
|
medpy/mcp_output/mcp_plugin/adapter.py
CHANGED
|
@@ -1,312 +1,205 @@
|
|
| 1 |
-
import
|
| 2 |
-
|
| 3 |
-
import traceback
|
| 4 |
import importlib
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
sys.path.insert(0,
|
| 12 |
|
| 13 |
|
| 14 |
class Adapter:
|
| 15 |
-
""
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
def
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
def
|
| 38 |
self,
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
details: Optional[Dict[str, Any]] = None,
|
| 44 |
) -> Dict[str, Any]:
|
| 45 |
-
|
| 46 |
-
if
|
| 47 |
-
|
| 48 |
-
if
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
if
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
"bin.medpy_extract_min_max": "bin.medpy_extract_min_max",
|
| 61 |
-
"bin.medpy_convert": "bin.medpy_convert",
|
| 62 |
-
}
|
| 63 |
-
for key, path in targets.items():
|
| 64 |
-
try:
|
| 65 |
-
self._modules[key] = importlib.import_module(path)
|
| 66 |
-
except Exception as exc:
|
| 67 |
-
self._import_errors[key] = f"{type(exc).__name__}: {exc}"
|
| 68 |
-
|
| 69 |
-
self._bind_imports()
|
| 70 |
-
if self._import_errors:
|
| 71 |
-
self.mode = "cli"
|
| 72 |
-
|
| 73 |
-
def _bind_imports(self) -> None:
|
| 74 |
-
# setup.py symbols
|
| 75 |
-
setup_mod = self._modules.get("setup")
|
| 76 |
-
if setup_mod is not None:
|
| 77 |
-
for symbol in ["read", "run_setup", "try_find_library", "BuildFailed", "ve_build_ext"]:
|
| 78 |
-
if hasattr(setup_mod, symbol):
|
| 79 |
-
self._imports[f"setup.{symbol}"] = getattr(setup_mod, symbol)
|
| 80 |
-
|
| 81 |
-
# bin/medpy_reslice_3d_to_4d.py symbols
|
| 82 |
-
r_mod = self._modules.get("bin.medpy_reslice_3d_to_4d")
|
| 83 |
-
if r_mod is not None:
|
| 84 |
-
for symbol in ["getArguments", "getParser", "main"]:
|
| 85 |
-
if hasattr(r_mod, symbol):
|
| 86 |
-
self._imports[f"bin.medpy_reslice_3d_to_4d.{symbol}"] = getattr(r_mod, symbol)
|
| 87 |
|
| 88 |
-
# bin/medpy_extract_min_max.py symbols
|
| 89 |
-
emm = self._modules.get("bin.medpy_extract_min_max")
|
| 90 |
-
if emm is not None:
|
| 91 |
-
for symbol in ["getArguments", "getParser", "main"]:
|
| 92 |
-
if hasattr(emm, symbol):
|
| 93 |
-
self._imports[f"bin.medpy_extract_min_max.{symbol}"] = getattr(emm, symbol)
|
| 94 |
-
|
| 95 |
-
# bin/medpy_convert.py symbols
|
| 96 |
-
c_mod = self._modules.get("bin.medpy_convert")
|
| 97 |
-
if c_mod is not None and hasattr(c_mod, "getArguments"):
|
| 98 |
-
self._imports["bin.medpy_convert.getArguments"] = getattr(c_mod, "getArguments")
|
| 99 |
-
|
| 100 |
-
def health(self) -> Dict[str, Any]:
|
| 101 |
-
"""
|
| 102 |
-
Report adapter readiness and import status.
|
| 103 |
-
|
| 104 |
-
Returns:
|
| 105 |
-
Dict with status, mode, imported modules, available symbols, and import errors.
|
| 106 |
-
"""
|
| 107 |
-
return self._result(
|
| 108 |
-
status="success" if not self._import_errors else "fallback",
|
| 109 |
-
data={
|
| 110 |
-
"imported_modules": sorted(self._modules.keys()),
|
| 111 |
-
"available_symbols": sorted(self._imports.keys()),
|
| 112 |
-
"import_errors": self._import_errors,
|
| 113 |
-
},
|
| 114 |
-
guidance=(
|
| 115 |
-
"Import mode fully available."
|
| 116 |
-
if not self._import_errors
|
| 117 |
-
else "Some imports failed. Use CLI commands as fallback."
|
| 118 |
-
),
|
| 119 |
-
)
|
| 120 |
-
|
| 121 |
-
# -------------------------------------------------------------------------
|
| 122 |
-
# Utility execution wrappers
|
| 123 |
-
# -------------------------------------------------------------------------
|
| 124 |
-
|
| 125 |
-
def _call(self, key: str, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 126 |
-
func = self._imports.get(key)
|
| 127 |
-
if func is None:
|
| 128 |
-
return self._result(
|
| 129 |
-
status="fallback",
|
| 130 |
-
error=f"Function '{key}' is not available in import mode.",
|
| 131 |
-
guidance="Verify source checkout and dependencies, or run the corresponding CLI command.",
|
| 132 |
-
details={"import_errors": self._import_errors},
|
| 133 |
-
)
|
| 134 |
try:
|
| 135 |
-
|
| 136 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
except Exception as exc:
|
| 138 |
-
return
|
| 139 |
-
status
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
try:
|
| 168 |
-
obj = cls(*args, **kwargs)
|
| 169 |
-
return self._result(status="success", data={"result": obj, "class": "setup.BuildFailed"})
|
| 170 |
-
except Exception as exc:
|
| 171 |
-
return self._result(
|
| 172 |
-
status="error",
|
| 173 |
-
error=f"{type(exc).__name__}: {exc}",
|
| 174 |
-
guidance="Review constructor arguments for BuildFailed.",
|
| 175 |
-
details={"traceback": traceback.format_exc()},
|
| 176 |
-
)
|
| 177 |
-
|
| 178 |
-
def create_ve_build_ext(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 179 |
-
"""
|
| 180 |
-
Create an instance of setup.ve_build_ext.
|
| 181 |
-
|
| 182 |
-
Parameters:
|
| 183 |
-
*args, **kwargs: Forwarded to ve_build_ext constructor.
|
| 184 |
|
| 185 |
-
Returns:
|
| 186 |
-
Unified status dictionary with the created instance in data.result.
|
| 187 |
-
"""
|
| 188 |
-
cls = self._imports.get("setup.ve_build_ext")
|
| 189 |
-
if cls is None:
|
| 190 |
-
return self._result(
|
| 191 |
-
status="fallback",
|
| 192 |
-
error="Class 'setup.ve_build_ext' is not available.",
|
| 193 |
-
guidance="Ensure setuptools extension build class is importable from setup.py.",
|
| 194 |
-
details={"import_errors": self._import_errors},
|
| 195 |
-
)
|
| 196 |
try:
|
| 197 |
-
|
| 198 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
except Exception as exc:
|
| 200 |
-
return
|
| 201 |
-
status
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
def call_setup_read(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 208 |
-
"""Call setup.read(*args, **kwargs)."""
|
| 209 |
-
return self._call("setup.read", *args, **kwargs)
|
| 210 |
-
|
| 211 |
-
def call_setup_run_setup(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 212 |
-
"""Call setup.run_setup(*args, **kwargs)."""
|
| 213 |
-
return self._call("setup.run_setup", *args, **kwargs)
|
| 214 |
-
|
| 215 |
-
def call_setup_try_find_library(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 216 |
-
"""Call setup.try_find_library(*args, **kwargs)."""
|
| 217 |
-
return self._call("setup.try_find_library", *args, **kwargs)
|
| 218 |
-
|
| 219 |
-
# -------------------------------------------------------------------------
|
| 220 |
-
# bin.medpy_reslice_3d_to_4d functions
|
| 221 |
-
# -------------------------------------------------------------------------
|
| 222 |
-
|
| 223 |
-
def call_medpy_reslice_3d_to_4d_get_arguments(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 224 |
-
"""
|
| 225 |
-
Call bin.medpy_reslice_3d_to_4d.getArguments.
|
| 226 |
-
|
| 227 |
-
Typically parses CLI-like arguments for reslicing 3D volumes into 4D.
|
| 228 |
-
"""
|
| 229 |
-
return self._call("bin.medpy_reslice_3d_to_4d.getArguments", *args, **kwargs)
|
| 230 |
-
|
| 231 |
-
def call_medpy_reslice_3d_to_4d_get_parser(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 232 |
-
"""
|
| 233 |
-
Call bin.medpy_reslice_3d_to_4d.getParser.
|
| 234 |
-
|
| 235 |
-
Returns parser object used by the script.
|
| 236 |
-
"""
|
| 237 |
-
return self._call("bin.medpy_reslice_3d_to_4d.getParser", *args, **kwargs)
|
| 238 |
-
|
| 239 |
-
def call_medpy_reslice_3d_to_4d_main(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 240 |
-
"""
|
| 241 |
-
Call bin.medpy_reslice_3d_to_4d.main.
|
| 242 |
-
|
| 243 |
-
Executes the script main flow in import mode.
|
| 244 |
-
"""
|
| 245 |
-
return self._call("bin.medpy_reslice_3d_to_4d.main", *args, **kwargs)
|
| 246 |
-
|
| 247 |
-
# -------------------------------------------------------------------------
|
| 248 |
-
# bin.medpy_extract_min_max functions
|
| 249 |
-
# -------------------------------------------------------------------------
|
| 250 |
-
|
| 251 |
-
def call_medpy_extract_min_max_get_arguments(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 252 |
-
"""
|
| 253 |
-
Call bin.medpy_extract_min_max.getArguments.
|
| 254 |
-
|
| 255 |
-
Typically parses arguments for min/max extraction from medical volumes.
|
| 256 |
-
"""
|
| 257 |
-
return self._call("bin.medpy_extract_min_max.getArguments", *args, **kwargs)
|
| 258 |
-
|
| 259 |
-
def call_medpy_extract_min_max_get_parser(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 260 |
-
"""
|
| 261 |
-
Call bin.medpy_extract_min_max.getParser.
|
| 262 |
-
|
| 263 |
-
Returns parser object used by the script.
|
| 264 |
-
"""
|
| 265 |
-
return self._call("bin.medpy_extract_min_max.getParser", *args, **kwargs)
|
| 266 |
-
|
| 267 |
-
def call_medpy_extract_min_max_main(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 268 |
-
"""
|
| 269 |
-
Call bin.medpy_extract_min_max.main.
|
| 270 |
-
|
| 271 |
-
Executes min/max extraction script logic.
|
| 272 |
-
"""
|
| 273 |
-
return self._call("bin.medpy_extract_min_max.main", *args, **kwargs)
|
| 274 |
-
|
| 275 |
-
# -------------------------------------------------------------------------
|
| 276 |
-
# bin.medpy_convert functions
|
| 277 |
-
# -------------------------------------------------------------------------
|
| 278 |
-
|
| 279 |
-
def call_medpy_convert_get_arguments(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 280 |
-
"""
|
| 281 |
-
Call bin.medpy_convert.getArguments.
|
| 282 |
-
|
| 283 |
-
Parses conversion CLI arguments for supported medical image formats.
|
| 284 |
-
"""
|
| 285 |
-
return self._call("bin.medpy_convert.getArguments", *args, **kwargs)
|
| 286 |
-
|
| 287 |
-
# -------------------------------------------------------------------------
|
| 288 |
-
# CLI fallback guidance (from analysis-reported commands)
|
| 289 |
-
# -------------------------------------------------------------------------
|
| 290 |
-
|
| 291 |
-
def cli_fallback_commands(self) -> Dict[str, Any]:
|
| 292 |
-
"""
|
| 293 |
-
Provide curated CLI fallback commands detected by analysis.
|
| 294 |
-
|
| 295 |
-
Returns:
|
| 296 |
-
Unified dictionary with command list and descriptions.
|
| 297 |
-
"""
|
| 298 |
-
commands: List[Dict[str, str]] = [
|
| 299 |
-
{"name": "medpy_info", "module": "bin/medpy_info.py", "description": "Inspect image metadata and dimensional information."},
|
| 300 |
-
{"name": "medpy_convert", "module": "bin/medpy_convert.py", "description": "Convert medical image volumes between supported formats."},
|
| 301 |
-
{"name": "medpy_diff", "module": "bin/medpy_diff.py", "description": "Compare two images/volumes and report differences."},
|
| 302 |
-
{"name": "medpy_resample", "module": "bin/medpy_resample.py", "description": "Resample image volume to target spacing/shape."},
|
| 303 |
-
{"name": "medpy_anisotropic_diffusion", "module": "bin/medpy_anisotropic_diffusion.py", "description": "Apply anisotropic diffusion filtering to a volume."},
|
| 304 |
-
{"name": "medpy_watershed", "module": "bin/medpy_watershed.py", "description": "Run watershed segmentation on image data."},
|
| 305 |
-
{"name": "medpy_graphcut_voxel", "module": "bin/medpy_graphcut_voxel.py", "description": "Voxel-level graph-cut segmentation CLI workflow."},
|
| 306 |
-
{"name": "medpy_graphcut_label", "module": "bin/medpy_graphcut_label.py", "description": "Label-based graph-cut segmentation CLI workflow."},
|
| 307 |
-
]
|
| 308 |
-
return self._result(
|
| 309 |
-
status="success",
|
| 310 |
-
data={"commands": commands},
|
| 311 |
-
guidance="If import calls fail, run these commands from the project environment where medpy dependencies are installed.",
|
| 312 |
-
)
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
|
|
|
| 3 |
import importlib
|
| 4 |
+
import inspect
|
| 5 |
+
import pkgutil
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Dict, List, Optional
|
| 9 |
|
| 10 |
+
SOURCE_DIR = Path(__file__).resolve().parents[2] / "source"
|
| 11 |
+
if SOURCE_DIR.exists():
|
| 12 |
+
source_str = str(SOURCE_DIR)
|
| 13 |
+
if source_str not in sys.path:
|
| 14 |
+
sys.path.insert(0, source_str)
|
| 15 |
|
| 16 |
|
| 17 |
class Adapter:
|
| 18 |
+
def __init__(self, base_package: str = "medpy") -> None:
|
| 19 |
+
self.base_package = base_package
|
| 20 |
+
self.loaded_modules: Dict[str, Any] = {}
|
| 21 |
+
self.failed_modules: Dict[str, str] = {}
|
| 22 |
+
self.mode = "ready"
|
| 23 |
+
self._bootstrap()
|
| 24 |
+
|
| 25 |
+
def _bootstrap(self) -> None:
|
| 26 |
+
root = self._safe_import(self.base_package)
|
| 27 |
+
if root is None:
|
| 28 |
+
self.mode = "blackbox"
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
self.loaded_modules[self.base_package] = root
|
| 32 |
+
package_path = getattr(root, "__path__", None)
|
| 33 |
+
if package_path is None:
|
| 34 |
+
self.mode = "ready"
|
| 35 |
+
return
|
| 36 |
+
|
| 37 |
+
for module_info in pkgutil.walk_packages(package_path, prefix=f"{self.base_package}."):
|
| 38 |
+
self._safe_import(module_info.name)
|
| 39 |
+
|
| 40 |
+
if len(self.loaded_modules) <= 1:
|
| 41 |
+
self.mode = "fallback"
|
| 42 |
+
|
| 43 |
+
def _safe_import(self, module_name: str) -> Optional[Any]:
|
| 44 |
+
try:
|
| 45 |
+
module = importlib.import_module(module_name)
|
| 46 |
+
self.loaded_modules[module_name] = module
|
| 47 |
+
return module
|
| 48 |
+
except Exception as exc: # pragma: no cover
|
| 49 |
+
self.failed_modules[module_name] = str(exc)
|
| 50 |
+
return None
|
| 51 |
|
| 52 |
+
def health(self) -> Dict[str, Any]:
|
| 53 |
+
if not self.loaded_modules:
|
| 54 |
+
return {
|
| 55 |
+
"status": "fallback",
|
| 56 |
+
"mode": "blackbox",
|
| 57 |
+
"base_package": self.base_package,
|
| 58 |
+
"loaded_count": 0,
|
| 59 |
+
"failed_count": len(self.failed_modules),
|
| 60 |
+
"failed_modules": self.failed_modules,
|
| 61 |
+
"source_dir": str(SOURCE_DIR),
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
status = "ok"
|
| 65 |
+
if self.mode in {"blackbox", "fallback"}:
|
| 66 |
+
status = "fallback"
|
| 67 |
+
|
| 68 |
+
return {
|
| 69 |
+
"status": status,
|
| 70 |
+
"mode": self.mode,
|
| 71 |
+
"base_package": self.base_package,
|
| 72 |
+
"loaded_count": len(self.loaded_modules),
|
| 73 |
+
"failed_count": len(self.failed_modules),
|
| 74 |
+
"loaded_modules": sorted(self.loaded_modules.keys()),
|
| 75 |
+
"failed_modules": self.failed_modules,
|
| 76 |
+
"source_dir": str(SOURCE_DIR),
|
| 77 |
+
}
|
| 78 |
|
| 79 |
+
def list_modules(self) -> Dict[str, Any]:
|
| 80 |
+
if not self.loaded_modules:
|
| 81 |
+
return {
|
| 82 |
+
"status": "fallback",
|
| 83 |
+
"mode": "blackbox",
|
| 84 |
+
"modules": [],
|
| 85 |
+
"failed_modules": self.failed_modules,
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"status": "ok",
|
| 90 |
+
"mode": self.mode,
|
| 91 |
+
"modules": sorted(self.loaded_modules.keys()),
|
| 92 |
+
"failed_modules": self.failed_modules,
|
| 93 |
+
}
|
| 94 |
|
| 95 |
+
def list_symbols(self, module_name: str) -> Dict[str, Any]:
|
| 96 |
+
module = self.loaded_modules.get(module_name)
|
| 97 |
+
if module is None:
|
| 98 |
+
module = self._safe_import(module_name)
|
| 99 |
+
|
| 100 |
+
if module is None:
|
| 101 |
+
return {
|
| 102 |
+
"status": "error",
|
| 103 |
+
"message": f"Module not available: {module_name}",
|
| 104 |
+
"failed_modules": self.failed_modules,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
symbols: List[Dict[str, str]] = []
|
| 108 |
+
for name, value in inspect.getmembers(module):
|
| 109 |
+
if name.startswith("_"):
|
| 110 |
+
continue
|
| 111 |
+
if inspect.isclass(value):
|
| 112 |
+
sym_type = "class"
|
| 113 |
+
elif inspect.isfunction(value) or inspect.isbuiltin(value):
|
| 114 |
+
sym_type = "function"
|
| 115 |
+
elif inspect.ismodule(value):
|
| 116 |
+
sym_type = "module"
|
| 117 |
+
else:
|
| 118 |
+
sym_type = "value"
|
| 119 |
+
symbols.append({"name": name, "type": sym_type})
|
| 120 |
+
|
| 121 |
+
return {
|
| 122 |
+
"status": "ok",
|
| 123 |
+
"module": module_name,
|
| 124 |
+
"symbol_count": len(symbols),
|
| 125 |
+
"symbols": symbols,
|
| 126 |
+
}
|
| 127 |
|
| 128 |
+
def call_function(
|
| 129 |
self,
|
| 130 |
+
module_name: str,
|
| 131 |
+
function_name: str,
|
| 132 |
+
args: Optional[List[Any]] = None,
|
| 133 |
+
kwargs: Optional[Dict[str, Any]] = None,
|
|
|
|
| 134 |
) -> Dict[str, Any]:
|
| 135 |
+
module = self.loaded_modules.get(module_name)
|
| 136 |
+
if module is None:
|
| 137 |
+
module = self._safe_import(module_name)
|
| 138 |
+
if module is None:
|
| 139 |
+
return {"status": "error", "message": f"Module not available: {module_name}"}
|
| 140 |
+
|
| 141 |
+
function_obj = getattr(module, function_name, None)
|
| 142 |
+
if function_obj is None or not callable(function_obj):
|
| 143 |
+
return {
|
| 144 |
+
"status": "error",
|
| 145 |
+
"message": f"Function not found or not callable: {module_name}.{function_name}",
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
call_args = args if args is not None else []
|
| 149 |
+
call_kwargs = kwargs if kwargs is not None else {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
try:
|
| 152 |
+
result = function_obj(*call_args, **call_kwargs)
|
| 153 |
+
return {
|
| 154 |
+
"status": "ok",
|
| 155 |
+
"module": module_name,
|
| 156 |
+
"function": function_name,
|
| 157 |
+
"result": result,
|
| 158 |
+
}
|
| 159 |
except Exception as exc:
|
| 160 |
+
return {
|
| 161 |
+
"status": "error",
|
| 162 |
+
"module": module_name,
|
| 163 |
+
"function": function_name,
|
| 164 |
+
"message": str(exc),
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
def create_instance(
|
| 168 |
+
self,
|
| 169 |
+
module_name: str,
|
| 170 |
+
class_name: str,
|
| 171 |
+
args: Optional[List[Any]] = None,
|
| 172 |
+
kwargs: Optional[Dict[str, Any]] = None,
|
| 173 |
+
) -> Dict[str, Any]:
|
| 174 |
+
module = self.loaded_modules.get(module_name)
|
| 175 |
+
if module is None:
|
| 176 |
+
module = self._safe_import(module_name)
|
| 177 |
+
if module is None:
|
| 178 |
+
return {"status": "error", "message": f"Module not available: {module_name}"}
|
| 179 |
+
|
| 180 |
+
class_obj = getattr(module, class_name, None)
|
| 181 |
+
if class_obj is None or not inspect.isclass(class_obj):
|
| 182 |
+
return {
|
| 183 |
+
"status": "error",
|
| 184 |
+
"message": f"Class not found: {module_name}.{class_name}",
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
init_args = args if args is not None else []
|
| 188 |
+
init_kwargs = kwargs if kwargs is not None else {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
try:
|
| 191 |
+
instance = class_obj(*init_args, **init_kwargs)
|
| 192 |
+
return {
|
| 193 |
+
"status": "ok",
|
| 194 |
+
"module": module_name,
|
| 195 |
+
"class": class_name,
|
| 196 |
+
"instance_type": type(instance).__name__,
|
| 197 |
+
"instance_repr": repr(instance),
|
| 198 |
+
}
|
| 199 |
except Exception as exc:
|
| 200 |
+
return {
|
| 201 |
+
"status": "error",
|
| 202 |
+
"module": module_name,
|
| 203 |
+
"class": class_name,
|
| 204 |
+
"message": str(exc),
|
| 205 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
medpy/mcp_output/mcp_plugin/main.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
"""
|
| 4 |
from mcp_service import create_app
|
| 5 |
|
| 6 |
-
def main():
|
| 7 |
-
"""Main entry point"""
|
| 8 |
-
app = create_app()
|
| 9 |
-
return app
|
| 10 |
|
| 11 |
if __name__ == "__main__":
|
| 12 |
-
|
| 13 |
-
app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
|
|
|
| 3 |
from mcp_service import create_app
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
if __name__ == "__main__":
|
| 7 |
+
# Local stdio entry point only (Claude Desktop / CLI), not for Docker/web deployment.
|
| 8 |
+
app = create_app()
|
| 9 |
+
try:
|
| 10 |
+
app.run(transport="stdio")
|
| 11 |
+
except TypeError:
|
| 12 |
+
app.run()
|
medpy/mcp_output/mcp_plugin/mcp_service.py
CHANGED
|
@@ -1,207 +1,343 @@
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
|
| 5 |
-
if
|
| 6 |
-
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
else:
|
| 80 |
-
converted_kwargs[key] = int(value)
|
| 81 |
-
except ValueError:
|
| 82 |
-
pass
|
| 83 |
-
|
| 84 |
-
instance = BuildFailed(*converted_args, **converted_kwargs)
|
| 85 |
-
return {"success": True, "result": str(instance), "error": None}
|
| 86 |
-
except Exception as e:
|
| 87 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 88 |
-
|
| 89 |
-
@mcp.tool(name="ve_build_ext", description="ve_build_ext class")
|
| 90 |
-
def ve_build_ext(*args, **kwargs):
|
| 91 |
-
"""ve_build_ext class"""
|
| 92 |
-
try:
|
| 93 |
-
if ve_build_ext is None:
|
| 94 |
-
return {"success": False, "result": None, "error": "Class ve_build_ext is not available, path may need adjustment"}
|
| 95 |
-
|
| 96 |
-
# MCP parameter type conversion
|
| 97 |
-
converted_args = []
|
| 98 |
-
converted_kwargs = kwargs.copy()
|
| 99 |
-
|
| 100 |
-
# Handle position argument type conversion
|
| 101 |
-
for arg in args:
|
| 102 |
-
if isinstance(arg, str):
|
| 103 |
-
# Try to convert to numeric type
|
| 104 |
-
try:
|
| 105 |
-
if '.' in arg:
|
| 106 |
-
converted_args.append(float(arg))
|
| 107 |
-
else:
|
| 108 |
-
converted_args.append(int(arg))
|
| 109 |
-
except ValueError:
|
| 110 |
-
converted_args.append(arg)
|
| 111 |
-
else:
|
| 112 |
-
converted_args.append(arg)
|
| 113 |
-
|
| 114 |
-
# Handle keyword argument type conversion
|
| 115 |
-
for key, value in converted_kwargs.items():
|
| 116 |
-
if isinstance(value, str):
|
| 117 |
-
try:
|
| 118 |
-
if '.' in value:
|
| 119 |
-
converted_kwargs[key] = float(value)
|
| 120 |
-
else:
|
| 121 |
-
converted_kwargs[key] = int(value)
|
| 122 |
-
except ValueError:
|
| 123 |
-
pass
|
| 124 |
-
|
| 125 |
-
instance = ve_build_ext(*converted_args, **converted_kwargs)
|
| 126 |
-
return {"success": True, "result": str(instance), "error": None}
|
| 127 |
-
except Exception as e:
|
| 128 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 129 |
-
|
| 130 |
-
@mcp.tool(name="getArguments", description="Auto-wrapped function getArguments")
|
| 131 |
-
def getArguments(payload: dict):
|
| 132 |
try:
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
try:
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
try:
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
try:
|
| 163 |
-
if
|
| 164 |
-
return
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
try:
|
| 173 |
-
if
|
| 174 |
-
return
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
try:
|
| 183 |
-
if
|
| 184 |
-
return
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
try:
|
| 193 |
-
if
|
| 194 |
-
return
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
except Exception as e:
|
| 198 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
|
| 202 |
def create_app():
|
| 203 |
-
"""Create and return FastMCP application instance"""
|
| 204 |
return mcp
|
| 205 |
|
|
|
|
| 206 |
if __name__ == "__main__":
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
import os
|
| 4 |
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Dict, List
|
| 7 |
|
| 8 |
+
SOURCE_DIR = Path(__file__).resolve().parents[2] / "source"
|
| 9 |
+
if SOURCE_DIR.exists():
|
| 10 |
+
source_str = str(SOURCE_DIR)
|
| 11 |
+
if source_str not in sys.path:
|
| 12 |
+
sys.path.insert(0, source_str)
|
| 13 |
|
| 14 |
+
try:
|
| 15 |
+
import numpy as np
|
| 16 |
+
except Exception: # pragma: no cover
|
| 17 |
+
np = None
|
| 18 |
|
| 19 |
+
try:
|
| 20 |
+
import medpy.io as medpy_io
|
| 21 |
+
except Exception: # pragma: no cover
|
| 22 |
+
medpy_io = None
|
| 23 |
|
| 24 |
+
try:
|
| 25 |
+
import medpy.filter as medpy_filter
|
| 26 |
+
except Exception: # pragma: no cover
|
| 27 |
+
medpy_filter = None
|
| 28 |
|
| 29 |
+
try:
|
| 30 |
+
import medpy.metric as medpy_metric
|
| 31 |
+
except Exception: # pragma: no cover
|
| 32 |
+
medpy_metric = None
|
| 33 |
|
| 34 |
+
try:
|
| 35 |
+
from fastmcp import FastMCP
|
| 36 |
+
except Exception: # pragma: no cover
|
| 37 |
+
FastMCP = None
|
| 38 |
+
|
| 39 |
+
from .adapter import Adapter
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class _FallbackMCP:
|
| 43 |
+
def tool(self, name: str, description: str):
|
| 44 |
+
def _decorator(func):
|
| 45 |
+
return func
|
| 46 |
+
|
| 47 |
+
return _decorator
|
| 48 |
+
|
| 49 |
+
def run(self, *args, **kwargs):
|
| 50 |
+
raise RuntimeError("fastmcp is not available. Please install dependencies.")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
mcp = FastMCP(name="medpy-mcp-service") if FastMCP is not None else _FallbackMCP()
|
| 54 |
+
adapter = Adapter(base_package="medpy")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _ok(result: Any) -> Dict[str, Any]:
|
| 58 |
+
return {"success": True, "result": result, "error": None}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _err(message: str) -> Dict[str, Any]:
|
| 62 |
+
return {"success": False, "result": None, "error": message}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _load_image(path: str):
|
| 66 |
+
if medpy_io is None:
|
| 67 |
+
raise RuntimeError("medpy.io is unavailable")
|
| 68 |
+
return medpy_io.load(path)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _save_image(array: Any, path: str, header: Any = None, use_compression: bool = False) -> None:
|
| 72 |
+
if medpy_io is None:
|
| 73 |
+
raise RuntimeError("medpy.io is unavailable")
|
| 74 |
+
medpy_io.save(array, path, hdr=header if header is not None else False, force=True, use_compression=use_compression)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@mcp.tool(name="health_check", description="Check MCP and MedPy dependency availability.")
|
| 78 |
+
def health_check() -> Dict[str, Any]:
|
| 79 |
+
"""Return runtime health status and dependency availability."""
|
| 80 |
+
deps = {
|
| 81 |
+
"fastmcp": FastMCP is not None,
|
| 82 |
+
"numpy": np is not None,
|
| 83 |
+
"medpy.io": medpy_io is not None,
|
| 84 |
+
"medpy.filter": medpy_filter is not None,
|
| 85 |
+
"medpy.metric": medpy_metric is not None,
|
| 86 |
+
"source_dir": str(SOURCE_DIR),
|
| 87 |
+
"source_dir_exists": SOURCE_DIR.exists(),
|
| 88 |
+
}
|
| 89 |
+
return _ok({"dependencies": deps, "adapter": adapter.health()})
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@mcp.tool(name="list_modules", description="List loaded MedPy modules and failed imports.")
|
| 93 |
+
def list_modules() -> Dict[str, Any]:
|
| 94 |
+
"""List module loading details from the adapter."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
try:
|
| 96 |
+
return _ok(adapter.list_modules())
|
| 97 |
+
except Exception as exc:
|
| 98 |
+
return _err(str(exc))
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@mcp.tool(name="list_symbols", description="List public symbols from a MedPy module.")
|
| 102 |
+
def list_symbols(module_name: str) -> Dict[str, Any]:
|
| 103 |
+
"""
|
| 104 |
+
List public members of the given module.
|
| 105 |
+
|
| 106 |
+
Parameters
|
| 107 |
+
----------
|
| 108 |
+
module_name : str
|
| 109 |
+
Fully qualified module path, for example 'medpy.filter' or 'medpy.metric.binary'.
|
| 110 |
+
"""
|
| 111 |
try:
|
| 112 |
+
return _ok(adapter.list_symbols(module_name))
|
| 113 |
+
except Exception as exc:
|
| 114 |
+
return _err(str(exc))
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
@mcp.tool(name="image_info", description="Load an image and return shape, dtype, and metadata summary.")
|
| 118 |
+
def image_info(image_path: str) -> Dict[str, Any]:
|
| 119 |
+
"""
|
| 120 |
+
Inspect an image file using MedPy I/O.
|
| 121 |
+
|
| 122 |
+
Parameters
|
| 123 |
+
----------
|
| 124 |
+
image_path : str
|
| 125 |
+
Path to an image file or DICOM directory.
|
| 126 |
+
"""
|
| 127 |
try:
|
| 128 |
+
image, header = _load_image(image_path)
|
| 129 |
+
spacing = None
|
| 130 |
+
offset = None
|
| 131 |
+
if medpy_io is not None:
|
| 132 |
+
spacing = medpy_io.get_voxel_spacing(header)
|
| 133 |
+
offset = medpy_io.get_offset(header)
|
| 134 |
+
|
| 135 |
+
info = {
|
| 136 |
+
"path": image_path,
|
| 137 |
+
"shape": list(image.shape),
|
| 138 |
+
"dtype": str(image.dtype),
|
| 139 |
+
"min": float(image.min()),
|
| 140 |
+
"max": float(image.max()),
|
| 141 |
+
"mean": float(image.mean()),
|
| 142 |
+
"voxel_spacing": list(spacing) if spacing is not None else None,
|
| 143 |
+
"offset": list(offset) if offset is not None else None,
|
| 144 |
+
}
|
| 145 |
+
return _ok(info)
|
| 146 |
+
except Exception as exc:
|
| 147 |
+
return _err(str(exc))
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@mcp.tool(name="anisotropic_diffusion", description="Apply anisotropic diffusion denoising and save output image.")
|
| 151 |
+
def anisotropic_diffusion(
|
| 152 |
+
input_path: str,
|
| 153 |
+
output_path: str,
|
| 154 |
+
niter: int = 5,
|
| 155 |
+
kappa: float = 30.0,
|
| 156 |
+
gamma: float = 0.1,
|
| 157 |
+
option: int = 1,
|
| 158 |
+
use_compression: bool = False,
|
| 159 |
+
) -> Dict[str, Any]:
|
| 160 |
+
"""
|
| 161 |
+
Denoise an image with MedPy anisotropic diffusion.
|
| 162 |
+
|
| 163 |
+
Parameters
|
| 164 |
+
----------
|
| 165 |
+
input_path : str
|
| 166 |
+
Input image path.
|
| 167 |
+
output_path : str
|
| 168 |
+
Output image path.
|
| 169 |
+
niter : int
|
| 170 |
+
Number of diffusion iterations.
|
| 171 |
+
kappa : float
|
| 172 |
+
Conduction coefficient.
|
| 173 |
+
gamma : float
|
| 174 |
+
Diffusion step size, usually <= 0.25.
|
| 175 |
+
option : int
|
| 176 |
+
Diffusion mode: 1, 2, or 3.
|
| 177 |
+
use_compression : bool
|
| 178 |
+
Whether to request compressed output where supported.
|
| 179 |
+
"""
|
| 180 |
try:
|
| 181 |
+
if medpy_filter is None:
|
| 182 |
+
return _err("medpy.filter is unavailable")
|
| 183 |
+
|
| 184 |
+
image, header = _load_image(input_path)
|
| 185 |
+
output = medpy_filter.anisotropic_diffusion(
|
| 186 |
+
image,
|
| 187 |
+
niter=int(niter),
|
| 188 |
+
kappa=float(kappa),
|
| 189 |
+
gamma=float(gamma),
|
| 190 |
+
option=int(option),
|
| 191 |
+
)
|
| 192 |
+
_save_image(output, output_path, header=header, use_compression=use_compression)
|
| 193 |
+
return _ok(
|
| 194 |
+
{
|
| 195 |
+
"input_path": input_path,
|
| 196 |
+
"output_path": output_path,
|
| 197 |
+
"shape": list(output.shape),
|
| 198 |
+
"dtype": str(output.dtype),
|
| 199 |
+
}
|
| 200 |
+
)
|
| 201 |
+
except Exception as exc:
|
| 202 |
+
return _err(str(exc))
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
@mcp.tool(name="otsu_segmentation", description="Compute Otsu threshold, generate binary mask, and save it.")
|
| 206 |
+
def otsu_segmentation(
|
| 207 |
+
input_path: str,
|
| 208 |
+
output_mask_path: str,
|
| 209 |
+
bins: int = 64,
|
| 210 |
+
keep_largest_component: bool = True,
|
| 211 |
+
use_compression: bool = False,
|
| 212 |
+
) -> Dict[str, Any]:
|
| 213 |
+
"""
|
| 214 |
+
Segment foreground using Otsu thresholding.
|
| 215 |
+
|
| 216 |
+
Parameters
|
| 217 |
+
----------
|
| 218 |
+
input_path : str
|
| 219 |
+
Input image path.
|
| 220 |
+
output_mask_path : str
|
| 221 |
+
Output binary mask path.
|
| 222 |
+
bins : int
|
| 223 |
+
Histogram bin count used by Otsu algorithm.
|
| 224 |
+
keep_largest_component : bool
|
| 225 |
+
If true, keep only the largest connected foreground component.
|
| 226 |
+
use_compression : bool
|
| 227 |
+
Whether to request compressed output where supported.
|
| 228 |
+
"""
|
| 229 |
try:
|
| 230 |
+
if medpy_filter is None:
|
| 231 |
+
return _err("medpy.filter is unavailable")
|
| 232 |
+
if np is None:
|
| 233 |
+
return _err("numpy is unavailable")
|
| 234 |
+
|
| 235 |
+
image, header = _load_image(input_path)
|
| 236 |
+
threshold = float(medpy_filter.otsu(image, bins=int(bins)))
|
| 237 |
+
mask = image >= threshold
|
| 238 |
+
|
| 239 |
+
if keep_largest_component:
|
| 240 |
+
mask = medpy_filter.largest_connected_component(mask)
|
| 241 |
+
|
| 242 |
+
mask_to_save = np.asarray(mask, dtype=np.uint8)
|
| 243 |
+
_save_image(mask_to_save, output_mask_path, header=header, use_compression=use_compression)
|
| 244 |
+
return _ok(
|
| 245 |
+
{
|
| 246 |
+
"input_path": input_path,
|
| 247 |
+
"output_mask_path": output_mask_path,
|
| 248 |
+
"threshold": threshold,
|
| 249 |
+
"foreground_voxels": int(np.count_nonzero(mask)),
|
| 250 |
+
}
|
| 251 |
+
)
|
| 252 |
+
except Exception as exc:
|
| 253 |
+
return _err(str(exc))
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
@mcp.tool(name="compute_overlap_metrics", description="Compute Dice/Jaccard/Precision/Recall between two masks.")
|
| 257 |
+
def compute_overlap_metrics(result_mask_path: str, reference_mask_path: str) -> Dict[str, Any]:
|
| 258 |
+
"""
|
| 259 |
+
Evaluate overlap metrics for two binary masks.
|
| 260 |
+
|
| 261 |
+
Parameters
|
| 262 |
+
----------
|
| 263 |
+
result_mask_path : str
|
| 264 |
+
Path to predicted or result mask.
|
| 265 |
+
reference_mask_path : str
|
| 266 |
+
Path to ground-truth or reference mask.
|
| 267 |
+
"""
|
| 268 |
try:
|
| 269 |
+
if medpy_metric is None:
|
| 270 |
+
return _err("medpy.metric is unavailable")
|
| 271 |
+
if np is None:
|
| 272 |
+
return _err("numpy is unavailable")
|
| 273 |
+
|
| 274 |
+
result, _ = _load_image(result_mask_path)
|
| 275 |
+
reference, _ = _load_image(reference_mask_path)
|
| 276 |
+
|
| 277 |
+
result_bin = np.asarray(result != 0)
|
| 278 |
+
reference_bin = np.asarray(reference != 0)
|
| 279 |
+
|
| 280 |
+
if result_bin.shape != reference_bin.shape:
|
| 281 |
+
return _err(
|
| 282 |
+
f"Mask shapes do not match: {result_bin.shape} vs {reference_bin.shape}"
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
metrics = {
|
| 286 |
+
"dice": float(medpy_metric.dc(result_bin, reference_bin)),
|
| 287 |
+
"jaccard": float(medpy_metric.jc(result_bin, reference_bin)),
|
| 288 |
+
"precision": float(medpy_metric.precision(result_bin, reference_bin)),
|
| 289 |
+
"recall": float(medpy_metric.recall(result_bin, reference_bin)),
|
| 290 |
+
}
|
| 291 |
+
return _ok(metrics)
|
| 292 |
+
except Exception as exc:
|
| 293 |
+
return _err(str(exc))
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
@mcp.tool(name="bounding_box", description="Compute bounding box slices for non-zero voxels in a mask.")
|
| 297 |
+
def bounding_box(mask_path: str) -> Dict[str, Any]:
|
| 298 |
+
"""
|
| 299 |
+
Return an axis-aligned bounding box around non-zero voxels.
|
| 300 |
+
|
| 301 |
+
Parameters
|
| 302 |
+
----------
|
| 303 |
+
mask_path : str
|
| 304 |
+
Input mask image path.
|
| 305 |
+
"""
|
| 306 |
try:
|
| 307 |
+
if medpy_filter is None:
|
| 308 |
+
return _err("medpy.filter is unavailable")
|
| 309 |
+
if np is None:
|
| 310 |
+
return _err("numpy is unavailable")
|
|
|
|
|
|
|
| 311 |
|
| 312 |
+
mask, _ = _load_image(mask_path)
|
| 313 |
+
mask_bin = np.asarray(mask != 0)
|
| 314 |
+
if not np.any(mask_bin):
|
| 315 |
+
return _ok({"bbox": [], "message": "mask is empty"})
|
| 316 |
+
|
| 317 |
+
bbox = medpy_filter.bounding_box(mask_bin)
|
| 318 |
+
result: List[Dict[str, int]] = []
|
| 319 |
+
for idx, item in enumerate(bbox):
|
| 320 |
+
result.append({"axis": idx, "start": int(item.start), "stop": int(item.stop)})
|
| 321 |
+
|
| 322 |
+
return _ok({"bbox": result})
|
| 323 |
+
except Exception as exc:
|
| 324 |
+
return _err(str(exc))
|
| 325 |
|
| 326 |
|
| 327 |
def create_app():
|
|
|
|
| 328 |
return mcp
|
| 329 |
|
| 330 |
+
|
| 331 |
if __name__ == "__main__":
|
| 332 |
+
transport = os.getenv("MCP_TRANSPORT", "stdio").lower()
|
| 333 |
+
port = int(os.getenv("MCP_PORT", "8000"))
|
| 334 |
+
if transport == "http":
|
| 335 |
+
try:
|
| 336 |
+
mcp.run(transport="http", host="0.0.0.0", port=port)
|
| 337 |
+
except TypeError:
|
| 338 |
+
mcp.run()
|
| 339 |
+
else:
|
| 340 |
+
try:
|
| 341 |
+
mcp.run(transport="stdio")
|
| 342 |
+
except TypeError:
|
| 343 |
+
mcp.run()
|
medpy/mcp_output/requirements.txt
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 1 |
fastmcp
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
numpy >= 1.24
|
| 7 |
-
SimpleITK >= 2.1
|
|
|
|
| 1 |
fastmcp
|
| 2 |
+
medpy
|
| 3 |
+
numpy>=1.24
|
| 4 |
+
scipy>=1.10
|
| 5 |
+
SimpleITK>=2.1
|
|
|
|
|
|
medpy/mcp_output/start_mcp.py
CHANGED
|
@@ -1,30 +1,35 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
"""
|
| 3 |
-
MCP Service Startup Entry
|
| 4 |
-
"""
|
| 5 |
-
import sys
|
| 6 |
import os
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
sys.path
|
|
|
|
| 12 |
|
| 13 |
from mcp_service import create_app
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
| 17 |
app = create_app()
|
| 18 |
-
|
| 19 |
-
port = int(os.environ.get("MCP_PORT", "8000"))
|
| 20 |
-
|
| 21 |
-
# Choose transport mode based on environment variable
|
| 22 |
-
transport = os.environ.get("MCP_TRANSPORT", "stdio")
|
| 23 |
if transport == "http":
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
else:
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
if __name__ == "__main__":
|
| 30 |
main()
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import os
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
|
| 7 |
+
PLUGIN_DIR = Path(__file__).resolve().parent / "mcp_plugin"
|
| 8 |
+
if PLUGIN_DIR.exists():
|
| 9 |
+
plugin_str = str(PLUGIN_DIR)
|
| 10 |
+
if plugin_str not in sys.path:
|
| 11 |
+
sys.path.insert(0, plugin_str)
|
| 12 |
|
| 13 |
from mcp_service import create_app
|
| 14 |
|
| 15 |
+
|
| 16 |
+
def main() -> None:
|
| 17 |
+
transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower()
|
| 18 |
+
port = int(os.getenv("MCP_PORT", "8000"))
|
| 19 |
+
|
| 20 |
app = create_app()
|
| 21 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
if transport == "http":
|
| 23 |
+
try:
|
| 24 |
+
app.run(transport="http", host="0.0.0.0", port=port)
|
| 25 |
+
except TypeError:
|
| 26 |
+
app.run()
|
| 27 |
else:
|
| 28 |
+
try:
|
| 29 |
+
app.run(transport="stdio")
|
| 30 |
+
except TypeError:
|
| 31 |
+
app.run()
|
| 32 |
+
|
| 33 |
|
| 34 |
if __name__ == "__main__":
|
| 35 |
main()
|
port.json
CHANGED
|
@@ -1,5 +1 @@
|
|
| 1 |
-
{
|
| 2 |
-
"repo": "medpy",
|
| 3 |
-
"port": 7898,
|
| 4 |
-
"timestamp": 1773273469
|
| 5 |
-
}
|
|
|
|
| 1 |
+
{"port": 7860}
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
fastmcp
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
fastapi
|
| 3 |
-
uvicorn
|
| 4 |
-
pydantic>=2.0.0
|
| 5 |
-
scipy >= 1.10
|
| 6 |
-
numpy >= 1.24
|
| 7 |
-
SimpleITK >= 2.1
|
|
|
|
| 1 |
fastmcp
|
| 2 |
+
medpy
|
| 3 |
+
numpy>=1.24
|
| 4 |
+
scipy>=1.10
|
| 5 |
+
SimpleITK>=2.1
|
| 6 |
fastapi
|
| 7 |
+
uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
run_docker.ps1
CHANGED
|
@@ -1,26 +1,7 @@
|
|
| 1 |
-
cd $PSScriptRoot
|
| 2 |
$ErrorActionPreference = "Stop"
|
| 3 |
-
|
| 4 |
-
$
|
| 5 |
-
$imageName =
|
| 6 |
-
|
| 7 |
-
$mcpPath = Join-Path $mcpDir "mcp.json"
|
| 8 |
-
if (!(Test-Path $mcpDir)) { New-Item -ItemType Directory -Path $mcpDir | Out-Null }
|
| 9 |
-
$config = @{}
|
| 10 |
-
if (Test-Path $mcpPath) {
|
| 11 |
-
try { $config = Get-Content $mcpPath -Raw | ConvertFrom-Json } catch { $config = @{} }
|
| 12 |
-
}
|
| 13 |
-
$serversOrdered = [ordered]@{}
|
| 14 |
-
if ($config -and ($config.PSObject.Properties.Name -contains "mcpServers") -and $config.mcpServers) {
|
| 15 |
-
$existing = $config.mcpServers
|
| 16 |
-
if ($existing -is [pscustomobject]) {
|
| 17 |
-
foreach ($p in $existing.PSObject.Properties) { if ($p.Name -ne $entryName) { $serversOrdered[$p.Name] = $p.Value } }
|
| 18 |
-
} elseif ($existing -is [System.Collections.IDictionary]) {
|
| 19 |
-
foreach ($k in $existing.Keys) { if ($k -ne $entryName) { $serversOrdered[$k] = $existing[$k] } }
|
| 20 |
-
}
|
| 21 |
-
}
|
| 22 |
-
$serversOrdered[$entryName] = @{ url = $entryUrl }
|
| 23 |
-
$config = @{ mcpServers = $serversOrdered }
|
| 24 |
-
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $mcpPath -Encoding UTF8
|
| 25 |
docker build -t $imageName .
|
| 26 |
-
docker run --rm -p
|
|
|
|
|
|
|
| 1 |
$ErrorActionPreference = "Stop"
|
| 2 |
+
|
| 3 |
+
$port = (Get-Content -Raw -Path "port.json" | ConvertFrom-Json).port
|
| 4 |
+
$imageName = "medpy-mcp"
|
| 5 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
docker build -t $imageName .
|
| 7 |
+
docker run --rm -it -p "${port}:${port}" $imageName
|
run_docker.sh
CHANGED
|
@@ -1,75 +1,8 @@
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
set -euo pipefail
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
if command -v python3 >/dev/null 2>&1; then
|
| 10 |
-
python3 - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
|
| 11 |
-
import json, os, sys
|
| 12 |
-
path, name, url = sys.argv[1:4]
|
| 13 |
-
cfg = {"mcpServers": {}}
|
| 14 |
-
if os.path.exists(path):
|
| 15 |
-
try:
|
| 16 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 17 |
-
cfg = json.load(f)
|
| 18 |
-
except Exception:
|
| 19 |
-
cfg = {"mcpServers": {}}
|
| 20 |
-
if not isinstance(cfg, dict):
|
| 21 |
-
cfg = {"mcpServers": {}}
|
| 22 |
-
servers = cfg.get("mcpServers")
|
| 23 |
-
if not isinstance(servers, dict):
|
| 24 |
-
servers = {}
|
| 25 |
-
ordered = {}
|
| 26 |
-
for k, v in servers.items():
|
| 27 |
-
if k != name:
|
| 28 |
-
ordered[k] = v
|
| 29 |
-
ordered[name] = {"url": url}
|
| 30 |
-
cfg = {"mcpServers": ordered}
|
| 31 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 32 |
-
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
| 33 |
-
PY
|
| 34 |
-
elif command -v python >/dev/null 2>&1; then
|
| 35 |
-
python - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
|
| 36 |
-
import json, os, sys
|
| 37 |
-
path, name, url = sys.argv[1:4]
|
| 38 |
-
cfg = {"mcpServers": {}}
|
| 39 |
-
if os.path.exists(path):
|
| 40 |
-
try:
|
| 41 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 42 |
-
cfg = json.load(f)
|
| 43 |
-
except Exception:
|
| 44 |
-
cfg = {"mcpServers": {}}
|
| 45 |
-
if not isinstance(cfg, dict):
|
| 46 |
-
cfg = {"mcpServers": {}}
|
| 47 |
-
servers = cfg.get("mcpServers")
|
| 48 |
-
if not isinstance(servers, dict):
|
| 49 |
-
servers = {}
|
| 50 |
-
ordered = {}
|
| 51 |
-
for k, v in servers.items():
|
| 52 |
-
if k != name:
|
| 53 |
-
ordered[k] = v
|
| 54 |
-
ordered[name] = {"url": url}
|
| 55 |
-
cfg = {"mcpServers": ordered}
|
| 56 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 57 |
-
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
| 58 |
-
PY
|
| 59 |
-
elif command -v jq >/dev/null 2>&1; then
|
| 60 |
-
name="${mcp_entry_name}"; url="${mcp_entry_url}"
|
| 61 |
-
if [ -f "${mcp_path}" ]; then
|
| 62 |
-
tmp="$(mktemp)"
|
| 63 |
-
jq --arg name "$name" --arg url "$url" '
|
| 64 |
-
.mcpServers = (.mcpServers // {})
|
| 65 |
-
| .mcpServers as $s
|
| 66 |
-
| ($s | with_entries(select(.key != $name))) as $base
|
| 67 |
-
| .mcpServers = ($base + {($name): {"url": $url}})
|
| 68 |
-
' "${mcp_path}" > "${tmp}" && mv "${tmp}" "${mcp_path}"
|
| 69 |
-
else
|
| 70 |
-
printf '{ "mcpServers": { "%s": { "url": "%s" } } }
|
| 71 |
-
' "$name" "$url" > "${mcp_path}"
|
| 72 |
-
fi
|
| 73 |
-
fi
|
| 74 |
-
docker build -t medpy-mcp .
|
| 75 |
-
docker run --rm -p 7898:7860 medpy-mcp
|
|
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
PORT=$(python3 -c 'import json; print(json.load(open("port.json", "r", encoding="utf-8"))["port"])')
|
| 5 |
+
IMAGE_NAME="medpy-mcp"
|
| 6 |
+
|
| 7 |
+
docker build -t "$IMAGE_NAME" .
|
| 8 |
+
docker run --rm -it -p "${PORT}:${PORT}" "$IMAGE_NAME"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|