ghh1125 commited on
Commit
a66d269
·
verified ·
1 Parent(s): a44b094

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", "MONAI/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 MONAI /app/MONAI
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
+ ENTRYPOINT ["python", "MONAI/mcp_output/start_mcp.py"]
MONAI/mcp_output/README_MCP.md CHANGED
@@ -1,145 +1,151 @@
1
- # MONAI MCP (Model Context Protocol) Service README
2
-
3
- ## 1) Project Introduction
4
-
5
- This MCP (Model Context Protocol) service exposes practical, high-level MONAI capabilities for medical imaging workflows, with a focus on:
6
-
7
- - Bundle-based workflow execution (train/infer/evaluate from config)
8
- - Config parsing and validation
9
- - Sliding-window inference for large 2D/3D images
10
- - Optional access to model/dataset pipeline primitives (networks, transforms, datasets)
11
-
12
- Best-fit usage: integrating MONAI automation into LLM-driven developer tooling with minimal custom glue code.
13
-
14
- ---
15
-
16
- ## 2) Installation Method
17
-
18
- ### Requirements
19
-
20
- - Python >= 3.9
21
- - Core:
22
- - torch
23
- - numpy
24
- - monai
25
- - Common optional dependencies (based on your workflow):
26
- - nibabel, SimpleITK, pydicom, itk
27
- - scipy, pillow, scikit-image
28
- - ignite
29
- - mlflow, tensorboard
30
- - opencv-python, cucim, zarr, einops
31
-
32
- ### Install (minimal)
33
-
34
- pip install monai torch numpy
35
-
36
- ### Install (typical medical imaging stack)
37
-
38
- pip install monai[all]
39
-
40
- If your environment does not resolve extras reliably, install optional packages explicitly per feature needs.
41
-
42
- ---
43
-
44
- ## 3) Quick Start
45
-
46
- ### A. Run a MONAI bundle workflow
47
-
48
- Use the Bundle orchestration layer as the primary MCP (Model Context Protocol) surface:
49
-
50
- - `monai.bundle.scripts.run(...)`
51
- - `monai.bundle.scripts.verify_metadata(...)`
52
- - `monai.bundle.scripts.verify_net_in_out(...)`
53
- - `monai.bundle.scripts.download(...)`
54
- - `monai.bundle.scripts.init_bundle(...)`
55
-
56
- Typical MCP (Model Context Protocol) flow:
57
- 1. Download/init bundle
58
- 2. Parse/resolve config
59
- 3. Verify metadata/network I/O
60
- 4. Run train/infer command
61
-
62
- ### B. Parse and inspect config safely
63
-
64
- Use `monai.bundle.config_parser.ConfigParser` and:
65
- - `load_config_file(...)`
66
- - `parse(...)`
67
- - `get_parsed_content(...)`
68
-
69
- This is the recommended path for declarative, controlled config access from tools/services.
70
-
71
- ### C. Sliding-window inference
72
-
73
- For large volumes/WSI-style inputs:
74
- - `monai.inferers.inferer.sliding_window_inference(...)`
75
- - Or class-based inferers: `SlidingWindowInferer`, `SimpleInferer`
76
-
77
- ---
78
-
79
- ## 4) Available Tools and Endpoints List
80
-
81
- Suggested MCP (Model Context Protocol) endpoints for a practical service layer:
82
-
83
- 1. `bundle.run`
84
- - Execute a MONAI bundle workflow from config overrides.
85
- - Backend: `monai.bundle.scripts.run`
86
-
87
- 2. `bundle.verify_metadata`
88
- - Validate bundle metadata schema/compliance.
89
- - Backend: `monai.bundle.scripts.verify_metadata`
90
-
91
- 3. `bundle.verify_net_io`
92
- - Check model input/output shape and compatibility.
93
- - Backend: `monai.bundle.scripts.verify_net_in_out`
94
-
95
- 4. `bundle.download`
96
- - Download model bundle assets.
97
- - Backend: `monai.bundle.scripts.download`
98
-
99
- 5. `bundle.init`
100
- - Initialize a new bundle scaffold.
101
- - Backend: `monai.bundle.scripts.init_bundle`
102
-
103
- 6. `config.parse`
104
- - Load and resolve MONAI config content.
105
- - Backend: `ConfigParser.load_config_file/parse/get_parsed_content`
106
-
107
- 7. `infer.sliding_window`
108
- - Perform patch-based inference over large tensors/images.
109
- - Backend: `sliding_window_inference`
110
-
111
- 8. `system.health`
112
- - Environment and dependency diagnostics (Python/Torch/MONAI/CUDA availability).
113
-
114
- ---
115
-
116
- ## 5) Common Issues and Notes
117
-
118
- - Dependency mismatches:
119
- - Many MONAI features are optional-dependency driven. Missing readers/transforms usually mean missing packages (e.g., nibabel/SimpleITK/itk).
120
- - GPU/CUDA issues:
121
- - Ensure Torch CUDA build matches your driver/runtime.
122
- - Large-volume inference performance:
123
- - Tune ROI size, overlap, batch size, and device placement for `sliding_window_inference`.
124
- - Config complexity:
125
- - Prefer parser-based validation before runtime execution.
126
- - Heavy automation modules:
127
- - `Auto3DSeg`/`nnUNet` paths are powerful but dependency-sensitive and compute-heavy.
128
- - Reproducibility:
129
- - Pin MONAI + Torch versions and keep bundle configs versioned.
130
-
131
- ---
132
-
133
- ## 6) Reference Links / Documentation
134
-
135
- - MONAI repository: https://github.com/Project-MONAI/MONAI
136
- - MONAI docs (installation/modules/config): `docs/source/` in repository
137
- - Bundle entrypoint: `python -m monai.bundle`
138
- - Auto3DSeg entrypoint: `python -m monai.apps.auto3dseg`
139
- - nnUNet entrypoint: `python -m monai.apps.nnunet`
140
- - Key modules:
141
- - `monai.bundle.scripts`
142
- - `monai.bundle.config_parser`
143
- - `monai.inferers.inferer`
144
- - `monai.transforms.compose`
145
- - `monai.networks.nets`
 
 
 
 
 
 
 
1
+ # MONAI MCP Plugin
2
+
3
+ ## Overview
4
+ This MCP plugin exposes core MONAI capabilities through FastMCP tools.
5
+ It supports `stdio` transport for local agent/desktop usage and `http` transport for service deployment.
6
+
7
+ ## Exposed Tools
8
+
9
+ ### 1) `health_check`
10
+ - Parameters: none
11
+ - Returns dependency availability (`fastmcp`, `monai`, `torch`, `numpy`) and adapter health.
12
+ - Example:
13
+ ```json
14
+ {"name": "health_check", "arguments": {}}
15
+ ```
16
+
17
+ ### 2) `list_modules`
18
+ - Parameters:
19
+ - `include_failed` (bool, default `false`)
20
+ - `limit` (int, default `100`)
21
+ - Lists MONAI modules loaded by adapter.
22
+ - Example:
23
+ ```json
24
+ {"name": "list_modules", "arguments": {"include_failed": true, "limit": 50}}
25
+ ```
26
+
27
+ ### 3) `list_symbols`
28
+ - Parameters:
29
+ - `module_name` (str)
30
+ - `public_only` (bool, default `true`)
31
+ - `limit` (int, default `100`)
32
+ - Lists exported symbols for a module.
33
+ - Example:
34
+ ```json
35
+ {"name": "list_symbols", "arguments": {"module_name": "monai.transforms"}}
36
+ ```
37
+
38
+ ### 4) `list_network_architectures`
39
+ - Parameters:
40
+ - `limit` (int, default `80`)
41
+ - Lists architecture classes from `monai.networks.nets`.
42
+ - Example:
43
+ ```json
44
+ {"name": "list_network_architectures", "arguments": {"limit": 40}}
45
+ ```
46
+
47
+ ### 5) `create_network`
48
+ - Parameters:
49
+ - `network_name` (str)
50
+ - `spatial_dims` (int)
51
+ - `in_channels` (int)
52
+ - `out_channels` (int)
53
+ - `channels_json` (str JSON list)
54
+ - `strides_json` (str JSON list)
55
+ - Creates a MONAI network instance and returns summary.
56
+ - Example:
57
+ ```json
58
+ {
59
+ "name": "create_network",
60
+ "arguments": {
61
+ "network_name": "UNet",
62
+ "spatial_dims": 3,
63
+ "in_channels": 1,
64
+ "out_channels": 2,
65
+ "channels_json": "[16,32,64,128]",
66
+ "strides_json": "[2,2,2]"
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### 6) `build_transform_pipeline`
72
+ - Parameters:
73
+ - `transform_names_json` (str JSON list)
74
+ - Creates a MONAI `Compose` pipeline using zero-argument transform constructors.
75
+ - Example:
76
+ ```json
77
+ {
78
+ "name": "build_transform_pipeline",
79
+ "arguments": {
80
+ "transform_names_json": "[\"EnsureChannelFirst\", \"ScaleIntensity\"]"
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### 7) `call_function`
86
+ - Parameters:
87
+ - `module_name` (str)
88
+ - `function_name` (str)
89
+ - `args_json` (str JSON list, default `[]`)
90
+ - `kwargs_json` (str JSON object, default `{}`)
91
+ - Calls a function through adapter reflection.
92
+ - Example:
93
+ ```json
94
+ {
95
+ "name": "call_function",
96
+ "arguments": {
97
+ "module_name": "monai.data",
98
+ "function_name": "decollate_batch",
99
+ "args_json": "[]",
100
+ "kwargs_json": "{}"
101
+ }
102
+ }
103
+ ```
104
+
105
+ ### 8) `create_instance`
106
+ - Parameters:
107
+ - `module_name` (str)
108
+ - `class_name` (str)
109
+ - `args_json` (str JSON list, default `[]`)
110
+ - `kwargs_json` (str JSON object, default `{}`)
111
+ - Creates a class instance through adapter reflection.
112
+ - Example:
113
+ ```json
114
+ {
115
+ "name": "create_instance",
116
+ "arguments": {
117
+ "module_name": "monai.losses",
118
+ "class_name": "DiceLoss",
119
+ "args_json": "[]",
120
+ "kwargs_json": "{}"
121
+ }
122
+ }
123
+ ```
124
+
125
+ ## Run Locally (stdio)
126
+
127
+ From `mcp_output` directory:
128
+
129
+ ```bash
130
+ python mcp_plugin/main.py
131
+ ```
132
+
133
+ Or with launcher:
134
+
135
+ ```bash
136
+ MCP_TRANSPORT=stdio python start_mcp.py
137
+ ```
138
+
139
+ ## Run via HTTP Transport
140
+
141
+ ```bash
142
+ MCP_TRANSPORT=http MCP_PORT=7860 python start_mcp.py
143
+ ```
144
+
145
+ Clients should connect to:
146
+
147
+ - `http://localhost:7860/mcp`
148
+
149
+ ## Notes
150
+ - The plugin prepends local `source/` path (relative to deployment package) to `sys.path`.
151
+ - If MONAI dependencies are missing, tools return structured error responses without crashing.
MONAI/mcp_output/mcp_plugin/adapter.py CHANGED
@@ -1,261 +1,195 @@
1
- import os
 
 
 
2
  import sys
3
- from typing import Any, Dict, Optional
 
4
 
5
- source_path = os.path.join(
6
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
7
- "source",
8
- )
9
- sys.path.insert(0, source_path)
10
 
11
 
12
  class Adapter:
13
- """
14
- MCP Import Mode Adapter for MONAI repository integration.
15
-
16
- This adapter prioritizes direct Python imports from the local `source` path and
17
- provides CLI fallback hints when imports are unavailable. All public methods
18
- return a unified dictionary with at least a `status` field.
19
- """
20
-
21
- def __init__(self) -> None:
22
- self.mode = "import"
23
- self._imports: Dict[str, Any] = {}
24
- self._errors: Dict[str, str] = {}
25
- self._initialize_imports()
26
-
27
- # -------------------------------------------------------------------------
28
- # Internal helpers
29
- # -------------------------------------------------------------------------
30
- def _result(self, status: str, **kwargs: Any) -> Dict[str, Any]:
31
- payload = {"status": status}
32
- payload.update(kwargs)
33
- return payload
34
-
35
- def _safe_import(self, key: str, module_path: str, attr_name: Optional[str] = None) -> None:
36
  try:
37
- module = __import__(module_path, fromlist=[attr_name] if attr_name else [])
38
- self._imports[key] = getattr(module, attr_name) if attr_name else module
39
  except Exception as exc:
40
- self._errors[key] = (
41
- f"Failed to import '{module_path}'"
42
- + (f".'{attr_name}'" if attr_name else "")
43
- + f": {exc}. Ensure repository source is present at '{source_path}' and dependencies are installed."
44
- )
45
-
46
- def _initialize_imports(self) -> None:
47
- self._safe_import("bundle_main", "monai.bundle.__main__")
48
- self._safe_import("auto3dseg_main", "monai.apps.auto3dseg.__main__")
49
- self._safe_import("nnunet_main", "monai.apps.nnunet.__main__")
50
-
51
- def _get_import(self, key: str) -> Dict[str, Any]:
52
- if key in self._imports:
53
- return self._result("success", object=self._imports[key], mode=self.mode)
54
- return self._result(
55
- "error",
56
- mode=self.mode,
57
- message=self._errors.get(
58
- key,
59
- f"Import key '{key}' is not available. Verify source path and optional dependencies.",
60
- ),
61
- fallback="Use CLI entrypoints if imports are unavailable.",
62
- )
63
-
64
- # -------------------------------------------------------------------------
65
- # Adapter status and diagnostics
66
- # -------------------------------------------------------------------------
67
- def health_check(self) -> Dict[str, Any]:
68
- """
69
- Check adapter readiness and imported module availability.
70
-
71
- Returns:
72
- Dict[str, Any]:
73
- {
74
- "status": "success" | "partial" | "error",
75
- "mode": "import",
76
- "available": [...],
77
- "failed": {...},
78
- "guidance": str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
80
- """
81
- available = sorted(self._imports.keys())
82
- failed = dict(self._errors)
83
- if available and not failed:
84
- status = "success"
85
- elif available and failed:
86
- status = "partial"
87
- else:
88
- status = "error"
89
- return self._result(
90
- status,
91
- mode=self.mode,
92
- available=available,
93
- failed=failed,
94
- guidance=(
95
- "Install required dependencies (python>=3.9, torch, numpy) and optional MONAI extras "
96
- "if specific modules fail."
97
- ),
98
- )
99
-
100
- # -------------------------------------------------------------------------
101
- # Module accessors (instance methods for identified modules/classes scope)
102
- # -------------------------------------------------------------------------
103
- def get_bundle_main_module(self) -> Dict[str, Any]:
104
- """
105
- Get imported module object for MONAI Bundle CLI entrypoint.
106
-
107
- Returns:
108
- Dict[str, Any]:
109
- status plus module object under `object` if available.
110
- """
111
- return self._get_import("bundle_main")
112
-
113
- def get_auto3dseg_main_module(self) -> Dict[str, Any]:
114
- """
115
- Get imported module object for MONAI Auto3DSeg CLI entrypoint.
116
-
117
- Returns:
118
- Dict[str, Any]:
119
- status plus module object under `object` if available.
120
- """
121
- return self._get_import("auto3dseg_main")
122
-
123
- def get_nnunet_main_module(self) -> Dict[str, Any]:
124
- """
125
- Get imported module object for MONAI nnUNet CLI entrypoint.
126
 
127
- Returns:
128
- Dict[str, Any]:
129
- status plus module object under `object` if available.
130
- """
131
- return self._get_import("nnunet_main")
132
-
133
- # -------------------------------------------------------------------------
134
- # CLI-aligned call methods (function-equivalent wrappers)
135
- # -------------------------------------------------------------------------
136
- def call_bundle_cli(self, args: Optional[list] = None) -> Dict[str, Any]:
137
- """
138
- Execute MONAI Bundle CLI via imported module when possible.
139
-
140
- Parameters:
141
- args (Optional[list]): Command-line style arguments. If None, defaults to [].
142
-
143
- Returns:
144
- Dict[str, Any]:
145
- Unified status dictionary with execution result or actionable fallback guidance.
146
- """
147
- args = args or []
148
- mod_res = self._get_import("bundle_main")
149
- if mod_res["status"] != "success":
150
- return self._result(
151
- "error",
152
- mode=self.mode,
153
- message=mod_res["message"],
154
- fallback="Run manually: python -m monai.bundle <args>",
155
- )
156
  try:
157
- module = mod_res["object"]
158
- if hasattr(module, "main") and callable(module.main):
159
- rc = module.main(args)
160
- return self._result("success", mode=self.mode, return_code=rc, entrypoint="python -m monai.bundle")
161
- return self._result(
162
- "error",
163
- mode=self.mode,
164
- message="Imported module does not expose a callable 'main(args)'.",
165
- fallback="Run manually: python -m monai.bundle <args>",
166
- )
167
  except Exception as exc:
168
- return self._result(
169
- "error",
170
- mode=self.mode,
171
- message=f"Bundle CLI execution failed: {exc}. Check argument syntax and required runtime dependencies.",
172
- fallback="Run manually: python -m monai.bundle <args>",
173
- )
174
-
175
- def call_auto3dseg_cli(self, args: Optional[list] = None) -> Dict[str, Any]:
176
- """
177
- Execute MONAI Auto3DSeg CLI via imported module when possible.
178
-
179
- Parameters:
180
- args (Optional[list]): Command-line style arguments. If None, defaults to [].
181
-
182
- Returns:
183
- Dict[str, Any]:
184
- Unified status dictionary with execution result or fallback guidance.
185
- """
186
  args = args or []
187
- mod_res = self._get_import("auto3dseg_main")
188
- if mod_res["status"] != "success":
189
- return self._result(
190
- "error",
191
- mode=self.mode,
192
- message=mod_res["message"],
193
- fallback="Run manually: python -m monai.apps.auto3dseg <args>",
194
- )
195
  try:
196
- module = mod_res["object"]
197
- if hasattr(module, "main") and callable(module.main):
198
- rc = module.main(args)
199
- return self._result(
200
- "success",
201
- mode=self.mode,
202
- return_code=rc,
203
- entrypoint="python -m monai.apps.auto3dseg",
204
- )
205
- return self._result(
206
- "error",
207
- mode=self.mode,
208
- message="Imported module does not expose a callable 'main(args)'.",
209
- fallback="Run manually: python -m monai.apps.auto3dseg <args>",
210
- )
211
  except Exception as exc:
212
- return self._result(
213
- "error",
214
- mode=self.mode,
215
- message=f"Auto3DSeg CLI execution failed: {exc}. Validate dataset/config paths and dependencies.",
216
- fallback="Run manually: python -m monai.apps.auto3dseg <args>",
217
- )
 
 
 
 
 
 
 
 
 
218
 
219
- def call_nnunet_cli(self, args: Optional[list] = None) -> Dict[str, Any]:
220
- """
221
- Execute MONAI nnUNet CLI via imported module when possible.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
- Parameters:
224
- args (Optional[list]): Command-line style arguments. If None, defaults to [].
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
- Returns:
227
- Dict[str, Any]:
228
- Unified status dictionary with execution result or fallback guidance.
229
- """
230
- args = args or []
231
- mod_res = self._get_import("nnunet_main")
232
- if mod_res["status"] != "success":
233
- return self._result(
234
- "error",
235
- mode=self.mode,
236
- message=mod_res["message"],
237
- fallback="Run manually: python -m monai.apps.nnunet <args>",
238
- )
239
  try:
240
- module = mod_res["object"]
241
- if hasattr(module, "main") and callable(module.main):
242
- rc = module.main(args)
243
- return self._result(
244
- "success",
245
- mode=self.mode,
246
- return_code=rc,
247
- entrypoint="python -m monai.apps.nnunet",
248
- )
249
- return self._result(
250
- "error",
251
- mode=self.mode,
252
- message="Imported module does not expose a callable 'main(args)'.",
253
- fallback="Run manually: python -m monai.apps.nnunet <args>",
254
- )
255
  except Exception as exc:
256
- return self._result(
257
- "error",
258
- mode=self.mode,
259
- message=f"nnUNet CLI execution failed: {exc}. Confirm nnUNet configuration and dependencies.",
260
- fallback="Run manually: python -m monai.apps.nnunet <args>",
261
- )
 
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import pkgutil
5
  import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
 
9
+ SOURCE_DIR = Path(__file__).resolve().parents[2] / "source"
10
+ if SOURCE_DIR.exists():
11
+ source_dir_str = str(SOURCE_DIR)
12
+ if source_dir_str not in sys.path:
13
+ sys.path.insert(0, source_dir_str)
14
 
15
 
16
  class Adapter:
17
+ def __init__(self, target_package: str = "monai") -> None:
18
+ self.target_package = target_package
19
+ self.mode = "normal"
20
+ self._root_module = None
21
+ self._loaded_modules: dict[str, Any] = {}
22
+ self._failed_modules: dict[str, str] = {}
23
+ self._bootstrap()
24
+
25
+ def _bootstrap(self) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  try:
27
+ self._root_module = importlib.import_module(self.target_package)
28
+ self._loaded_modules[self.target_package] = self._root_module
29
  except Exception as exc:
30
+ self.mode = "blackbox"
31
+ self._failed_modules[self.target_package] = str(exc)
32
+ return
33
+
34
+ if not hasattr(self._root_module, "__path__"):
35
+ return
36
+
37
+ for module_info in pkgutil.walk_packages(self._root_module.__path__, prefix=f"{self.target_package}."):
38
+ module_name = module_info.name
39
+ try:
40
+ loaded = importlib.import_module(module_name)
41
+ self._loaded_modules[module_name] = loaded
42
+ except Exception as exc:
43
+ self._failed_modules[module_name] = str(exc)
44
+
45
+ if not self._loaded_modules:
46
+ self.mode = "blackbox"
47
+
48
+ def _status(self) -> str:
49
+ if self.mode == "blackbox":
50
+ return "fallback"
51
+ return "ok"
52
+
53
+ def health(self) -> dict[str, Any]:
54
+ return {
55
+ "status": self._status(),
56
+ "mode": self.mode,
57
+ "target_package": self.target_package,
58
+ "loaded_count": len(self._loaded_modules),
59
+ "failed_count": len(self._failed_modules),
60
+ "loaded_root": self.target_package in self._loaded_modules,
61
+ }
62
+
63
+ def list_modules(self, include_failed: bool = False, limit: int = 200) -> dict[str, Any]:
64
+ loaded = sorted(self._loaded_modules.keys())[: max(limit, 0)]
65
+ result: dict[str, Any] = {
66
+ "status": self._status(),
67
+ "mode": self.mode,
68
+ "loaded_modules": loaded,
69
+ "loaded_count": len(self._loaded_modules),
70
+ }
71
+ if include_failed:
72
+ failed_items = sorted(self._failed_modules.items(), key=lambda x: x[0])[: max(limit, 0)]
73
+ result["failed_modules"] = [{"module": k, "error": v} for k, v in failed_items]
74
+ result["failed_count"] = len(self._failed_modules)
75
+ return result
76
+
77
+ def list_symbols(self, module_name: str, public_only: bool = True, limit: int = 200) -> dict[str, Any]:
78
+ module = self._loaded_modules.get(module_name)
79
+ if module is None:
80
+ try:
81
+ module = importlib.import_module(module_name)
82
+ except Exception as exc:
83
+ return {
84
+ "status": "error",
85
+ "module": module_name,
86
+ "error": f"failed to import module: {exc}",
87
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  try:
90
+ symbols = dir(module)
91
+ if public_only:
92
+ symbols = [s for s in symbols if not s.startswith("_")]
93
+ symbols = sorted(symbols)[: max(limit, 0)]
94
+ return {
95
+ "status": "ok",
96
+ "module": module_name,
97
+ "symbols": symbols,
98
+ "count": len(symbols),
99
+ }
100
  except Exception as exc:
101
+ return {
102
+ "status": "error",
103
+ "module": module_name,
104
+ "error": str(exc),
105
+ }
106
+
107
+ def call_function(
108
+ self,
109
+ module_name: str,
110
+ function_name: str,
111
+ args: list[Any] | None = None,
112
+ kwargs: dict[str, Any] | None = None,
113
+ ) -> dict[str, Any]:
 
 
 
 
 
114
  args = args or []
115
+ kwargs = kwargs or {}
116
+
 
 
 
 
 
 
117
  try:
118
+ module = importlib.import_module(module_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  except Exception as exc:
120
+ return {
121
+ "status": "error",
122
+ "module": module_name,
123
+ "function": function_name,
124
+ "error": f"failed to import module: {exc}",
125
+ }
126
+
127
+ fn = getattr(module, function_name, None)
128
+ if fn is None or not callable(fn):
129
+ return {
130
+ "status": "error",
131
+ "module": module_name,
132
+ "function": function_name,
133
+ "error": "function not found or not callable",
134
+ }
135
 
136
+ try:
137
+ value = fn(*args, **kwargs)
138
+ return {
139
+ "status": "ok",
140
+ "module": module_name,
141
+ "function": function_name,
142
+ "result": value,
143
+ }
144
+ except Exception as exc:
145
+ return {
146
+ "status": "error",
147
+ "module": module_name,
148
+ "function": function_name,
149
+ "error": str(exc),
150
+ }
151
+
152
+ def create_instance(
153
+ self,
154
+ module_name: str,
155
+ class_name: str,
156
+ args: list[Any] | None = None,
157
+ kwargs: dict[str, Any] | None = None,
158
+ ) -> dict[str, Any]:
159
+ args = args or []
160
+ kwargs = kwargs or {}
161
 
162
+ try:
163
+ module = importlib.import_module(module_name)
164
+ except Exception as exc:
165
+ return {
166
+ "status": "error",
167
+ "module": module_name,
168
+ "class": class_name,
169
+ "error": f"failed to import module: {exc}",
170
+ }
171
+
172
+ cls = getattr(module, class_name, None)
173
+ if cls is None:
174
+ return {
175
+ "status": "error",
176
+ "module": module_name,
177
+ "class": class_name,
178
+ "error": "class not found",
179
+ }
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  try:
182
+ instance = cls(*args, **kwargs)
183
+ return {
184
+ "status": "ok",
185
+ "module": module_name,
186
+ "class": class_name,
187
+ "instance": instance,
188
+ }
 
 
 
 
 
 
 
 
189
  except Exception as exc:
190
+ return {
191
+ "status": "error",
192
+ "module": module_name,
193
+ "class": class_name,
194
+ "error": str(exc),
195
+ }
MONAI/mcp_output/mcp_plugin/main.py CHANGED
@@ -1,13 +1,9 @@
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
+ # Local stdio entry point only (Claude Desktop / CLI). Not used for web/Docker deployment.
4
  from mcp_service import create_app
5
 
 
 
 
 
6
 
7
  if __name__ == "__main__":
8
+ app = create_app()
9
+ app.run()
MONAI/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,201 +1,354 @@
1
- import os
 
 
2
  import sys
 
3
  from typing import Any
4
 
5
- source_path = os.path.join(
6
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
7
- "source",
8
- )
9
- if source_path not in sys.path:
10
- sys.path.insert(0, source_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- from fastmcp import FastMCP
13
- import monai
 
 
14
 
 
 
 
 
 
 
15
 
16
- mcp = FastMCP("monai_service")
17
 
 
 
18
 
19
- def _ok(result: Any) -> dict:
 
 
 
 
 
20
  return {"success": True, "result": result, "error": None}
21
 
22
 
23
- def _err(message: str) -> dict:
24
  return {"success": False, "result": None, "error": message}
25
 
26
 
27
- @mcp.tool(name="monai_version", description="Get MONAI version and basic environment info.")
28
- def monai_version() -> dict:
29
- """
30
- Return MONAI version details.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  Returns:
33
- dict: Standard response dictionary with version and module path details.
34
  """
35
- try:
36
- result = {
37
- "version": getattr(monai, "__version__", "unknown"),
38
- "module": monai.__name__,
39
- "file": getattr(monai, "__file__", None),
 
 
 
 
 
 
40
  }
41
- return _ok(result)
42
- except Exception as exc:
43
- return _err(str(exc))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
 
46
- @mcp.tool(name="bundle_run", description="Run MONAI bundle CLI-style commands programmatically.")
47
- def bundle_run(args: str) -> dict:
 
 
 
 
 
 
 
 
 
48
  """
49
- Execute MONAI bundle script entry with argument string.
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  Parameters:
52
- args (str): CLI-like argument string, e.g. 'run --config_file config.json'.
53
 
54
  Returns:
55
- dict: Standard response dictionary with script return value.
56
  """
57
- try:
58
- from monai.bundle.scripts import run as bundle_run_fn
59
 
60
- argv = [a for a in args.split(" ") if a.strip()]
61
- out = bundle_run_fn(argv)
62
- return _ok(out)
 
 
 
 
 
 
 
 
63
  except Exception as exc:
64
  return _err(str(exc))
65
 
66
 
67
- @mcp.tool(name="auto3dseg_run", description="Run Auto3DSeg pipeline from MONAI apps.")
68
- def auto3dseg_run(
69
- input_data_config: str,
70
- work_dir: str,
71
- algorithms: str = "",
72
- templates_path_or_url: str = "",
73
- ) -> dict:
74
- """
75
- Launch an Auto3DSeg run using MONAI AutoRunner.
 
76
 
77
  Parameters:
78
- input_data_config (str): Path to data list/config JSON.
79
- work_dir (str): Working directory for outputs.
80
- algorithms (str): Comma-separated algorithm names to include.
81
- templates_path_or_url (str): Optional local path or URL for templates.
 
 
82
 
83
  Returns:
84
- dict: Standard response dictionary with run status/output.
85
  """
 
 
 
86
  try:
87
- from monai.apps.auto3dseg.auto_runner import AutoRunner
88
-
89
- algos = [a.strip() for a in algorithms.split(",") if a.strip()] or None
90
- runner = AutoRunner(
91
- input=input_data_config,
92
- work_dir=work_dir,
93
- algos=algos,
94
- templates_path_or_url=templates_path_or_url or None,
 
 
 
 
 
 
 
 
 
 
 
 
95
  )
96
- output = runner.run()
97
- return _ok(output)
98
  except Exception as exc:
99
  return _err(str(exc))
100
 
101
 
102
- @mcp.tool(name="nnunetv2_run", description="Run MONAI nnUNetV2 workflow wrapper.")
103
- def nnunetv2_run(
104
- input_config: str,
105
- work_dir: str,
106
- datalist: str = "",
107
- fold: int = 0,
108
- ) -> dict:
109
- """
110
- Execute nnUNetV2 runner from MONAI apps.
111
 
112
  Parameters:
113
- input_config (str): Path to nnUNetV2 config file.
114
- work_dir (str): Working directory.
115
- datalist (str): Optional datalist path.
116
- fold (int): Fold index.
117
 
118
  Returns:
119
- dict: Standard response dictionary with runner output.
120
  """
121
- try:
122
- from monai.apps.nnunet.nnunetv2_runner import NNUNetV2Runner
123
 
124
- runner = NNUNetV2Runner(
125
- input_config=input_config,
126
- work_dir=work_dir,
127
- datalist=datalist or None,
128
- fold=fold,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  )
130
- output = runner.run()
131
- return _ok(output)
132
  except Exception as exc:
133
  return _err(str(exc))
134
 
135
 
136
- @mcp.tool(name="config_parse", description="Parse a MONAI bundle config file and return resolved content.")
137
- def config_parse(config_file: str) -> dict:
138
- """
139
- Parse and resolve MONAI bundle configuration.
140
 
141
  Parameters:
142
- config_file (str): Path to config JSON/YAML.
 
 
 
143
 
144
  Returns:
145
- dict: Standard response dictionary with parsed config content.
146
  """
147
- try:
148
- from monai.bundle.config_parser import ConfigParser
149
 
150
- parser = ConfigParser()
151
- parser.read_config(config_file)
152
- parser.parse()
153
- content = parser.get_parsed_content()
154
- return _ok(content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  except Exception as exc:
156
  return _err(str(exc))
157
 
158
 
159
- @mcp.tool(name="list_optional_dependencies", description="Check presence of common optional MONAI dependencies.")
160
- def list_optional_dependencies() -> dict:
161
- """
162
- Check import availability for key optional packages used by MONAI.
 
 
 
 
 
163
 
164
  Returns:
165
- dict: Standard response dictionary containing package availability map.
166
  """
 
 
 
167
  try:
168
- packages = [
169
- "nibabel",
170
- "SimpleITK",
171
- "pydicom",
172
- "itk",
173
- "scipy",
174
- "PIL",
175
- "ignite",
176
- "einops",
177
- "skimage",
178
- "mlflow",
179
- "tensorboard",
180
- "cv2",
181
- "cucim",
182
- "zarr",
183
- ]
184
- availability = {}
185
- for pkg in packages:
186
- try:
187
- __import__(pkg)
188
- availability[pkg] = True
189
- except Exception:
190
- availability[pkg] = False
191
- return _ok(availability)
192
  except Exception as exc:
193
  return _err(str(exc))
194
 
195
 
196
- def create_app() -> FastMCP:
197
  return mcp
198
 
199
 
200
  if __name__ == "__main__":
201
- mcp.run()
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
  import sys
5
+ from pathlib import Path
6
  from typing import Any
7
 
8
+ SOURCE_DIR = Path(__file__).resolve().parents[2] / "source"
9
+ if SOURCE_DIR.exists():
10
+ source_dir_str = str(SOURCE_DIR)
11
+ if source_dir_str not in sys.path:
12
+ sys.path.insert(0, source_dir_str)
13
+
14
+ try:
15
+ from fastmcp import FastMCP
16
+ except Exception:
17
+ FastMCP = None
18
+
19
+ try:
20
+ import monai
21
+ except Exception:
22
+ monai = None
23
+
24
+ try:
25
+ import torch
26
+ except Exception:
27
+ torch = None
28
+
29
+ try:
30
+ import numpy as np
31
+ except Exception:
32
+ np = None
33
+
34
+ try:
35
+ from .adapter import Adapter
36
+ except Exception:
37
+ Adapter = None
38
+
39
 
40
+ class _FallbackMCP:
41
+ def __init__(self, name: str) -> None:
42
+ self.name = name
43
+ self.tools: list[Any] = []
44
 
45
+ def tool(self, name: str, description: str):
46
+ def decorator(func):
47
+ func.__mcp_name__ = name
48
+ func.__mcp_description__ = description
49
+ self.tools.append(func)
50
+ return func
51
 
52
+ return decorator
53
 
54
+ def run(self, *args: Any, **kwargs: Any) -> None:
55
+ raise RuntimeError("fastmcp is not available. Please install dependencies first.")
56
 
57
+
58
+ mcp = FastMCP(name="MONAI MCP Service") if FastMCP is not None else _FallbackMCP(name="MONAI MCP Service")
59
+ _adapter = Adapter(target_package="monai") if Adapter is not None else None
60
+
61
+
62
+ def _ok(result: Any) -> dict[str, Any]:
63
  return {"success": True, "result": result, "error": None}
64
 
65
 
66
+ def _err(message: str) -> dict[str, Any]:
67
  return {"success": False, "result": None, "error": message}
68
 
69
 
70
+ def _json_to_list(value: str) -> list[Any]:
71
+ parsed = json.loads(value)
72
+ if not isinstance(parsed, list):
73
+ raise ValueError("expected a JSON list")
74
+ return parsed
75
+
76
+
77
+ def _json_to_dict(value: str) -> dict[str, Any]:
78
+ parsed = json.loads(value)
79
+ if not isinstance(parsed, dict):
80
+ raise ValueError("expected a JSON object")
81
+ return parsed
82
+
83
+
84
+ @mcp.tool(name="health_check", description="Report availability of FastMCP and MONAI-related dependencies.")
85
+ def health_check() -> dict[str, Any]:
86
+ """Return health information for runtime dependencies and adapter state.
87
+
88
+ Parameters:
89
+ None.
90
 
91
  Returns:
92
+ Standard MCP response containing dependency availability and adapter health.
93
  """
94
+ adapter_health = _adapter.health() if _adapter is not None else {"status": "error", "error": "adapter unavailable"}
95
+ return _ok(
96
+ {
97
+ "dependencies": {
98
+ "fastmcp": FastMCP is not None,
99
+ "monai": monai is not None,
100
+ "torch": torch is not None,
101
+ "numpy": np is not None,
102
+ "adapter": _adapter is not None,
103
+ },
104
+ "adapter": adapter_health,
105
  }
106
+ )
107
+
108
+
109
+ @mcp.tool(name="list_modules", description="List loaded MONAI modules and optional failures from the adapter.")
110
+ def list_modules(include_failed: bool = False, limit: int = 100) -> dict[str, Any]:
111
+ """List modules discovered by the adapter.
112
+
113
+ Parameters:
114
+ include_failed: Whether to include modules that failed to import.
115
+ limit: Maximum number of module records to return.
116
+
117
+ Returns:
118
+ Standard MCP response with module inventory.
119
+ """
120
+ if _adapter is None:
121
+ return _err("adapter is not available")
122
+
123
+ return _ok(_adapter.list_modules(include_failed=include_failed, limit=limit))
124
 
125
 
126
+ @mcp.tool(name="list_symbols", description="List symbols exported by a MONAI module.")
127
+ def list_symbols(module_name: str, public_only: bool = True, limit: int = 100) -> dict[str, Any]:
128
+ """List attributes and symbols from a module.
129
+
130
+ Parameters:
131
+ module_name: Full module path, for example 'monai.transforms'.
132
+ public_only: Exclude symbols that start with underscore when true.
133
+ limit: Maximum number of symbols to return.
134
+
135
+ Returns:
136
+ Standard MCP response with symbol list.
137
  """
138
+ if _adapter is None:
139
+ return _err("adapter is not available")
140
+
141
+ result = _adapter.list_symbols(module_name=module_name, public_only=public_only, limit=limit)
142
+ if result.get("status") == "error":
143
+ return _err(result.get("error", "unknown error"))
144
+ return _ok(result)
145
+
146
+
147
+ @mcp.tool(name="list_network_architectures", description="List available MONAI network architecture class names.")
148
+ def list_network_architectures(limit: int = 80) -> dict[str, Any]:
149
+ """List candidate network architecture classes from monai.networks.nets.
150
 
151
  Parameters:
152
+ limit: Maximum number of architecture names to return.
153
 
154
  Returns:
155
+ Standard MCP response with architecture names.
156
  """
157
+ if monai is None:
158
+ return _err("monai is not available")
159
 
160
+ try:
161
+ nets_module = __import__("monai.networks.nets", fromlist=["*"])
162
+ names: list[str] = []
163
+ for name in dir(nets_module):
164
+ if name.startswith("_"):
165
+ continue
166
+ value = getattr(nets_module, name, None)
167
+ if isinstance(value, type):
168
+ names.append(name)
169
+ names = sorted(set(names))[: max(limit, 0)]
170
+ return _ok({"architectures": names, "count": len(names)})
171
  except Exception as exc:
172
  return _err(str(exc))
173
 
174
 
175
+ @mcp.tool(name="create_network", description="Create a MONAI network instance from monai.networks.nets.")
176
+ def create_network(
177
+ network_name: str,
178
+ spatial_dims: int,
179
+ in_channels: int,
180
+ out_channels: int,
181
+ channels_json: str,
182
+ strides_json: str,
183
+ ) -> dict[str, Any]:
184
+ """Instantiate a MONAI network with common constructor arguments.
185
 
186
  Parameters:
187
+ network_name: Class name under monai.networks.nets, such as 'UNet' or 'DynUNet'.
188
+ spatial_dims: Number of spatial dimensions (2 or 3 usually).
189
+ in_channels: Number of input channels.
190
+ out_channels: Number of output channels.
191
+ channels_json: JSON list of channel widths, for example '[16,32,64,128]'.
192
+ strides_json: JSON list of strides, for example '[2,2,2]'.
193
 
194
  Returns:
195
+ Standard MCP response with object summary.
196
  """
197
+ if monai is None:
198
+ return _err("monai is not available")
199
+
200
  try:
201
+ channels = [int(v) for v in _json_to_list(channels_json)]
202
+ strides = [int(v) for v in _json_to_list(strides_json)]
203
+ nets_module = __import__("monai.networks.nets", fromlist=[network_name])
204
+ network_cls = getattr(nets_module, network_name, None)
205
+ if network_cls is None:
206
+ return _err(f"network class not found: {network_name}")
207
+
208
+ network = network_cls(
209
+ spatial_dims=spatial_dims,
210
+ in_channels=in_channels,
211
+ out_channels=out_channels,
212
+ channels=channels,
213
+ strides=strides,
214
+ )
215
+ return _ok(
216
+ {
217
+ "network_name": network_name,
218
+ "network_type": type(network).__name__,
219
+ "repr": repr(network),
220
+ }
221
  )
 
 
222
  except Exception as exc:
223
  return _err(str(exc))
224
 
225
 
226
+ @mcp.tool(name="build_transform_pipeline", description="Build a MONAI Compose pipeline from transform class names.")
227
+ def build_transform_pipeline(transform_names_json: str) -> dict[str, Any]:
228
+ """Create a Compose pipeline from zero-argument MONAI transform constructors.
 
 
 
 
 
 
229
 
230
  Parameters:
231
+ transform_names_json: JSON list of transform class names under monai.transforms,
232
+ for example '["EnsureChannelFirst", "ScaleIntensity"]'.
 
 
233
 
234
  Returns:
235
+ Standard MCP response with pipeline summary.
236
  """
237
+ if monai is None:
238
+ return _err("monai is not available")
239
 
240
+ try:
241
+ names = [str(v) for v in _json_to_list(transform_names_json)]
242
+ transforms_module = __import__("monai.transforms", fromlist=["Compose"])
243
+ compose_cls = getattr(transforms_module, "Compose", None)
244
+ if compose_cls is None:
245
+ return _err("Compose is not available in monai.transforms")
246
+
247
+ instances: list[Any] = []
248
+ created_names: list[str] = []
249
+ for transform_name in names:
250
+ transform_cls = getattr(transforms_module, transform_name, None)
251
+ if transform_cls is None:
252
+ return _err(f"transform class not found: {transform_name}")
253
+ instance = transform_cls()
254
+ instances.append(instance)
255
+ created_names.append(transform_name)
256
+
257
+ pipeline = compose_cls(instances)
258
+ return _ok(
259
+ {
260
+ "pipeline_type": type(pipeline).__name__,
261
+ "num_transforms": len(created_names),
262
+ "transforms": created_names,
263
+ }
264
  )
 
 
265
  except Exception as exc:
266
  return _err(str(exc))
267
 
268
 
269
+ @mcp.tool(name="call_function", description="Call a MONAI function via adapter reflection with JSON args.")
270
+ def call_function(module_name: str, function_name: str, args_json: str = "[]", kwargs_json: str = "{}") -> dict[str, Any]:
271
+ """Call a function from a target module using JSON-encoded arguments.
 
272
 
273
  Parameters:
274
+ module_name: Full module path, for example 'monai.data'.
275
+ function_name: Callable name inside the module.
276
+ args_json: JSON list of positional arguments.
277
+ kwargs_json: JSON object of keyword arguments.
278
 
279
  Returns:
280
+ Standard MCP response with call result if serializable.
281
  """
282
+ if _adapter is None:
283
+ return _err("adapter is not available")
284
 
285
+ try:
286
+ args = _json_to_list(args_json)
287
+ kwargs = _json_to_dict(kwargs_json)
288
+ result = _adapter.call_function(module_name=module_name, function_name=function_name, args=args, kwargs=kwargs)
289
+ if result.get("status") == "error":
290
+ return _err(result.get("error", "unknown error"))
291
+
292
+ value = result.get("result")
293
+ safe_value: Any
294
+ try:
295
+ json.dumps(value)
296
+ safe_value = value
297
+ except Exception:
298
+ safe_value = repr(value)
299
+
300
+ return _ok(
301
+ {
302
+ "status": result.get("status"),
303
+ "module": module_name,
304
+ "function": function_name,
305
+ "result": safe_value,
306
+ }
307
+ )
308
  except Exception as exc:
309
  return _err(str(exc))
310
 
311
 
312
+ @mcp.tool(name="create_instance", description="Create an instance from a MONAI class via adapter reflection.")
313
+ def create_instance(module_name: str, class_name: str, args_json: str = "[]", kwargs_json: str = "{}") -> dict[str, Any]:
314
+ """Instantiate a class from a module using JSON arguments.
315
+
316
+ Parameters:
317
+ module_name: Full module path, for example 'monai.losses'.
318
+ class_name: Class name in the module.
319
+ args_json: JSON list of positional constructor args.
320
+ kwargs_json: JSON object of keyword constructor args.
321
 
322
  Returns:
323
+ Standard MCP response with instance summary.
324
  """
325
+ if _adapter is None:
326
+ return _err("adapter is not available")
327
+
328
  try:
329
+ args = _json_to_list(args_json)
330
+ kwargs = _json_to_dict(kwargs_json)
331
+ result = _adapter.create_instance(module_name=module_name, class_name=class_name, args=args, kwargs=kwargs)
332
+ if result.get("status") == "error":
333
+ return _err(result.get("error", "unknown error"))
334
+
335
+ instance = result.get("instance")
336
+ return _ok(
337
+ {
338
+ "status": result.get("status"),
339
+ "module": module_name,
340
+ "class": class_name,
341
+ "instance_type": type(instance).__name__ if instance is not None else None,
342
+ "repr": repr(instance),
343
+ }
344
+ )
 
 
 
 
 
 
 
 
345
  except Exception as exc:
346
  return _err(str(exc))
347
 
348
 
349
+ def create_app() -> Any:
350
  return mcp
351
 
352
 
353
  if __name__ == "__main__":
354
+ mcp.run()
MONAI/mcp_output/requirements.txt CHANGED
@@ -1,6 +1,4 @@
1
- fastmcp
2
- fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
- torch>=2.4.1; platform_system != "Windows"
6
- numpy>=1.24,<3.0
 
1
+ fastmcp>=2.0.0
2
+ monai
3
+ numpy
4
+ torch
 
 
MONAI/mcp_output/start_mcp.py CHANGED
@@ -1,30 +1,37 @@
 
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
+ plugin_dir_str = str(PLUGIN_DIR)
9
+ if plugin_dir_str not in sys.path:
10
+ sys.path.insert(0, plugin_dir_str)
11
 
12
  from mcp_service import create_app
13
 
14
+
15
+ def main() -> None:
16
+ transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower()
17
+ port = int(os.getenv("MCP_PORT", "8000"))
18
+
19
  app = create_app()
20
+
 
 
 
 
21
  if transport == "http":
22
+ try:
23
+ app.run(transport="http", host="0.0.0.0", port=port)
24
+ return
25
+ except TypeError:
26
+ try:
27
+ app.run(transport="http", port=port)
28
+ return
29
+ except TypeError:
30
+ app.run(port=port)
31
+ return
32
+
33
+ app.run()
34
+
35
 
36
  if __name__ == "__main__":
37
  main()
README.md CHANGED
@@ -1,10 +1,67 @@
1
  ---
2
- title: MONAI
3
- emoji: 🔥
4
- colorFrom: indigo
5
- colorTo: pink
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: MONAI MCP Service
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # MONAI MCP Service
12
+
13
+ This deployment package wraps MONAI capabilities as MCP tools using FastMCP.
14
+ It supports local `stdio` usage and HTTP MCP endpoint deployment for Docker/HuggingFace Spaces.
15
+
16
+ ## Available Tools
17
+
18
+ - `health_check`
19
+ - `list_modules`
20
+ - `list_symbols`
21
+ - `list_network_architectures`
22
+ - `create_network`
23
+ - `build_transform_pipeline`
24
+ - `call_function`
25
+ - `create_instance`
26
+
27
+ Detailed tool docs are in `MONAI/mcp_output/README_MCP.md`.
28
+
29
+ ## Local stdio (Claude Desktop / CLI)
30
+
31
+ ```bash
32
+ cd MONAI/mcp_output
33
+ python mcp_plugin/main.py
34
+ ```
35
+
36
+ or:
37
+
38
+ ```bash
39
+ MCP_TRANSPORT=stdio python start_mcp.py
40
+ ```
41
+
42
+ ## HTTP MCP endpoint (Docker/HF Spaces)
43
+
44
+ The Docker entrypoint launches FastMCP directly from:
45
+
46
+ - `python MONAI/mcp_output/start_mcp.py`
47
+
48
+ Runtime env in container:
49
+
50
+ - `MCP_TRANSPORT=http`
51
+ - `MCP_PORT=7860`
52
+
53
+ MCP clients should connect to:
54
+
55
+ - `https://<your-host>/mcp`
56
+
57
+ ## Build and Run Docker
58
+
59
+ ```bash
60
+ ./run_docker.sh
61
+ ```
62
+
63
+ On Windows PowerShell:
64
+
65
+ ```powershell
66
+ ./run_docker.ps1
67
+ ```
app.py CHANGED
@@ -1,45 +1,49 @@
1
- from fastapi import FastAPI
 
2
  import os
3
  import sys
 
 
 
 
 
 
 
 
 
4
 
5
- mcp_plugin_path = os.path.join(os.path.dirname(__file__), "MONAI", "mcp_output", "mcp_plugin")
6
- sys.path.insert(0, mcp_plugin_path)
 
7
 
8
- app = FastAPI(
9
- title="Monai MCP Service",
10
- description="Auto-generated MCP service for MONAI",
11
- version="1.0.0"
12
- )
13
 
14
  @app.get("/")
15
- def root():
16
  return {
17
- "service": "Monai 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": "MONAI 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
7
+
8
+ from fastapi import FastAPI
9
+
10
+ PLUGIN_DIR = Path(__file__).resolve().parent / "MONAI" / "mcp_output" / "mcp_plugin"
11
+ plugin_dir_str = str(PLUGIN_DIR)
12
+ if plugin_dir_str not in sys.path:
13
+ sys.path.insert(0, plugin_dir_str)
14
 
15
+ PORT = int(os.getenv("PORT", "7860"))
16
+
17
+ app = FastAPI(title="MONAI MCP Info App", version="1.0.0")
18
 
 
 
 
 
 
19
 
20
  @app.get("/")
21
+ def root() -> dict[str, Any]:
22
  return {
23
+ "service": "MONAI MCP Service",
24
+ "description": "Supplementary info API for local development.",
25
+ "mcp_http_endpoint": "/mcp",
26
+ "default_port": PORT,
27
+ "note": "This FastAPI app is not the MCP runtime entry point.",
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, Any]:
38
  try:
39
  from mcp_service import create_app
40
+
41
+ mcp = create_app()
42
+ items = []
43
+ for tool in getattr(mcp, "tools", []):
44
+ name = getattr(tool, "name", None) or getattr(tool, "__mcp_name__", None) or getattr(tool, "__name__", "unknown")
45
+ description = getattr(tool, "description", None) or getattr(tool, "__mcp_description__", None) or ""
46
+ items.append({"name": name, "description": description})
47
+ return {"count": len(items), "tools": items}
48
+ except Exception as exc:
49
+ return {"count": 0, "tools": [], "error": str(exc)}
 
 
 
 
 
port.json CHANGED
@@ -1,5 +1 @@
1
- {
2
- "repo": "MONAI",
3
- "port": 7919,
4
- "timestamp": 1773273129
5
- }
 
1
+ {"port": 7860}
 
 
 
 
requirements.txt CHANGED
@@ -1,6 +1,6 @@
1
- fastmcp
 
 
 
2
  fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
- torch>=2.4.1; platform_system != "Windows"
6
- numpy>=1.24,<3.0
 
1
+ fastmcp>=2.0.0
2
+ monai
3
+ numpy
4
+ torch
5
  fastapi
6
+ uvicorn
 
 
 
run_docker.ps1 CHANGED
@@ -1,26 +1,8 @@
1
- cd $PSScriptRoot
2
- $ErrorActionPreference = "Stop"
3
- $entryName = if ($env:MCP_ENTRY_NAME) { $env:MCP_ENTRY_NAME } else { "MONAI" }
4
- $entryUrl = if ($env:MCP_ENTRY_URL) { $env:MCP_ENTRY_URL } else { "http://localhost:7919/mcp" }
5
- $imageName = if ($env:MCP_IMAGE_NAME) { $env:MCP_IMAGE_NAME } else { "MONAI-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 7919:7860 $imageName
 
 
 
1
+ $port = (Get-Content -Raw -Path "port.json" | ConvertFrom-Json).port
2
+ $imageName = "MONAI-mcp"
3
+
4
+ Write-Host "Building Docker image: $imageName"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  docker build -t $imageName .
6
+
7
+ Write-Host "Running container on port $port"
8
+ docker run --rm -p "${port}:${port}" $imageName
run_docker.sh CHANGED
@@ -1,75 +1,12 @@
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
- cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
4
- mcp_entry_name="${MCP_ENTRY_NAME:-MONAI}"
5
- mcp_entry_url="${MCP_ENTRY_URL:-http://localhost:7919/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 MONAI-mcp .
75
- docker run --rm -p 7919:7860 MONAI-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="MONAI-mcp"
6
+
7
+
8
+ echo "Building Docker image: ${IMAGE_NAME}"
9
+ docker build -t "${IMAGE_NAME}" .
10
+
11
+ echo "Running container on port ${PORT}"
12
+ docker run --rm -p "${PORT}:${PORT}" "${IMAGE_NAME}"