ghh1125 commited on
Commit
0a7e7d6
·
verified ·
1 Parent(s): 611b2e2

Upload 14 files

Browse files
Dockerfile CHANGED
@@ -1,18 +1,23 @@
1
- FROM python:3.10
2
 
3
- RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
- USER user
5
- ENV PATH="/home/user/.local/bin:$PATH"
6
 
7
  WORKDIR /app
8
 
9
- COPY --chown=user ./requirements.txt requirements.txt
10
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
 
 
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: Medpy
3
- emoji: 🏢
4
- colorFrom: green
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 fastapi import FastAPI
 
2
  import os
3
  import sys
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- mcp_plugin_path = os.path.join(os.path.dirname(__file__), "medpy", "mcp_output", "mcp_plugin")
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
- "service": "Medpy MCP Service",
18
- "version": "1.0.0",
19
- "status": "running",
20
- "transport": os.environ.get("MCP_TRANSPORT", "http")
21
  }
22
 
 
23
  @app.get("/health")
24
- def health_check():
25
- return {"status": "healthy", "service": "medpy MCP"}
 
26
 
27
  @app.get("/tools")
28
- def list_tools():
29
- try:
30
- from mcp_service import create_app
31
- mcp_app = create_app()
32
- tools = []
33
- for tool_name, tool_func in mcp_app.tools.items():
34
- tools.append({
35
- "name": tool_name,
36
- "description": tool_func.__doc__ or "No description available"
37
- })
38
- return {"tools": tools}
39
- except Exception as e:
40
- return {"error": f"Failed to load tools: {str(e)}"}
41
-
42
- if __name__ == "__main__":
43
- import uvicorn
44
- port = int(os.environ.get("PORT", 7860))
45
- uvicorn.run(app, host="0.0.0.0", port=port)
 
 
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 (Model Context Protocol) Service README
2
-
3
- ## 1) Project Introduction
4
-
5
- This MCP (Model Context Protocol) service wraps core capabilities from **[medpy](https://github.com/loli/medpy)** for medical image processing workflows.
6
-
7
- It is designed for developers who need programmatic access to:
8
-
9
- - Medical image I/O (load/save + metadata)
10
- - Filtering and preprocessing (resampling, smoothing, morphology)
11
- - Feature extraction (intensity, histogram, texture)
12
- - Segmentation utilities (watershed, graph-cut workflows)
13
- - Evaluation metrics (Dice, Hausdorff, ASD/ASSD, etc.)
14
-
15
- Repository analyzed: `loli/medpy` (Python package with CLI tools and test coverage).
16
-
17
- ---
18
-
19
- ## 2) Installation Method
20
-
21
- ### Requirements
22
-
23
- - Python 3.8+ recommended
24
- - Required:
25
- - `numpy`
26
- - `scipy`
27
- - Optional (depending on formats/features used):
28
- - `SimpleITK`
29
- - `pydicom`
30
- - `nibabel`
31
- - `networkx`
32
- - `maxflow` / `pymaxflow`
33
-
34
- ### Install commands
35
-
36
- - Install MedPy:
37
- - `pip install medpy`
38
- - Or install from source:
39
- - `git clone https://github.com/loli/medpy.git`
40
- - `cd medpy`
41
- - `pip install .`
42
-
43
- If graph-cut functionality is required, ensure a compatible maxflow backend is installed.
44
-
45
- ---
46
-
47
- ## 3) Quick Start
48
-
49
- ### Python API style usage
50
-
51
- - Load image:
52
- - `medpy.io.load(path)` → `(array, header)`
53
- - Save image:
54
- - `medpy.io.save(array, output_path, hdr=header)`
55
- - Resample image:
56
- - `medpy.filter.image.resample(img, hdr, target_spacing, bspline_order=3, mode='nearest')`
57
- - Metrics:
58
- - `medpy.metric.binary.dc(result, reference)` (Dice)
59
- - `medpy.metric.binary.hd95(result, reference, voxelspacing=..., connectivity=1)`
60
-
61
- ### CLI-style workflow examples (service-wrapped)
62
-
63
- - Inspect metadata: `medpy_info`
64
- - Convert image format: `medpy_convert`
65
- - Compare volumes: `medpy_diff`
66
- - Resample volume: `medpy_resample`
67
- - Anisotropic diffusion: `medpy_anisotropic_diffusion`
68
- - Watershed segmentation: `medpy_watershed`
69
- - Graph-cut segmentation: `medpy_graphcut_voxel`, `medpy_graphcut_label`
70
-
71
- In MCP (Model Context Protocol) deployments, these are typically exposed as service endpoints/tools rather than raw shell commands.
72
-
73
- ---
74
-
75
- ## 4) Available Tools and Endpoints List
76
-
77
- Recommended MCP (Model Context Protocol) service endpoints:
78
-
79
- - `image_info`
80
- - Backed by: `medpy_info`
81
- - Returns shape, spacing, datatype, header metadata summary.
82
-
83
- - `image_convert`
84
- - Backed by: `medpy_convert`
85
- - Converts between supported medical image formats.
86
-
87
- - `image_diff`
88
- - Backed by: `medpy_diff`
89
- - Computes/report differences between two images.
90
-
91
- - `image_resample`
92
- - Backed by: `medpy_resample` / `medpy.filter.image.resample`
93
- - Resamples to target voxel spacing or geometry.
94
-
95
- - `anisotropic_diffusion`
96
- - Backed by: `medpy_anisotropic_diffusion` / `medpy.filter.smoothing.anisotropic_diffusion`
97
- - Noise reduction with edge-preserving smoothing.
98
-
99
- - `watershed_segment`
100
- - Backed by: `medpy_watershed`
101
- - Watershed-based segmentation pipeline.
102
-
103
- - `graphcut_voxel_segment`
104
- - Backed by: `medpy_graphcut_voxel`
105
- - Voxel-level graph-cut segmentation.
106
-
107
- - `graphcut_label_segment`
108
- - Backed by: `medpy_graphcut_label`
109
- - Label-based graph-cut segmentation.
110
-
111
- - `segmentation_metrics`
112
- - Backed by: `medpy.metric.binary`
113
- - Dice, Jaccard, Hausdorff, ASD/ASSD, sensitivity/specificity.
114
-
115
- - `feature_extraction`
116
- - Backed by: `medpy.features.intensity|histogram|texture`
117
- - Computes handcrafted radiomics-style feature vectors.
118
-
119
- ---
120
-
121
- ## 5) Common Issues and Notes
122
-
123
- - **Optional dependency gaps**
124
- Some image formats require `SimpleITK`, `pydicom`, or `nibabel`. Missing backends can cause load/save errors.
125
-
126
- - **Graph-cut setup**
127
- Graph-cut tools may fail without `maxflow/pymaxflow` and compatible compiled components.
128
-
129
- - **Metadata consistency**
130
- When resampling/saving, preserve and validate header spacing/origin metadata (`medpy.io.header` helpers).
131
-
132
- - **Large volume performance**
133
- 3D/4D processing can be memory-intensive. Prefer chunk/patch workflows where possible.
134
-
135
- - **Input assumptions**
136
- Many metric functions assume binary masks and aligned geometry. Always verify shape, spacing, and orientation before evaluation.
137
-
138
- - **Import feasibility**
139
- Analysis indicates good integration feasibility (~0.84) with low intrusiveness and medium complexity.
140
-
141
- ---
142
-
143
- ## 6) Reference Links / Documentation
144
-
145
- - Main repository: https://github.com/loli/medpy
146
- - Package docs/readme (in repo): `README.md`, `README_PYPI.md`
147
- - Tests for usage patterns: `tests/`
148
- - CLI scripts: `bin/`
149
- - Core modules:
150
- - `medpy.io`
151
- - `medpy.filter`
152
- - `medpy.features`
153
- - `medpy.metric`
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 os
2
- import sys
3
- import traceback
4
  import importlib
5
- from typing import Any, Dict, List, Optional, Tuple
 
 
 
 
6
 
7
- source_path = os.path.join(
8
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
9
- "source",
10
- )
11
- sys.path.insert(0, source_path)
12
 
13
 
14
  class Adapter:
15
- """
16
- MCP import-mode adapter for the medpy repository.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- This adapter prioritizes direct Python imports from the local `source` tree and
19
- gracefully falls back to a CLI-guidance mode when imports or runtime calls fail.
20
- All public methods return a unified dictionary format with at least:
21
- - status: "success" | "error" | "fallback"
22
- - mode: "import" | "cli"
23
- - data / error / guidance fields where relevant
24
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- # -------------------------------------------------------------------------
27
- # Initialization and module management
28
- # -------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- def __init__(self) -> None:
31
- self.mode = "import"
32
- self._modules: Dict[str, Any] = {}
33
- self._imports: Dict[str, Any] = {}
34
- self._import_errors: Dict[str, str] = {}
35
- self._load_modules()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- def _result(
38
  self,
39
- status: str,
40
- data: Optional[Dict[str, Any]] = None,
41
- error: Optional[str] = None,
42
- guidance: Optional[str] = None,
43
- details: Optional[Dict[str, Any]] = None,
44
  ) -> Dict[str, Any]:
45
- payload: Dict[str, Any] = {"status": status, "mode": self.mode}
46
- if data is not None:
47
- payload["data"] = data
48
- if error is not None:
49
- payload["error"] = error
50
- if guidance is not None:
51
- payload["guidance"] = guidance
52
- if details is not None:
53
- payload["details"] = details
54
- return payload
55
-
56
- def _load_modules(self) -> None:
57
- targets = {
58
- "setup": "setup",
59
- "bin.medpy_reslice_3d_to_4d": "bin.medpy_reslice_3d_to_4d",
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
- value = func(*args, **kwargs)
136
- return self._result(status="success", data={"result": value, "callable": key})
 
 
 
 
 
137
  except Exception as exc:
138
- return self._result(
139
- status="error",
140
- error=f"{type(exc).__name__}: {exc}",
141
- guidance="Check input arguments and required runtime dependencies (e.g., numpy/scipy).",
142
- details={"traceback": traceback.format_exc(), "callable": key},
143
- )
144
-
145
- # -------------------------------------------------------------------------
146
- # setup.py classes and functions
147
- # -------------------------------------------------------------------------
148
-
149
- def create_build_failed(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
150
- """
151
- Create an instance of setup.BuildFailed.
152
-
153
- Parameters:
154
- *args, **kwargs: Forwarded to BuildFailed constructor.
155
-
156
- Returns:
157
- Unified status dictionary with the created instance in data.result.
158
- """
159
- cls = self._imports.get("setup.BuildFailed")
160
- if cls is None:
161
- return self._result(
162
- status="fallback",
163
- error="Class 'setup.BuildFailed' is not available.",
164
- guidance="Ensure setup.py can be imported from source.",
165
- details={"import_errors": self._import_errors},
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
- obj = cls(*args, **kwargs)
198
- return self._result(status="success", data={"result": obj, "class": "setup.ve_build_ext"})
 
 
 
 
 
 
199
  except Exception as exc:
200
- return self._result(
201
- status="error",
202
- error=f"{type(exc).__name__}: {exc}",
203
- guidance="Pass a valid setuptools Distribution object and expected parameters.",
204
- details={"traceback": traceback.format_exc()},
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
- MCP Service Auto-Wrapper - Auto-generated
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
- app = main()
13
- app.run()
 
 
 
 
 
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
- source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
5
- if source_path not in sys.path:
6
- sys.path.insert(0, source_path)
 
 
7
 
8
- from fastmcp import FastMCP
 
 
 
9
 
10
- from setup import run_setup, read, ve_build_ext, try_find_library, BuildFailed
11
- from bin.medpy_reslice_3d_to_4d import main, getArguments, getParser
12
- from bin.medpy_extract_min_max import main, getArguments, getParser
13
- from bin.medpy_convert import getArguments
14
 
15
- mcp = FastMCP("unknown_service")
 
 
 
16
 
 
 
 
 
17
 
18
- @mcp.tool(name="read", description="Auto-wrapped function read")
19
- def read(payload: dict):
20
- try:
21
- if read is None:
22
- return {"success": False, "result": None, "error": "Function read is not available"}
23
- result = read(**payload)
24
- return {"success": True, "result": result, "error": None}
25
- except Exception as e:
26
- return {"success": False, "result": None, "error": str(e)}
27
-
28
- @mcp.tool(name="run_setup", description="Auto-wrapped function run_setup")
29
- def run_setup(payload: dict):
30
- try:
31
- if run_setup is None:
32
- return {"success": False, "result": None, "error": "Function run_setup is not available"}
33
- result = run_setup(**payload)
34
- return {"success": True, "result": result, "error": None}
35
- except Exception as e:
36
- return {"success": False, "result": None, "error": str(e)}
37
-
38
- @mcp.tool(name="try_find_library", description="Auto-wrapped function try_find_library")
39
- def try_find_library(payload: dict):
40
- try:
41
- if try_find_library is None:
42
- return {"success": False, "result": None, "error": "Function try_find_library is not available"}
43
- result = try_find_library(**payload)
44
- return {"success": True, "result": result, "error": None}
45
- except Exception as e:
46
- return {"success": False, "result": None, "error": str(e)}
47
-
48
- @mcp.tool(name="buildfailed", description="BuildFailed class")
49
- def buildfailed(*args, **kwargs):
50
- """BuildFailed class"""
51
- try:
52
- if BuildFailed is None:
53
- return {"success": False, "result": None, "error": "Class BuildFailed is not available, path may need adjustment"}
54
-
55
- # MCP parameter type conversion
56
- converted_args = []
57
- converted_kwargs = kwargs.copy()
58
-
59
- # Handle position argument type conversion
60
- for arg in args:
61
- if isinstance(arg, str):
62
- # Try to convert to numeric type
63
- try:
64
- if '.' in arg:
65
- converted_args.append(float(arg))
66
- else:
67
- converted_args.append(int(arg))
68
- except ValueError:
69
- converted_args.append(arg)
70
- else:
71
- converted_args.append(arg)
72
-
73
- # Handle keyword argument type conversion
74
- for key, value in converted_kwargs.items():
75
- if isinstance(value, str):
76
- try:
77
- if '.' in value:
78
- converted_kwargs[key] = float(value)
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
- if getArguments is None:
134
- return {"success": False, "result": None, "error": "Function getArguments is not available"}
135
- result = getArguments(**payload)
136
- return {"success": True, "result": result, "error": None}
137
- except Exception as e:
138
- return {"success": False, "result": None, "error": str(e)}
139
-
140
- @mcp.tool(name="getParser", description="Auto-wrapped function getParser")
141
- def getParser(payload: dict):
 
 
 
 
 
 
142
  try:
143
- if getParser is None:
144
- return {"success": False, "result": None, "error": "Function getParser is not available"}
145
- result = getParser(**payload)
146
- return {"success": True, "result": result, "error": None}
147
- except Exception as e:
148
- return {"success": False, "result": None, "error": str(e)}
149
-
150
- @mcp.tool(name="main", description="Auto-wrapped function main")
151
- def main(payload: dict):
 
 
 
 
 
 
152
  try:
153
- if main is None:
154
- return {"success": False, "result": None, "error": "Function main is not available"}
155
- result = main(**payload)
156
- return {"success": True, "result": result, "error": None}
157
- except Exception as e:
158
- return {"success": False, "result": None, "error": str(e)}
159
-
160
- @mcp.tool(name="getArguments", description="Auto-wrapped function getArguments")
161
- def getArguments(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  try:
163
- if getArguments is None:
164
- return {"success": False, "result": None, "error": "Function getArguments is not available"}
165
- result = getArguments(**payload)
166
- return {"success": True, "result": result, "error": None}
167
- except Exception as e:
168
- return {"success": False, "result": None, "error": str(e)}
169
-
170
- @mcp.tool(name="getParser", description="Auto-wrapped function getParser")
171
- def getParser(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  try:
173
- if getParser is None:
174
- return {"success": False, "result": None, "error": "Function getParser is not available"}
175
- result = getParser(**payload)
176
- return {"success": True, "result": result, "error": None}
177
- except Exception as e:
178
- return {"success": False, "result": None, "error": str(e)}
179
-
180
- @mcp.tool(name="main", description="Auto-wrapped function main")
181
- def main(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  try:
183
- if main is None:
184
- return {"success": False, "result": None, "error": "Function main is not available"}
185
- result = main(**payload)
186
- return {"success": True, "result": result, "error": None}
187
- except Exception as e:
188
- return {"success": False, "result": None, "error": str(e)}
189
-
190
- @mcp.tool(name="getArguments", description="Auto-wrapped function getArguments")
191
- def getArguments(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  try:
193
- if getArguments is None:
194
- return {"success": False, "result": None, "error": "Function getArguments is not available"}
195
- result = getArguments(**payload)
196
- return {"success": True, "result": result, "error": None}
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
- mcp.run(transport="http", host="0.0.0.0", port=8000)
 
 
 
 
 
 
 
 
 
 
 
 
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
- fastapi
3
- uvicorn[standard]
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
 
 
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
- project_root = os.path.dirname(os.path.abspath(__file__))
9
- mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
- if mcp_plugin_dir not in sys.path:
11
- sys.path.insert(0, mcp_plugin_dir)
 
12
 
13
  from mcp_service import create_app
14
 
15
- def main():
16
- """Start FastMCP service"""
 
 
 
17
  app = create_app()
18
- # Use environment variable to configure port, default 8000
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
- app.run(transport="http", host="0.0.0.0", port=port)
 
 
 
25
  else:
26
- # Default to STDIO mode
27
- app.run()
 
 
 
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[standard]
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
- $entryName = if ($env:MCP_ENTRY_NAME) { $env:MCP_ENTRY_NAME } else { "medpy" }
4
- $entryUrl = if ($env:MCP_ENTRY_URL) { $env:MCP_ENTRY_URL } else { "http://localhost:7898/mcp" }
5
- $imageName = if ($env:MCP_IMAGE_NAME) { $env:MCP_IMAGE_NAME } else { "medpy-mcp" }
6
- $mcpDir = Join-Path $env:USERPROFILE ".cursor"
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 7898:7860 $imageName
 
 
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
- cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
4
- mcp_entry_name="${MCP_ENTRY_NAME:-medpy}"
5
- mcp_entry_url="${MCP_ENTRY_URL:-http://localhost:7898/mcp}"
6
- mcp_dir="${HOME}/.cursor"
7
- mcp_path="${mcp_dir}/mcp.json"
8
- mkdir -p "${mcp_dir}"
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"