Spaces:
Sleeping
Sleeping
Upload 14 files
Browse files- Dockerfile +13 -8
- README.md +59 -4
- app.py +58 -29
- mne-python/mcp_output/README_MCP.md +158 -129
- mne-python/mcp_output/mcp_plugin/adapter.py +200 -331
- mne-python/mcp_output/mcp_plugin/main.py +3 -9
- mne-python/mcp_output/mcp_plugin/mcp_service.py +276 -172
- mne-python/mcp_output/requirements.txt +3 -15
- mne-python/mcp_output/start_mcp.py +26 -19
- port.json +1 -5
- requirements.txt +4 -14
- run_docker.ps1 +7 -25
- run_docker.sh +10 -73
Dockerfile
CHANGED
|
@@ -1,18 +1,23 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
|
| 7 |
WORKDIR /app
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
COPY --chown=user . /app
|
| 13 |
ENV MCP_TRANSPORT=http
|
| 14 |
ENV MCP_PORT=7860
|
| 15 |
|
| 16 |
EXPOSE 7860
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
| 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
)
|
|
|
|
| 13 |
|
| 14 |
@app.get("/")
|
| 15 |
-
def root():
|
| 16 |
return {
|
| 17 |
-
"service": "
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
|
|
|
| 21 |
}
|
| 22 |
|
|
|
|
| 23 |
@app.get("/health")
|
| 24 |
-
def
|
| 25 |
-
return {"status": "healthy"
|
|
|
|
| 26 |
|
| 27 |
@app.get("/tools")
|
| 28 |
-
def list_tools():
|
| 29 |
try:
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
if __name__ == "__main__":
|
| 43 |
import uvicorn
|
| 44 |
-
|
| 45 |
-
uvicorn.run(app, host="0.0.0.0", 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
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
-
|
| 9 |
-
-
|
| 10 |
-
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
- **
|
| 17 |
-
|
| 18 |
-
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
-
|
| 25 |
-
-
|
| 26 |
-
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
-
|
| 33 |
-
-
|
| 34 |
-
- `
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
-
|
| 71 |
-
|
| 72 |
-
-
|
| 73 |
-
|
| 74 |
-
- `
|
| 75 |
-
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
-
|
| 91 |
-
|
| 92 |
-
-
|
| 93 |
-
|
| 94 |
-
- `
|
| 95 |
-
-
|
| 96 |
-
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
##
|
| 112 |
-
|
| 113 |
-
- **
|
| 114 |
-
-
|
| 115 |
-
- **
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 2 |
import sys
|
| 3 |
-
import importlib
|
| 4 |
-
import traceback
|
| 5 |
-
from typing import Any, Dict, List, Optional
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
sys.path.insert(0, source_path)
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
| 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 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 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 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
)
|
| 65 |
-
self.available = loaded > 0
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
def
|
| 74 |
self,
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "
|
| 95 |
-
"
|
| 96 |
-
"
|
| 97 |
-
"
|
| 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 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
"mode": self.mode,
|
| 119 |
-
"
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
)
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 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 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
except Exception as exc:
|
| 180 |
-
return
|
| 181 |
-
"
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 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 |
-
|
| 322 |
class_name: str,
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
) ->
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
try:
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 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
|
| 354 |
-
"
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 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 |
-
|
| 13 |
-
app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 2 |
import sys
|
| 3 |
-
from typing import Optional, Dict, Any, List
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
if source_path not in sys.path:
|
| 10 |
-
sys.path.insert(0, source_path)
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
except Exception as exc:
|
| 22 |
-
return False, None, str(exc)
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
|
| 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 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
Parameters:
|
| 52 |
-
key: Configuration key to lookup.
|
| 53 |
-
default: Optional fallback if key is not found.
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 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 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 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="
|
| 98 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
"""
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
"""
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
return {"success": False, "result": None, "error": err}
|
| 116 |
try:
|
| 117 |
-
info =
|
| 118 |
-
|
| 119 |
"nchan": int(info["nchan"]),
|
| 120 |
"sfreq": float(info["sfreq"]),
|
| 121 |
"ch_names": list(info["ch_names"]),
|
| 122 |
-
"
|
| 123 |
-
"lowpass": float(info.get("lowpass", 0.0) or 0.0),
|
| 124 |
}
|
| 125 |
-
return
|
| 126 |
except Exception as exc:
|
| 127 |
-
return
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
@mcp.tool(name="
|
| 131 |
-
def
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
) ->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
"""
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
"""
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
return {"success": False, "result": None, "error": err}
|
| 155 |
try:
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
| 162 |
verbose=False,
|
| 163 |
)
|
| 164 |
-
|
| 165 |
-
return
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
| 170 |
except Exception as exc:
|
| 171 |
-
return
|
| 172 |
|
| 173 |
|
| 174 |
-
@mcp.tool(name="
|
| 175 |
-
def
|
| 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 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 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 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 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
|
| 208 |
|
| 209 |
|
| 210 |
-
def create_app()
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 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 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
|
| 13 |
from mcp_service import create_app
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
app = create_app()
|
| 18 |
-
|
| 19 |
-
|
| 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
|
| 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 |
-
|
| 2 |
-
$
|
| 3 |
-
|
| 4 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 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}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|