ghh1125 commited on
Commit
75f694d
·
verified ·
1 Parent(s): b7a8635

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", "mne-python/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 mne-python /app/mne-python
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", "mne-python/mcp_output/start_mcp.py"]
README.md CHANGED
@@ -1,10 +1,65 @@
1
  ---
2
- title: Mne Python
3
- emoji: 🏃
4
- colorFrom: pink
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: mne-python MCP Service
3
+ emoji: 🔧
4
+ colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # mne-python MCP Service
12
+
13
+ This deployment package exposes core MNE-Python capabilities through a FastMCP service.
14
+
15
+ ## Available MCP Tools
16
+
17
+ - `health_check`
18
+ - `get_mne_version`
19
+ - `list_builtin_montages`
20
+ - `create_info`
21
+ - `create_raw_array`
22
+ - `filter_data_array`
23
+ - `compute_psd_welch`
24
+ - `list_loaded_modules`
25
+
26
+ ## Local stdio usage (Claude Desktop / CLI)
27
+
28
+ From `mne-python/mcp_output/mcp_plugin/`:
29
+
30
+ ```bash
31
+ python main.py
32
+ ```
33
+
34
+ Or from `mne-python/mcp_output/`:
35
+
36
+ ```bash
37
+ python start_mcp.py
38
+ ```
39
+
40
+ Default transport is `stdio`.
41
+
42
+ ## HTTP client usage
43
+
44
+ Run with HTTP transport:
45
+
46
+ ```bash
47
+ MCP_TRANSPORT=http MCP_PORT=8000 python mne-python/mcp_output/start_mcp.py
48
+ ```
49
+
50
+ Then connect MCP clients to:
51
+
52
+ - `http://127.0.0.1:8000/mcp`
53
+
54
+ ## Docker / HuggingFace Spaces
55
+
56
+ This package is configured for HuggingFace Spaces Docker deployment:
57
+
58
+ - fixed port `7860`
59
+ - non-root runtime user UID `1000`
60
+ - MCP HTTP endpoint served directly by FastMCP
61
+ - container entrypoint: `python mne-python/mcp_output/start_mcp.py`
62
+
63
+ Clients should connect to:
64
+
65
+ - `https://<your-space>.hf.space/mcp`
app.py CHANGED
@@ -1,45 +1,74 @@
1
- from fastapi import FastAPI
 
2
  import os
 
3
  import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- mcp_plugin_path = os.path.join(os.path.dirname(__file__), "mne-python", "mcp_output", "mcp_plugin")
6
- sys.path.insert(0, mcp_plugin_path)
7
 
8
- app = FastAPI(
9
- title="Mne-Python MCP Service",
10
- description="Auto-generated MCP service for mne-python",
11
- version="1.0.0"
12
- )
 
13
 
14
  @app.get("/")
15
- def root():
16
  return {
17
- "service": "Mne-Python 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": "mne-python 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
+ from pathlib import Path
5
  import sys
6
+ import importlib
7
+
8
+ try:
9
+ FastAPI = importlib.import_module("fastapi").FastAPI
10
+ except Exception: # pragma: no cover
11
+ FastAPI = None
12
+
13
+
14
+ BASE_DIR = Path(__file__).resolve().parent
15
+ MCP_OUTPUT_DIR = BASE_DIR / "mne-python" / "mcp_output"
16
+ output_str = str(MCP_OUTPUT_DIR)
17
+ if output_str not in sys.path:
18
+ sys.path.insert(0, output_str)
19
 
 
 
20
 
21
+ if FastAPI is None:
22
+ raise RuntimeError("fastapi is required to run app.py")
23
+
24
+
25
+ app = FastAPI(title="mne-python MCP Companion API", version="1.0.0")
26
+
27
 
28
  @app.get("/")
29
+ def root() -> dict[str, object]:
30
  return {
31
+ "service": "mne-python-mcp",
32
+ "description": "Supplementary API for MCP deployment info.",
33
+ "mcp_transport_env": os.getenv("MCP_TRANSPORT", "stdio"),
34
+ "mcp_port_env": int(os.getenv("MCP_PORT", "7860")),
35
+ "note": "This FastAPI app is informational and does not run MCP itself.",
36
  }
37
 
38
+
39
  @app.get("/health")
40
+ def health() -> dict[str, str]:
41
+ return {"status": "healthy"}
42
+
43
 
44
  @app.get("/tools")
45
+ def list_tools() -> dict[str, object]:
46
  try:
47
+ mcp_module = importlib.import_module("mcp_plugin.mcp_service")
48
+ create_app = getattr(mcp_module, "create_app")
49
+ mcp = create_app()
50
+ raw_tools = getattr(mcp, "tools", [])
51
+ tools_list: list[dict[str, str]] = []
52
+
53
+ if isinstance(raw_tools, dict):
54
+ iterable = raw_tools.values()
55
+ else:
56
+ iterable = raw_tools
57
+
58
+ for tool in iterable:
59
+ name = getattr(tool, "name", None) or getattr(tool, "__name__", "unknown")
60
+ description = getattr(tool, "description", None) or getattr(tool, "__doc__", "")
61
+ tools_list.append({"name": str(name), "description": str(description).strip()})
62
+
63
+ return {"count": len(tools_list), "tools": tools_list}
64
+ except Exception as exc:
65
+ return {"count": 0, "tools": [], "error": str(exc)}
66
+
67
+
68
+ PORT = int(os.getenv("PORT", "7860"))
69
+
70
 
71
  if __name__ == "__main__":
72
  import uvicorn
73
+
74
+ uvicorn.run(app, host="0.0.0.0", port=PORT)
mne-python/mcp_output/README_MCP.md CHANGED
@@ -1,129 +1,158 @@
1
- # MNE-Python MCP (Model Context Protocol) Service README
2
-
3
- ## 1) Project Introduction
4
-
5
- This service wraps core **MNE-Python** capabilities for EEG/MEG workflows in an MCP (Model Context Protocol)-friendly interface.
6
-
7
- Primary goals:
8
- - Load raw neurophysiology data (FIF, EDF, BDF, BrainVision).
9
- - Perform event handling and channel picking.
10
- - Run common preprocessing (EOG/ECG detection, ICA-related preparation).
11
- - Compute PSD and time-frequency features.
12
- - Optionally render diagnostic visualizations (when GUI/backends are available).
13
-
14
- Recommended integration path:
15
- - **Primary:** in-process Python imports from `mne` modules.
16
- - **Fallback:** call the `mne` CLI for heavier or isolated execution contexts.
17
-
18
- ---
19
-
20
- ## 2) Installation Method
21
-
22
- ### Requirements
23
- Core dependencies:
24
- - `numpy`
25
- - `scipy`
26
- - `matplotlib`
27
- - `packaging`
28
- - `pooch`
29
- - `tqdm`
30
-
31
- Common optional dependencies (feature-dependent):
32
- - `scikit-learn`, `pandas`, `h5py`, `nibabel`
33
- - `pyvista`, `vtk`, `mne-qt-browser`
34
- - `numba`
35
-
36
- ### Install
37
- - Install MNE-Python and common scientific stack via pip:
38
- - `pip install mne`
39
- - For broader functionality:
40
- - `pip install mne[full]` (if supported by your environment/version)
41
- - If your service uses project-local dependency files, prefer:
42
- - `pyproject.toml` / `environment.yml` in your deployment workflow.
43
-
44
- ---
45
-
46
- ## 3) Quick Start
47
-
48
- ### Minimal service flow (Python-side)
49
- 1. Read raw data:
50
- - `mne.io.read_raw_fif(...)` / `read_raw_edf(...)` / `read_raw_bdf(...)` / `read_raw_brainvision(...)`
51
- 2. Extract events:
52
- - `mne.find_events(raw)` or `mne.read_events(path)`
53
- 3. Pick channels:
54
- - `mne.pick_types(raw.info, meg=True, eeg=True, eog=True, exclude="bads")`
55
- 4. Preprocess (optional):
56
- - `mne.preprocessing.find_eog_events(raw)`, `find_ecg_events(raw)`, ICA workflows
57
- 5. Spectral analysis:
58
- - `mne.time_frequency.psd_array_welch(...)` or `psd_array_multitaper(...)`
59
- 6. Return structured results from your MCP (Model Context Protocol) service endpoint.
60
-
61
- ### CLI fallback
62
- - Use `mne` command wrapper when import-time overhead or environment isolation is needed.
63
-
64
- ---
65
-
66
- ## 4) Available Tools and Endpoints List
67
-
68
- Suggested MCP (Model Context Protocol) service endpoints:
69
-
70
- - `load_raw`
71
- - Load raw recordings from FIF/EDF/BDF/BrainVision.
72
- - Maps to `mne.io.read_raw_*`.
73
-
74
- - `read_info`
75
- - Read metadata/header info without full processing.
76
- - Maps to `mne.io.read_info`.
77
-
78
- - `events_detect`
79
- - Detect or load event markers.
80
- - Maps to `mne.find_events`, `mne.read_events`, `mne.merge_events`.
81
-
82
- - `channels_pick`
83
- - Build channel selections by modality/type.
84
- - Maps to `mne.pick_types`.
85
-
86
- - `preprocess_eog_ecg`
87
- - EOG/ECG event detection and projection helpers.
88
- - Maps to `mne.preprocessing.find_eog_events`, `find_ecg_events`, `compute_proj_eog`, `compute_proj_ecg`.
89
-
90
- - `ica_workflow`
91
- - Artifact decomposition/removal pipeline.
92
- - Maps to `mne.preprocessing.ICA`, `EOGRegression`.
93
-
94
- - `psd_compute`
95
- - Power spectral density calculations.
96
- - Maps to `mne.time_frequency.psd_array_welch`, `psd_array_multitaper`.
97
-
98
- - `tfr_compute`
99
- - Time-frequency decomposition.
100
- - Maps to `tfr_morlet`, `tfr_multitaper`, `csd_multitaper`.
101
-
102
- - `viz_diagnostics` (optional/headless-sensitive)
103
- - Event/covariance/alignment diagnostic plotting.
104
- - Maps to `mne.viz.plot_events`, `plot_cov`, `plot_bem`, `plot_alignment`.
105
-
106
- - `cli_exec` (fallback)
107
- - Run `mne` subcommands for isolated or heavyweight tasks.
108
-
109
- ---
110
-
111
- ## 5) Common Issues and Notes
112
-
113
- - **Complexity:** MNE-Python is feature-rich; keep endpoint contracts narrow and typed.
114
- - **Import overhead:** First import can be non-trivial; consider lazy-loading per endpoint.
115
- - **GUI dependencies:** Visualization endpoints may fail in headless servers unless backend is configured.
116
- - **Optional packages:** Some analyses silently require extras (`sklearn`, `h5py`, `nibabel`, etc.).
117
- - **Performance:** Large FIF files and TFR/ICA jobs are memory/CPU intensive; set resource limits.
118
- - **Reproducibility:** Pin MNE + NumPy/SciPy versions in deployment.
119
- - **Risk profile:** Import feasibility is moderate (~0.72); keep CLI fallback available.
120
- - **Data handling:** Validate paths, file formats, and channel metadata before heavy processing.
121
-
122
- ---
123
-
124
- ## 6) Reference Links / Documentation
125
-
126
- - MNE-Python repository: https://github.com/mne-tools/mne-python
127
- - MNE official docs: https://mne.tools/stable/index.html
128
- - MNE API reference: https://mne.tools/stable/python_reference.html
129
- - MNE command-line tools: https://mne.tools/stable/overview/command_line.html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MNE-Python MCP Layer
2
+
3
+ This directory contains the MCP plugin and launch scripts for exposing selected MNE-Python capabilities.
4
+
5
+ ## Exposed MCP Tools
6
+
7
+ ### 1) `health_check`
8
+ - **Description**: Reports dependency availability and adapter health.
9
+ - **Parameters**: none
10
+ - **Example**:
11
+ ```json
12
+ {"tool": "health_check", "arguments": {}}
13
+ ```
14
+
15
+ ### 2) `get_mne_version`
16
+ - **Description**: Returns installed MNE version and module location.
17
+ - **Parameters**: none
18
+ - **Example**:
19
+ ```json
20
+ {"tool": "get_mne_version", "arguments": {}}
21
+ ```
22
+
23
+ ### 3) `list_builtin_montages`
24
+ - **Description**: Lists available built-in electrode montages.
25
+ - **Parameters**: none
26
+ - **Example**:
27
+ ```json
28
+ {"tool": "list_builtin_montages", "arguments": {}}
29
+ ```
30
+
31
+ ### 4) `create_info`
32
+ - **Description**: Creates an MNE `Info` object and returns a summary.
33
+ - **Parameters**:
34
+ - `ch_names: list[str]`
35
+ - `sfreq: float`
36
+ - `ch_types: str = "eeg"`
37
+ - **Example**:
38
+ ```json
39
+ {
40
+ "tool": "create_info",
41
+ "arguments": {
42
+ "ch_names": ["Fz", "Cz", "Pz"],
43
+ "sfreq": 250.0,
44
+ "ch_types": "eeg"
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### 5) `create_raw_array`
50
+ - **Description**: Builds an in-memory `RawArray` from matrix data and returns metadata.
51
+ - **Parameters**:
52
+ - `data: list[list[float]]` (shape `[n_channels, n_times]`)
53
+ - `sfreq: float`
54
+ - `ch_names: list[str]`
55
+ - `ch_types: str = "eeg"`
56
+ - **Example**:
57
+ ```json
58
+ {
59
+ "tool": "create_raw_array",
60
+ "arguments": {
61
+ "data": [[0.1, 0.2, 0.3], [0.0, -0.1, 0.1]],
62
+ "sfreq": 1000.0,
63
+ "ch_names": ["EEG001", "EEG002"],
64
+ "ch_types": "eeg"
65
+ }
66
+ }
67
+ ```
68
+
69
+ ### 6) `filter_data_array`
70
+ - **Description**: Applies MNE filtering to array data.
71
+ - **Parameters**:
72
+ - `data: list[list[float]]`
73
+ - `sfreq: float`
74
+ - `l_freq: float | null`
75
+ - `h_freq: float | null`
76
+ - **Example**:
77
+ ```json
78
+ {
79
+ "tool": "filter_data_array",
80
+ "arguments": {
81
+ "data": [[0.1, 0.2, 0.3, 0.2], [0.0, -0.1, 0.1, 0.0]],
82
+ "sfreq": 250.0,
83
+ "l_freq": 1.0,
84
+ "h_freq": 40.0
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### 7) `compute_psd_welch`
90
+ - **Description**: Computes Welch PSD from array data.
91
+ - **Parameters**:
92
+ - `data: list[list[float]]`
93
+ - `sfreq: float`
94
+ - `fmin: float = 0.0`
95
+ - `fmax: float = 120.0`
96
+ - `n_fft: int = 256`
97
+ - **Example**:
98
+ ```json
99
+ {
100
+ "tool": "compute_psd_welch",
101
+ "arguments": {
102
+ "data": [[0.1, 0.2, 0.3, 0.2], [0.0, -0.1, 0.1, 0.0]],
103
+ "sfreq": 250.0,
104
+ "fmin": 1.0,
105
+ "fmax": 60.0,
106
+ "n_fft": 128
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### 8) `list_loaded_modules`
112
+ - **Description**: Lists modules loaded by the adapter.
113
+ - **Parameters**:
114
+ - `limit: int = 100`
115
+ - **Example**:
116
+ ```json
117
+ {"tool": "list_loaded_modules", "arguments": {"limit": 50}}
118
+ ```
119
+
120
+ ## Running Locally (stdio)
121
+
122
+ From `mcp_output/`:
123
+
124
+ ```bash
125
+ python start_mcp.py
126
+ ```
127
+
128
+ Or directly:
129
+
130
+ ```bash
131
+ python mcp_plugin/main.py
132
+ ```
133
+
134
+ Both default to stdio transport for Claude Desktop / CLI style MCP clients.
135
+
136
+ ## Running as HTTP MCP Service
137
+
138
+ ```bash
139
+ MCP_TRANSPORT=http MCP_PORT=8000 python start_mcp.py
140
+ ```
141
+
142
+ The MCP HTTP endpoint is served at:
143
+
144
+ - `http://127.0.0.1:8000/mcp`
145
+
146
+ ## Response Shape
147
+
148
+ All MCP tools return:
149
+
150
+ ```json
151
+ {
152
+ "success": true,
153
+ "result": {},
154
+ "error": null
155
+ }
156
+ ```
157
+
158
+ On errors, `success` is `false`, `result` is `null`, and `error` contains a message.
mne-python/mcp_output/mcp_plugin/adapter.py CHANGED
@@ -1,358 +1,227 @@
1
- import os
 
 
2
  import sys
3
- import importlib
4
- import traceback
5
- from typing import Any, Dict, List, Optional
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
- Import-mode adapter for the mne-python MCP plugin integration.
 
17
 
18
- This adapter prioritizes Python imports and provides a graceful CLI fallback
19
- pathway when import-based execution is unavailable.
20
- """
21
 
22
- # -------------------------------------------------------------------------
23
- # Lifecycle / Initialization
24
- # -------------------------------------------------------------------------
25
- def __init__(self) -> None:
26
- """
27
- Initialize adapter state, import registry, and module availability.
28
 
29
- Attributes:
30
- mode: Adapter execution mode, fixed to "import".
31
- package_root: Root import path for repository package.
32
- modules: Loaded module cache keyed by logical name.
33
- available: Boolean import readiness state.
34
- warnings: List of non-fatal issues detected during initialization.
35
- """
36
- self.mode = "import"
37
- self.package_root = "mne"
38
- self.modules: Dict[str, Any] = {}
39
- self.available = False
40
- self.warnings: List[str] = []
41
- self._initialize_imports()
42
 
43
- def _initialize_imports(self) -> None:
44
- """
45
- Attempt to import core and command modules identified by analysis.
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- This method captures and stores import exceptions so the adapter can
48
- continue operating in graceful fallback mode.
49
- """
50
- targets = {
51
- "mne": "mne",
52
- "commands": "mne.commands",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
 
55
- loaded = 0
56
- for key, mod_path in targets.items():
57
- try:
58
- self.modules[key] = importlib.import_module(mod_path)
59
- loaded += 1
60
- except Exception as exc:
61
- self.modules[key] = None
62
- self.warnings.append(
63
- f"Failed to import '{mod_path}'. Verify local source checkout and dependencies. Detail: {exc}"
64
- )
65
- self.available = loaded > 0
66
 
67
- # -------------------------------------------------------------------------
68
- # Unified response helpers
69
- # -------------------------------------------------------------------------
70
- def _ok(self, message: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
71
- return {"status": "success", "message": message, "data": data or {}}
 
 
 
 
 
72
 
73
- def _fail(
74
  self,
75
- message: str,
76
- error: Optional[str] = None,
77
- guidance: Optional[str] = None,
78
- data: Optional[Dict[str, Any]] = None,
79
- ) -> Dict[str, Any]:
80
- payload = {"status": "error", "message": message, "data": data or {}}
81
- if error:
82
- payload["error"] = error
83
- if guidance:
84
- payload["guidance"] = guidance
85
- return payload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- def _fallback(
88
- self,
89
- action: str,
90
- reason: str,
91
- extra: Optional[Dict[str, Any]] = None,
92
- ) -> Dict[str, Any]:
93
  return {
94
- "status": "fallback",
95
- "message": f"Import mode unavailable for '{action}'.",
96
- "reason": reason,
97
- "guidance": (
98
- "Ensure repository source is present under the configured 'source' directory and "
99
- "install required dependencies (numpy, scipy, matplotlib, packaging, pooch, tqdm). "
100
- "If import still fails, use the CLI fallback via the 'mne' command."
101
- ),
102
- "data": extra or {},
103
  }
104
 
105
- # -------------------------------------------------------------------------
106
- # Health / Introspection
107
- # -------------------------------------------------------------------------
108
- def health_check(self) -> Dict[str, Any]:
109
- """
110
- Report adapter readiness and import diagnostics.
111
-
112
- Returns:
113
- Dict with status, mode, import availability, and warning details.
114
- """
115
- return self._ok(
116
- "Adapter health check completed.",
117
- {
 
 
 
 
118
  "mode": self.mode,
119
- "available": self.available,
120
- "loaded_modules": {k: bool(v) for k, v in self.modules.items()},
121
- "warnings": self.warnings,
122
- },
123
- )
124
-
125
- def list_known_packages(self) -> Dict[str, Any]:
126
- """
127
- Return package namespaces discovered during analysis.
128
-
129
- Returns:
130
- Dict containing analyzed package names for discovery/debugging.
131
- """
132
- packages = [
133
- "deployment.mne-python.source",
134
- "mcp_output.mcp_plugin",
135
- "source.mne",
136
- "source.mne._fiff",
137
- "source.mne.beamformer",
138
- "source.mne.channels",
139
- "source.mne.commands",
140
- "source.mne.data",
141
- "source.mne.datasets",
142
- "source.mne.decoding",
143
- "source.mne.export",
144
- "source.mne.forward",
145
- "source.mne.gui",
146
- "source.mne.html_templates",
147
- "source.mne.inverse_sparse",
148
- "source.mne.io",
149
- "source.mne.minimum_norm",
150
- "source.mne.preprocessing",
151
- "source.mne.report",
152
- "source.mne.simulation",
153
- "source.mne.source_space",
154
- "source.mne.stats",
155
- "source.mne.tests",
156
- "source.mne.time_frequency",
157
- "source.mne.utils",
158
- "source.mne.viz",
159
- ]
160
- return self._ok("Known package list prepared.", {"packages": packages})
161
-
162
- # -------------------------------------------------------------------------
163
- # Module management
164
- # -------------------------------------------------------------------------
165
- def import_module(self, module_path: str) -> Dict[str, Any]:
166
- """
167
- Dynamically import an MNE module using full package path.
168
-
169
- Args:
170
- module_path: Absolute module path (e.g., 'mne.io', 'mne.preprocessing').
171
-
172
- Returns:
173
- Unified status dictionary with module import result.
174
- """
175
  try:
176
- mod = importlib.import_module(module_path)
177
- self.modules[module_path] = mod
178
- return self._ok("Module imported successfully.", {"module_path": module_path})
 
 
 
 
179
  except Exception as exc:
180
- return self._fail(
181
- "Module import failed.",
182
- error=str(exc),
183
- guidance="Confirm module path and dependency installation.",
184
- data={"module_path": module_path},
185
- )
186
-
187
- def get_module_attributes(self, module_path: str, limit: int = 200) -> Dict[str, Any]:
188
- """
189
- Enumerate public attributes for a module to support function discovery.
190
 
191
- Args:
192
- module_path: Full module path.
193
- limit: Maximum number of attributes to return.
194
-
195
- Returns:
196
- Unified status dictionary containing exported attribute names.
197
- """
198
- try:
199
- mod = self.modules.get(module_path) or importlib.import_module(module_path)
200
- attrs = [a for a in dir(mod) if not a.startswith("_")]
201
- return self._ok(
202
- "Module attributes fetched.",
203
- {"module_path": module_path, "attributes": attrs[: max(1, limit)]},
204
- )
205
- except Exception as exc:
206
- return self._fail(
207
- "Could not inspect module attributes.",
208
- error=str(exc),
209
- guidance="Import the module first and verify it is available in local source.",
210
- data={"module_path": module_path},
211
- )
212
-
213
- # -------------------------------------------------------------------------
214
- # Core MNE call surface
215
- # -------------------------------------------------------------------------
216
- def call_mne_function(self, function_name: str, *args: Any, **kwargs: Any) -> Dict[str, Any]:
217
- """
218
- Call a function from the top-level mne module by name.
219
-
220
- Args:
221
- function_name: Name of the function in module 'mne'.
222
- *args: Positional arguments forwarded to the target function.
223
- **kwargs: Keyword arguments forwarded to the target function.
224
-
225
- Returns:
226
- Unified status dictionary with execution result or actionable failure.
227
- """
228
- if not self.modules.get("mne"):
229
- return self._fallback("call_mne_function", "Top-level module 'mne' is not importable.")
230
-
231
- try:
232
- target = getattr(self.modules["mne"], function_name, None)
233
- if target is None or not callable(target):
234
- return self._fail(
235
- "Requested MNE function was not found.",
236
- guidance="Use get_module_attributes('mne') to discover available callables.",
237
- data={"function_name": function_name},
238
- )
239
- result = target(*args, **kwargs)
240
- return self._ok(
241
- "MNE function executed successfully.",
242
- {"function_name": function_name, "result": result},
243
- )
244
- except Exception as exc:
245
- return self._fail(
246
- "MNE function execution failed.",
247
- error=str(exc),
248
- guidance="Validate function arguments and data formats expected by MNE.",
249
- data={"function_name": function_name, "traceback": traceback.format_exc()},
250
- )
251
-
252
- # -------------------------------------------------------------------------
253
- # CLI wrapper (fallback-friendly)
254
- # -------------------------------------------------------------------------
255
- def call_mne_cli(self, argv: Optional[List[str]] = None) -> Dict[str, Any]:
256
- """
257
- Execute the primary MNE command-line wrapper from imported command module.
258
-
259
- Args:
260
- argv: Optional list of CLI-like arguments. If omitted, attempts default call.
261
-
262
- Returns:
263
- Unified status dictionary with invocation status and hints.
264
- """
265
- commands_mod = self.modules.get("commands")
266
- if not commands_mod:
267
- return self._fallback("call_mne_cli", "Module 'mne.commands' is not importable.")
268
-
269
- try:
270
- entry_candidates = ["main", "run", "command_main"]
271
- entry = None
272
- for name in entry_candidates:
273
- fn = getattr(commands_mod, name, None)
274
- if callable(fn):
275
- entry = fn
276
- break
277
-
278
- if entry is None:
279
- return self._fail(
280
- "No callable CLI entry point found in mne.commands.",
281
- guidance="Inspect mne.commands module attributes and map the correct callable.",
282
- data={"checked_candidates": entry_candidates},
283
- )
284
-
285
- if argv is None:
286
- out = entry()
287
- else:
288
- out = entry(argv)
289
-
290
- return self._ok(
291
- "MNE CLI wrapper executed.",
292
- {"argv": argv or [], "result": out, "entry_point": entry.__name__},
293
- )
294
- except TypeError:
295
- try:
296
- out = entry() # type: ignore[misc]
297
- return self._ok(
298
- "MNE CLI wrapper executed with default signature.",
299
- {"argv": argv or [], "result": out, "entry_point": entry.__name__}, # type: ignore[union-attr]
300
- )
301
- except Exception as exc:
302
- return self._fail(
303
- "MNE CLI invocation failed.",
304
- error=str(exc),
305
- guidance="Pass valid subcommands/arguments or verify command module compatibility.",
306
- data={"traceback": traceback.format_exc()},
307
- )
308
- except Exception as exc:
309
- return self._fail(
310
- "MNE CLI invocation failed.",
311
- error=str(exc),
312
- guidance="Check installed optional dependencies and command arguments.",
313
- data={"traceback": traceback.format_exc()},
314
- )
315
-
316
- # -------------------------------------------------------------------------
317
- # Class instantiation helper (generic, analysis-driven)
318
- # -------------------------------------------------------------------------
319
  def create_instance(
320
  self,
321
- module_path: str,
322
  class_name: str,
323
- *args: Any,
324
- **kwargs: Any,
325
- ) -> Dict[str, Any]:
326
- """
327
- Instantiate a class from a target module path.
328
-
329
- Args:
330
- module_path: Full module path containing the class.
331
- class_name: Class name to instantiate.
332
- *args: Positional constructor arguments.
333
- **kwargs: Keyword constructor arguments.
334
-
335
- Returns:
336
- Unified status dictionary with instantiated object handle.
337
- """
 
 
 
 
338
  try:
339
- mod = self.modules.get(module_path) or importlib.import_module(module_path)
340
- cls = getattr(mod, class_name, None)
341
- if cls is None:
342
- return self._fail(
343
- "Class not found in module.",
344
- guidance="Use get_module_attributes(module_path) to verify class exports.",
345
- data={"module_path": module_path, "class_name": class_name},
346
- )
347
- instance = cls(*args, **kwargs)
348
- return self._ok(
349
- "Class instantiated successfully.",
350
- {"module_path": module_path, "class_name": class_name, "instance": instance},
351
- )
352
  except Exception as exc:
353
- return self._fail(
354
- "Class instantiation failed.",
355
- error=str(exc),
356
- guidance="Verify constructor arguments and required dependencies for this class.",
357
- data={"module_path": module_path, "class_name": class_name, "traceback": traceback.format_exc()},
358
- )
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
  import sys
 
 
 
5
 
6
+ try:
7
+ import importlib
8
+ except Exception:
9
+ importlib = None
 
10
 
11
+ try:
12
+ import inspect
13
+ except Exception:
14
+ inspect = None
15
 
16
+ try:
17
+ import pkgutil
18
+ except Exception:
19
+ pkgutil = None
20
 
 
 
 
21
 
22
+ CURRENT_DIR = Path(__file__).resolve().parent
23
+ SOURCE_DIR = CURRENT_DIR.parent.parent / "source"
24
+ if SOURCE_DIR.exists():
25
+ source_str = str(SOURCE_DIR)
26
+ if source_str not in sys.path:
27
+ sys.path.insert(0, source_str)
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ class Adapter:
31
+ def __init__(self, root_package: str = "mne") -> None:
32
+ self.root_package = root_package
33
+ self.loaded_modules: dict[str, object] = {}
34
+ self.failed_modules: dict[str, str] = {}
35
+ self.mode = "normal"
36
+ self._load_all_submodules()
37
+
38
+ def _load_all_submodules(self) -> dict[str, object]:
39
+ if importlib is None:
40
+ self.mode = "blackbox"
41
+ return {
42
+ "status": "fallback",
43
+ "mode": self.mode,
44
+ "reason": "importlib unavailable",
45
+ }
46
 
47
+ try:
48
+ root_mod = importlib.import_module(self.root_package)
49
+ self.loaded_modules[self.root_package] = root_mod
50
+ except Exception as exc:
51
+ self.mode = "blackbox"
52
+ self.failed_modules[self.root_package] = str(exc)
53
+ return {
54
+ "status": "fallback",
55
+ "mode": self.mode,
56
+ "reason": f"failed to import root package {self.root_package}: {exc}",
57
+ }
58
+
59
+ if pkgutil is not None and hasattr(root_mod, "__path__"):
60
+ for module_info in pkgutil.walk_packages(
61
+ root_mod.__path__,
62
+ prefix=f"{self.root_package}.",
63
+ ):
64
+ name = module_info.name
65
+ try:
66
+ mod = importlib.import_module(name)
67
+ self.loaded_modules[name] = mod
68
+ except Exception as exc:
69
+ self.failed_modules[name] = str(exc)
70
+
71
+ if not self.loaded_modules:
72
+ self.mode = "blackbox"
73
+ return {
74
+ "status": "fallback",
75
+ "mode": self.mode,
76
+ "reason": "no modules loaded",
77
+ }
78
+
79
+ return {
80
+ "status": "ok",
81
+ "mode": self.mode,
82
+ "loaded_count": len(self.loaded_modules),
83
+ "failed_count": len(self.failed_modules),
84
  }
85
 
86
+ def health(self) -> dict[str, object]:
87
+ status = "fallback" if self.mode == "blackbox" else "ok"
88
+ return {
89
+ "status": status,
90
+ "mode": self.mode,
91
+ "root_package": self.root_package,
92
+ "loaded_count": len(self.loaded_modules),
93
+ "failed_count": len(self.failed_modules),
94
+ }
 
 
95
 
96
+ def list_modules(self, include_failed: bool = True) -> dict[str, object]:
97
+ status = "fallback" if self.mode == "blackbox" else "ok"
98
+ result: dict[str, object] = {
99
+ "status": status,
100
+ "mode": self.mode,
101
+ "loaded_modules": sorted(self.loaded_modules.keys()),
102
+ }
103
+ if include_failed:
104
+ result["failed_modules"] = self.failed_modules
105
+ return result
106
 
107
+ def list_symbols(
108
  self,
109
+ module_name: str,
110
+ include_private: bool = False,
111
+ only_callables: bool = False,
112
+ ) -> dict[str, object]:
113
+ module = self.loaded_modules.get(module_name)
114
+ if module is None:
115
+ return {
116
+ "status": "error",
117
+ "error": f"module not loaded: {module_name}",
118
+ }
119
+
120
+ names = dir(module)
121
+ if not include_private:
122
+ names = [name for name in names if not name.startswith("_")]
123
+
124
+ if only_callables:
125
+ symbols = []
126
+ for name in names:
127
+ try:
128
+ value = getattr(module, name)
129
+ if callable(value):
130
+ symbols.append(name)
131
+ except Exception:
132
+ continue
133
+ else:
134
+ symbols = names
135
 
 
 
 
 
 
 
136
  return {
137
+ "status": "ok",
138
+ "module": module_name,
139
+ "count": len(symbols),
140
+ "symbols": sorted(symbols),
 
 
 
 
 
141
  }
142
 
143
+ def call_function(
144
+ self,
145
+ module_name: str,
146
+ function_name: str,
147
+ args: list[object] | None = None,
148
+ kwargs: dict[str, object] | None = None,
149
+ ) -> dict[str, object]:
150
+ module = self.loaded_modules.get(module_name)
151
+ if module is None:
152
+ return {
153
+ "status": "error",
154
+ "error": f"module not loaded: {module_name}",
155
+ }
156
+
157
+ if inspect is None:
158
+ return {
159
+ "status": "fallback",
160
  "mode": self.mode,
161
+ "error": "inspect unavailable",
162
+ }
163
+
164
+ fn = getattr(module, function_name, None)
165
+ if fn is None or not callable(fn):
166
+ return {
167
+ "status": "error",
168
+ "error": f"callable not found: {module_name}.{function_name}",
169
+ }
170
+
171
+ local_args = args or []
172
+ local_kwargs = kwargs or {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  try:
174
+ output = fn(*local_args, **local_kwargs)
175
+ return {
176
+ "status": "ok",
177
+ "module": module_name,
178
+ "function": function_name,
179
+ "result": output,
180
+ }
181
  except Exception as exc:
182
+ return {
183
+ "status": "error",
184
+ "module": module_name,
185
+ "function": function_name,
186
+ "error": str(exc),
187
+ }
 
 
 
 
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  def create_instance(
190
  self,
191
+ module_name: str,
192
  class_name: str,
193
+ args: list[object] | None = None,
194
+ kwargs: dict[str, object] | None = None,
195
+ ) -> dict[str, object]:
196
+ module = self.loaded_modules.get(module_name)
197
+ if module is None:
198
+ return {
199
+ "status": "error",
200
+ "error": f"module not loaded: {module_name}",
201
+ }
202
+
203
+ cls = getattr(module, class_name, None)
204
+ if cls is None:
205
+ return {
206
+ "status": "error",
207
+ "error": f"class not found: {module_name}.{class_name}",
208
+ }
209
+
210
+ local_args = args or []
211
+ local_kwargs = kwargs or {}
212
  try:
213
+ instance = cls(*local_args, **local_kwargs)
214
+ return {
215
+ "status": "ok",
216
+ "module": module_name,
217
+ "class": class_name,
218
+ "instance_type": type(instance).__name__,
219
+ "instance_repr": repr(instance),
220
+ }
 
 
 
 
 
221
  except Exception as exc:
222
+ return {
223
+ "status": "error",
224
+ "module": module_name,
225
+ "class": class_name,
226
+ "error": str(exc),
227
+ }
mne-python/mcp_output/mcp_plugin/main.py CHANGED
@@ -1,13 +1,7 @@
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 mcp_service import create_app
2
 
 
 
 
 
3
 
4
  if __name__ == "__main__":
5
+ # Local stdio entry point only (Claude Desktop / CLI); not for Docker/web deployment.
6
+ app = create_app()
7
+ app.run()
mne-python/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,215 +1,319 @@
1
- import os
 
 
2
  import sys
3
- from typing import Optional, Dict, Any, List
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
 
14
- mcp = FastMCP("mne_python_service")
 
 
 
15
 
 
 
 
 
16
 
17
- def _safe_import_mne():
18
- try:
19
- import mne # type: ignore
20
- return True, mne, None
21
- except Exception as exc:
22
- return False, None, str(exc)
23
 
 
 
 
 
24
 
25
- @mcp.tool(name="mne_get_version", description="Get installed MNE version information.")
26
- def mne_get_version() -> Dict[str, Any]:
27
- """
28
- Return MNE package version.
 
 
 
 
 
29
 
30
- Returns:
31
- Dict with:
32
- - success: bool indicating operation status
33
- - result: version string when successful
34
- - error: error string when failed
35
- """
36
- ok, mne_mod, err = _safe_import_mne()
37
- if not ok:
38
- return {"success": False, "result": None, "error": err}
39
- try:
40
- version = getattr(mne_mod, "__version__", "unknown")
41
- return {"success": True, "result": version, "error": None}
42
- except Exception as exc:
43
- return {"success": False, "result": None, "error": str(exc)}
44
 
45
 
46
- @mcp.tool(name="mne_get_config", description="Read MNE configuration value by key.")
47
- def mne_get_config(key: str, default: Optional[str] = None) -> Dict[str, Any]:
48
- """
49
- Read a single MNE config key.
 
 
50
 
51
- Parameters:
52
- key: Configuration key to lookup.
53
- default: Optional fallback if key is not found.
54
 
55
- Returns:
56
- Dict with:
57
- - success: bool
58
- - result: config value
59
- - error: error message when failed
60
- """
61
- ok, mne_mod, err = _safe_import_mne()
62
- if not ok:
63
- return {"success": False, "result": None, "error": err}
64
- try:
65
- value = mne_mod.get_config(key=key, default=default)
66
- return {"success": True, "result": value, "error": None}
67
- except Exception as exc:
68
- return {"success": False, "result": None, "error": str(exc)}
69
 
 
 
 
 
 
 
70
 
71
- @mcp.tool(name="mne_set_config", description="Set an MNE configuration key to a value.")
72
- def mne_set_config(key: str, value: str, set_env: bool = False) -> Dict[str, Any]:
73
- """
74
- Set a single MNE config key.
75
-
76
- Parameters:
77
- key: Configuration key.
78
- value: Configuration value.
79
- set_env: If True, also set environment variable for current process.
80
-
81
- Returns:
82
- Dict with:
83
- - success: bool
84
- - result: True when set successfully
85
- - error: error message when failed
86
- """
87
- ok, mne_mod, err = _safe_import_mne()
88
- if not ok:
89
- return {"success": False, "result": None, "error": err}
90
- try:
91
- mne_mod.set_config(key=key, value=value, set_env=set_env)
92
- return {"success": True, "result": True, "error": None}
93
- except Exception as exc:
94
- return {"success": False, "result": None, "error": str(exc)}
95
 
96
 
97
- @mcp.tool(name="mne_create_info", description="Create an MNE Info object from channels and sampling frequency.")
98
- def mne_create_info(ch_names: List[str], sfreq: float, ch_types: Optional[List[str]] = None) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  """
100
- Create a lightweight channel metadata structure.
101
-
102
- Parameters:
103
- ch_names: List of channel names.
104
- sfreq: Sampling frequency in Hz.
105
- ch_types: Optional list of channel types aligned with ch_names.
106
-
107
- Returns:
108
- Dict with:
109
- - success: bool
110
- - result: serializable summary of created Info
111
- - error: error message when failed
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  """
113
- ok, mne_mod, err = _safe_import_mne()
114
- if not ok:
115
- return {"success": False, "result": None, "error": err}
116
  try:
117
- info = mne_mod.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
118
- result = {
119
  "nchan": int(info["nchan"]),
120
  "sfreq": float(info["sfreq"]),
121
  "ch_names": list(info["ch_names"]),
122
- "highpass": float(info.get("highpass", 0.0) or 0.0),
123
- "lowpass": float(info.get("lowpass", 0.0) or 0.0),
124
  }
125
- return {"success": True, "result": result, "error": None}
126
  except Exception as exc:
127
- return {"success": False, "result": None, "error": str(exc)}
128
-
129
-
130
- @mcp.tool(name="mne_compute_events", description="Detect events from a stim channel in a raw FIF file.")
131
- def mne_compute_events(
132
- raw_fif_path: str,
133
- stim_channel: Optional[str] = None,
134
- shortest_event: int = 1,
135
- min_duration: float = 0.0,
136
- ) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  """
138
- Load raw FIF and compute events from stimulation channel.
139
-
140
- Parameters:
141
- raw_fif_path: Path to a readable raw FIF file.
142
- stim_channel: Optional stim channel name. If None, MNE default detection is used.
143
- shortest_event: Minimum number of samples for an event.
144
- min_duration: Minimum event duration in seconds.
145
-
146
- Returns:
147
- Dict with:
148
- - success: bool
149
- - result: event count and preview rows
150
- - error: error message when failed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  """
152
- ok, mne_mod, err = _safe_import_mne()
153
- if not ok:
154
- return {"success": False, "result": None, "error": err}
155
  try:
156
- raw = mne_mod.io.read_raw_fif(raw_fif_path, preload=False, verbose=False)
157
- events = mne_mod.find_events(
158
- raw,
159
- stim_channel=stim_channel,
160
- shortest_event=shortest_event,
161
- min_duration=min_duration,
 
 
 
162
  verbose=False,
163
  )
164
- preview = events[:20].tolist() if len(events) > 0 else []
165
- return {
166
- "success": True,
167
- "result": {"count": int(len(events)), "preview": preview},
168
- "error": None,
169
- }
 
 
 
170
  except Exception as exc:
171
- return {"success": False, "result": None, "error": str(exc)}
172
 
173
 
174
- @mcp.tool(name="mne_estimate_rank", description="Estimate data rank from an epochs or raw FIF file.")
175
- def mne_estimate_rank(fif_path: str, tol: Optional[float] = None) -> Dict[str, Any]:
176
- """
177
- Estimate numerical rank from MNE data object loaded from FIF.
178
-
179
- Parameters:
180
- fif_path: Path to raw or epochs FIF file.
181
- tol: Optional tolerance passed to rank estimator.
182
 
183
- Returns:
184
- Dict with:
185
- - success: bool
186
- - result: estimated rank dictionary or scalar
187
- - error: error message when failed
188
  """
189
- ok, mne_mod, err = _safe_import_mne()
190
- if not ok:
191
- return {"success": False, "result": None, "error": err}
192
  try:
193
- result_obj: Any
194
- try:
195
- raw = mne_mod.io.read_raw_fif(fif_path, preload=False, verbose=False)
196
- result_obj = mne_mod.compute_rank(raw, tol=tol, verbose=False)
197
- except Exception:
198
- epochs = mne_mod.read_epochs(fif_path, preload=False, verbose=False)
199
- result_obj = mne_mod.compute_rank(epochs, tol=tol, verbose=False)
200
-
201
- if isinstance(result_obj, dict):
202
- serializable = {str(k): int(v) for k, v in result_obj.items()}
203
- else:
204
- serializable = int(result_obj)
205
- return {"success": True, "result": serializable, "error": None}
206
  except Exception as exc:
207
- return {"success": False, "result": None, "error": str(exc)}
208
 
209
 
210
- def create_app() -> FastMCP:
211
  return mcp
212
 
213
 
214
  if __name__ == "__main__":
215
- mcp.run()
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
  import sys
 
5
 
6
+ try:
7
+ from fastmcp import FastMCP
8
+ except Exception:
9
+ FastMCP = None
 
 
10
 
11
+ try:
12
+ import importlib
13
+ except Exception:
14
+ importlib = None
15
 
16
+ try:
17
+ np = importlib.import_module("numpy") if importlib is not None else None
18
+ except Exception:
19
+ np = None
20
 
21
+ try:
22
+ mne = importlib.import_module("mne") if importlib is not None else None
23
+ except Exception:
24
+ mne = None
25
 
26
+ try:
27
+ mne_channels = importlib.import_module("mne.channels") if importlib is not None else None
28
+ except Exception:
29
+ mne_channels = None
 
 
30
 
31
+ try:
32
+ mne_io = importlib.import_module("mne.io") if importlib is not None else None
33
+ except Exception:
34
+ mne_io = None
35
 
36
+ try:
37
+ mne_time_frequency = importlib.import_module("mne.time_frequency") if importlib is not None else None
38
+ psd_array_welch = (
39
+ getattr(mne_time_frequency, "psd_array_welch", None)
40
+ if mne_time_frequency is not None
41
+ else None
42
+ )
43
+ except Exception:
44
+ psd_array_welch = None
45
 
46
+ from adapter import Adapter
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
 
49
+ CURRENT_DIR = Path(__file__).resolve().parent
50
+ SOURCE_DIR = CURRENT_DIR.parent.parent / "source"
51
+ if SOURCE_DIR.exists():
52
+ source_str = str(SOURCE_DIR)
53
+ if source_str not in sys.path:
54
+ sys.path.insert(0, source_str)
55
 
 
 
 
56
 
57
+ class _FallbackMCP:
58
+ def __init__(self) -> None:
59
+ self.tools: list[object] = []
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ def tool(self, name: str, description: str):
62
+ def decorator(func):
63
+ setattr(func, "name", name)
64
+ setattr(func, "description", description)
65
+ self.tools.append(func)
66
+ return func
67
 
68
+ return decorator
69
+
70
+ def run(self, *args, **kwargs) -> None:
71
+ raise RuntimeError("fastmcp is not available")
72
+
73
+
74
+ mcp = FastMCP("mne_python_mcp_service") if FastMCP is not None else _FallbackMCP()
75
+ adapter = Adapter(root_package="mne")
76
+
77
+
78
+ def _ok(result: object) -> dict[str, object]:
79
+ return {"success": True, "result": result, "error": None}
80
+
81
+
82
+ def _err(message: str) -> dict[str, object]:
83
+ return {"success": False, "result": None, "error": message}
 
 
 
 
 
 
 
 
84
 
85
 
86
+ @mcp.tool(name="health_check", description="Report dependency and adapter health status")
87
+ def health_check() -> dict[str, object]:
88
+ """Return availability status for key MCP dependencies and mne imports."""
89
+ deps = {
90
+ "fastmcp": FastMCP is not None,
91
+ "numpy": np is not None,
92
+ "mne": mne is not None,
93
+ "mne.channels": mne_channels is not None,
94
+ "mne.io": mne_io is not None,
95
+ "mne.time_frequency.psd_array_welch": psd_array_welch is not None,
96
+ }
97
+ return _ok({"dependencies": deps, "adapter": adapter.health()})
98
+
99
+
100
+ @mcp.tool(name="get_mne_version", description="Get installed MNE version information")
101
+ def get_mne_version() -> dict[str, object]:
102
+ """Return installed MNE version and module file path."""
103
+ if mne is None:
104
+ return _err("mne is not available")
105
+ return _ok({"version": mne.__version__, "module": getattr(mne, "__file__", None)})
106
+
107
+
108
+ @mcp.tool(name="list_builtin_montages", description="List available built-in electrode montages")
109
+ def list_builtin_montages() -> dict[str, object]:
110
+ """Return names of built-in montages from mne.channels.
111
+
112
+ Returns
113
+ -------
114
+ dict
115
+ Standard MCP response with montage names and count.
116
  """
117
+ if mne_channels is None:
118
+ return _err("mne.channels is not available")
119
+ try:
120
+ names = mne_channels.get_builtin_montages(descriptions=False)
121
+ return _ok({"count": len(names), "montages": names})
122
+ except Exception as exc:
123
+ return _err(str(exc))
124
+
125
+
126
+ @mcp.tool(name="create_info", description="Create and summarize an MNE Info object")
127
+ def create_info(
128
+ ch_names: list[str],
129
+ sfreq: float,
130
+ ch_types: str = "eeg",
131
+ ) -> dict[str, object]:
132
+ """Create an MNE Info object.
133
+
134
+ Parameters
135
+ ----------
136
+ ch_names : list[str]
137
+ Channel names.
138
+ sfreq : float
139
+ Sampling frequency in Hz.
140
+ ch_types : str
141
+ Channel type applied to all channels.
142
  """
143
+ if mne is None:
144
+ return _err("mne is not available")
 
145
  try:
146
+ info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
147
+ summary = {
148
  "nchan": int(info["nchan"]),
149
  "sfreq": float(info["sfreq"]),
150
  "ch_names": list(info["ch_names"]),
151
+ "ch_types": list(info.get_channel_types()),
 
152
  }
153
+ return _ok(summary)
154
  except Exception as exc:
155
+ return _err(str(exc))
156
+
157
+
158
+ @mcp.tool(name="create_raw_array", description="Create RawArray metadata from matrix-like data")
159
+ def create_raw_array(
160
+ data: list[list[float]],
161
+ sfreq: float,
162
+ ch_names: list[str],
163
+ ch_types: str = "eeg",
164
+ ) -> dict[str, object]:
165
+ """Create an in-memory RawArray and return metadata.
166
+
167
+ Parameters
168
+ ----------
169
+ data : list[list[float]]
170
+ Data shaped as [n_channels, n_times].
171
+ sfreq : float
172
+ Sampling frequency in Hz.
173
+ ch_names : list[str]
174
+ Channel names, length must equal number of rows in data.
175
+ ch_types : str
176
+ Channel type applied to all channels.
177
+ """
178
+ if np is None or mne is None or mne_io is None:
179
+ return _err("numpy or mne.io is not available")
180
+ try:
181
+ arr = np.asarray(data, dtype=float)
182
+ if arr.ndim != 2:
183
+ return _err("data must be a 2D array [n_channels, n_times]")
184
+ if arr.shape[0] != len(ch_names):
185
+ return _err("len(ch_names) must equal n_channels")
186
+ info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
187
+ raw = mne_io.RawArray(arr, info, verbose=False)
188
+ return _ok(
189
+ {
190
+ "n_channels": int(raw.info["nchan"]),
191
+ "n_times": int(raw.n_times),
192
+ "duration_sec": float(raw.n_times / raw.info["sfreq"]),
193
+ "sfreq": float(raw.info["sfreq"]),
194
+ }
195
+ )
196
+ except Exception as exc:
197
+ return _err(str(exc))
198
+
199
+
200
+ @mcp.tool(name="filter_data_array", description="Band-pass or high/low-pass filter array data")
201
+ def filter_data_array(
202
+ data: list[list[float]],
203
+ sfreq: float,
204
+ l_freq: float | None,
205
+ h_freq: float | None,
206
+ ) -> dict[str, object]:
207
+ """Filter array data with MNE filtering utilities.
208
+
209
+ Parameters
210
+ ----------
211
+ data : list[list[float]]
212
+ Input array shaped as [n_channels, n_times].
213
+ sfreq : float
214
+ Sampling frequency in Hz.
215
+ l_freq : float | None
216
+ Lower passband edge in Hz. Use None for low-pass only.
217
+ h_freq : float | None
218
+ Upper passband edge in Hz. Use None for high-pass only.
219
  """
220
+ if np is None or mne is None:
221
+ return _err("numpy or mne is not available")
222
+ try:
223
+ arr = np.asarray(data, dtype=float)
224
+ if arr.ndim != 2:
225
+ return _err("data must be a 2D array [n_channels, n_times]")
226
+ filtered = mne.filter.filter_data(
227
+ data=arr,
228
+ sfreq=sfreq,
229
+ l_freq=l_freq,
230
+ h_freq=h_freq,
231
+ verbose=False,
232
+ )
233
+ return _ok(
234
+ {
235
+ "shape": [int(filtered.shape[0]), int(filtered.shape[1])],
236
+ "mean": float(np.mean(filtered)),
237
+ "std": float(np.std(filtered)),
238
+ "data": filtered.tolist(),
239
+ }
240
+ )
241
+ except Exception as exc:
242
+ return _err(str(exc))
243
+
244
+
245
+ @mcp.tool(name="compute_psd_welch", description="Compute PSD using Welch on array data")
246
+ def compute_psd_welch(
247
+ data: list[list[float]],
248
+ sfreq: float,
249
+ fmin: float = 0.0,
250
+ fmax: float = 120.0,
251
+ n_fft: int = 256,
252
+ ) -> dict[str, object]:
253
+ """Compute power spectral density with Welch method.
254
+
255
+ Parameters
256
+ ----------
257
+ data : list[list[float]]
258
+ Input array shaped as [n_channels, n_times].
259
+ sfreq : float
260
+ Sampling frequency in Hz.
261
+ fmin : float
262
+ Minimum frequency of interest.
263
+ fmax : float
264
+ Maximum frequency of interest.
265
+ n_fft : int
266
+ FFT length.
267
  """
268
+ if np is None or psd_array_welch is None:
269
+ return _err("numpy or psd_array_welch is not available")
 
270
  try:
271
+ arr = np.asarray(data, dtype=float)
272
+ if arr.ndim != 2:
273
+ return _err("data must be a 2D array [n_channels, n_times]")
274
+ psds, freqs = psd_array_welch(
275
+ x=arr,
276
+ sfreq=sfreq,
277
+ fmin=fmin,
278
+ fmax=fmax,
279
+ n_fft=n_fft,
280
  verbose=False,
281
  )
282
+ mean_psd = np.mean(psds, axis=0)
283
+ return _ok(
284
+ {
285
+ "n_channels": int(arr.shape[0]),
286
+ "n_freqs": int(len(freqs)),
287
+ "freqs": freqs.tolist(),
288
+ "mean_psd": mean_psd.tolist(),
289
+ }
290
+ )
291
  except Exception as exc:
292
+ return _err(str(exc))
293
 
294
 
295
+ @mcp.tool(name="list_loaded_modules", description="List modules loaded by adapter")
296
+ def list_loaded_modules(limit: int = 100) -> dict[str, object]:
297
+ """List loaded and failed modules discovered by the Adapter.
 
 
 
 
 
298
 
299
+ Parameters
300
+ ----------
301
+ limit : int
302
+ Maximum number of loaded modules returned.
 
303
  """
 
 
 
304
  try:
305
+ modules = adapter.list_modules(include_failed=True)
306
+ loaded = modules.get("loaded_modules", [])
307
+ if isinstance(loaded, list):
308
+ modules["loaded_modules"] = loaded[: max(0, limit)]
309
+ return _ok(modules)
 
 
 
 
 
 
 
 
310
  except Exception as exc:
311
+ return _err(str(exc))
312
 
313
 
314
+ def create_app():
315
  return mcp
316
 
317
 
318
  if __name__ == "__main__":
319
+ mcp.run()
mne-python/mcp_output/requirements.txt CHANGED
@@ -1,16 +1,4 @@
1
  fastmcp
2
- fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
- decorator >= 5.1
6
- jinja2 >= 3.1
7
- lazy_loader >= 0.3
8
- matplotlib >= 3.8
9
- numpy >= 1.26, < 3
10
- packaging
11
- pooch >= 1.5
12
- scipy >= 1.12
13
- tqdm >= 4.66
14
- nest-asyncio2
15
- pymef
16
- pyobjc-framework-Cocoa >=5.2.0;platform_system=='Darwin'
 
1
  fastmcp
2
+ mne
3
+ numpy
4
+ scipy
 
 
 
 
 
 
 
 
 
 
 
 
mne-python/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
+ from pathlib import Path
5
+ import sys
6
+
7
 
8
+ CURRENT_DIR = Path(__file__).resolve().parent
9
+ PLUGIN_DIR = CURRENT_DIR / "mcp_plugin"
10
+ plugin_str = str(PLUGIN_DIR)
11
+ if plugin_str not in sys.path:
12
+ sys.path.insert(0, plugin_str)
13
 
14
  from mcp_service import create_app
15
 
16
+
17
+ def main() -> None:
18
+ transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower()
19
+ port = int(os.getenv("MCP_PORT", "8000"))
20
  app = create_app()
21
+
22
+ if transport == "stdio":
 
 
 
 
 
 
 
23
  app.run()
24
+ return
25
+
26
+ if transport == "http":
27
+ try:
28
+ app.run(transport="http", host="0.0.0.0", port=port)
29
+ except TypeError:
30
+ app.run(transport="http", port=port)
31
+ return
32
+
33
+ raise ValueError(f"Unsupported MCP_TRANSPORT: {transport}")
34
+
35
 
36
  if __name__ == "__main__":
37
  main()
port.json CHANGED
@@ -1,5 +1 @@
1
- {
2
- "repo": "mne-python",
3
- "port": 7938,
4
- "timestamp": 1773462397
5
- }
 
1
+ {"port": 7860}
 
 
 
 
requirements.txt CHANGED
@@ -1,16 +1,6 @@
1
  fastmcp
 
 
 
2
  fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
- decorator >= 5.1
6
- jinja2 >= 3.1
7
- lazy_loader >= 0.3
8
- matplotlib >= 3.8
9
- numpy >= 1.26, < 3
10
- packaging
11
- pooch >= 1.5
12
- scipy >= 1.12
13
- tqdm >= 4.66
14
- nest-asyncio2
15
- pymef
16
- pyobjc-framework-Cocoa >=5.2.0;platform_system=='Darwin'
 
1
  fastmcp
2
+ mne
3
+ numpy
4
+ scipy
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 { "mne-python" }
4
- $entryUrl = if ($env:MCP_ENTRY_URL) { $env:MCP_ENTRY_URL } else { "http://localhost:7938/mcp" }
5
- $imageName = if ($env:MCP_IMAGE_NAME) { $env:MCP_IMAGE_NAME } else { "mne-python-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 7938:7860 $imageName
 
 
 
1
+ $port = (Get-Content -Raw -Path "port.json" | ConvertFrom-Json).port
2
+ $imageName = "mne-python-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 -it -p "${port}:${port}" -e MCP_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:-mne-python}"
5
- mcp_entry_url="${MCP_ENTRY_URL:-http://localhost:7938/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 mne-python-mcp .
75
- docker run --rm -p 7938:7860 mne-python-mcp
 
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
+
4
+ PORT=$(python3 -c "import json; print(json.load(open('port.json'))['port'])")
5
+ IMAGE_NAME="mne-python-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 -it -p "${PORT}:${PORT}" -e MCP_PORT="${PORT}" "${IMAGE_NAME}"