Skip to content

Commit 6807ed2

Browse files
HanSur94claude
andcommitted
docs: improve docstrings across all modules + update README
Source code documentation: - pool/engine.py, pool/manager.py: engine lifecycle, pool scaling - jobs/executor.py, models.py, tracker.py: job state machine, pruning - monitoring/collector.py, store.py, health.py, routes.py, dashboard.py - output/plotly_style_mapper.py: style mapping tables - tools/custom.py, monitoring.py: custom tool framework - config.py: configuration loading and validation README updates: - Added Windows one-click installer section (install.bat) - Updated MATLAB version requirement to R2022b+ - Added install.bat instructions for Windows 10/11 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7211999 commit 6807ed2

16 files changed

Lines changed: 320 additions & 34 deletions

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ A Python MCP server that connects **any AI agent** (Claude, Cursor, Copilot, cus
5555
| Multi-user (SSE) | Session isolation with per-user workspaces |
5656
| Custom tools | Expose your `.m` functions as MCP tools via YAML |
5757
| Progress reporting | Long jobs report percentage back to the agent |
58-
| Cross-platform | Windows + macOS, MATLAB 2020b+ |
58+
| Cross-platform | Windows + macOS, MATLAB R2022b+ |
59+
| One-click Windows install | Offline `install.bat` — no admin rights needed |
5960

6061
## MATLAB Plot Conversion to Interactive Plotly
6162

@@ -92,7 +93,7 @@ Line styles, colors, markers, legends, and axis labels are all preserved in the
9293
### Prerequisites
9394

9495
- **Python 3.10+**
95-
- **MATLAB 2020b+** with the [MATLAB Engine API for Python](https://www.mathworks.com/help/matlab/matlab-engine-for-python.html) installed
96+
- **MATLAB R2022b+** with the [MATLAB Engine API for Python](https://www.mathworks.com/help/matlab/matlab-engine-for-python.html) installed
9697

9798
```bash
9899
# Install MATLAB Engine API (from your MATLAB installation)
@@ -103,6 +104,18 @@ pip install .
103104

104105
### Install the server
105106

107+
**Windows (one-click, no admin needed):**
108+
109+
```cmd
110+
git clone https://github.com/HanSur94/matlab-mcp-server-python.git
111+
cd matlab-mcp-server-python
112+
install.bat
113+
```
114+
115+
The installer auto-detects MATLAB, creates a virtual environment, and installs everything from bundled wheels — fully offline, no internet required. Works on Windows 10/11 with Python 3.10, 3.11, or 3.12.
116+
117+
**macOS / Linux:**
118+
106119
```bash
107120
# Option 1: Install from PyPI
108121
pip install matlab-mcp-python
@@ -658,7 +671,7 @@ AI Agent (Claude, Cursor, etc.)
658671
└──────────┬──────────────────┘ └─────────┬──────────────┘
659672
│ │
660673
┌──────────▼──────────────────┐ ┌─────────▼──────────────┐
661-
│ MATLAB Engines (2020b+) │ │ Dashboard (Starlette) │
674+
│ MATLAB Engines (R2022b+) │ │ Dashboard (Starlette) │
662675
│ Engine 1 │ Engine 2 │ ... │ │ /health /metrics │
663676
│ Workspace isolation │ │ /dashboard (Plotly.js) │
664677
└──────────────────────────────┘ └─────────────────────────┘

src/matlab_mcp/config.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020

2121
class ServerConfig(BaseModel):
22+
"""General server settings (name, transport, host/port, logging, drain)."""
23+
2224
name: str = "matlab-mcp-server"
2325
transport: Literal["stdio", "sse"] = "stdio"
2426
host: str = "0.0.0.0"
@@ -30,6 +32,8 @@ class ServerConfig(BaseModel):
3032

3133

3234
class PoolConfig(BaseModel):
35+
"""MATLAB engine pool sizing, timeouts, and auto-scaling thresholds."""
36+
3337
min_engines: int = 2
3438
max_engines: int = 10
3539
scale_down_idle_timeout: int = 900
@@ -41,6 +45,8 @@ class PoolConfig(BaseModel):
4145

4246

4347
class ExecutionConfig(BaseModel):
48+
"""Code execution timeouts, workspace isolation, and temp directory."""
49+
4450
sync_timeout: int = 30
4551
max_execution_time: int = 86400
4652
workspace_isolation: bool = True
@@ -50,20 +56,28 @@ class ExecutionConfig(BaseModel):
5056

5157

5258
class WorkspaceConfig(BaseModel):
59+
"""MATLAB workspace setup: default paths and startup commands."""
60+
5361
default_paths: List[str] = Field(default_factory=list)
5462
startup_commands: List[str] = Field(default_factory=lambda: ["format long"])
5563

5664

5765
class ToolboxesConfig(BaseModel):
66+
"""Toolbox visibility: whitelist, blacklist, or expose all."""
67+
5868
mode: Literal["whitelist", "blacklist", "all"] = "whitelist"
5969
list: List[str] = Field(default_factory=list)
6070

6171

6272
class CustomToolsConfig(BaseModel):
73+
"""Path to the YAML file defining user-provided custom MCP tools."""
74+
6375
config_file: str = "./custom_tools.yaml"
6476

6577

6678
class SecurityConfig(BaseModel):
79+
"""Security controls: blocked MATLAB functions, upload limits, proxy auth."""
80+
6781
blocked_functions_enabled: bool = True
6882
blocked_functions: List[str] = Field(
6983
default_factory=lambda: [
@@ -77,12 +91,16 @@ class SecurityConfig(BaseModel):
7791

7892

7993
class CodeCheckerConfig(BaseModel):
94+
"""Code linting (checkcode/mlint) toggle and severity filter."""
95+
8096
enabled: bool = True
8197
auto_check_before_execute: bool = False
8298
severity_levels: List[str] = Field(default_factory=lambda: ["error", "warning"])
8399

84100

85101
class OutputConfig(BaseModel):
102+
"""Output formatting: Plotly conversion, image format, thumbnails, truncation."""
103+
86104
plotly_conversion: bool = True
87105
static_image_format: Literal["png", "jpg", "svg"] = "png"
88106
static_image_dpi: int = 150
@@ -93,12 +111,16 @@ class OutputConfig(BaseModel):
93111

94112

95113
class SessionsConfig(BaseModel):
114+
"""Session limits, idle timeout, and finished-job retention."""
115+
96116
max_sessions: int = 50
97117
session_timeout: int = 3600
98118
job_retention_seconds: int = 86400
99119

100120

101121
class MonitoringConfig(BaseModel):
122+
"""Monitoring subsystem: sampling interval, retention, DB path, dashboard."""
123+
102124
enabled: bool = True
103125
sample_interval: int = 10
104126
retention_days: int = 7
@@ -108,6 +130,12 @@ class MonitoringConfig(BaseModel):
108130

109131

110132
class AppConfig(BaseModel):
133+
"""Top-level application configuration aggregating all sub-configs.
134+
135+
Relative filesystem paths are resolved to absolute paths via
136+
:meth:`resolve_paths` after loading.
137+
"""
138+
111139
server: ServerConfig = Field(default_factory=ServerConfig)
112140
pool: PoolConfig = Field(default_factory=PoolConfig)
113141
execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
@@ -125,6 +153,12 @@ class AppConfig(BaseModel):
125153

126154
@model_validator(mode="after")
127155
def validate_pool(self) -> "AppConfig":
156+
"""Validate pool constraints after model construction.
157+
158+
Raises ``ValueError`` if ``min_engines > max_engines`` and emits a
159+
warning on macOS when ``max_engines > 4`` due to known stability
160+
limitations.
161+
"""
128162
if self.pool.min_engines > self.pool.max_engines:
129163
raise ValueError(
130164
f"pool.min_engines ({self.pool.min_engines}) must not exceed "
@@ -191,7 +225,10 @@ def _apply_env_overrides(data: dict) -> dict:
191225
def load_config(path: Optional[Path] = None) -> AppConfig:
192226
"""Load application config from a YAML file with env var overrides.
193227
194-
If *path* is None or the file does not exist, default values are used.
228+
If *path* is ``None`` or the file does not exist, default values are
229+
used. After parsing, ``MATLAB_MCP_*`` environment variables are
230+
applied (see :func:`_apply_env_overrides`) and relative paths are
231+
resolved against the config file's parent directory.
195232
"""
196233
data: dict = {}
197234
config_dir = Path.cwd()

src/matlab_mcp/jobs/executor.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,12 @@ def _inject_job_context(
168168
job: Job,
169169
temp_dir: Optional[str],
170170
) -> None:
171-
"""Inject job metadata into the MATLAB workspace."""
171+
"""Inject job metadata into the MATLAB workspace.
172+
173+
Sets ``__mcp_job_id__`` and optionally ``__mcp_temp_dir__`` as
174+
workspace variables so that MATLAB scripts can reference them.
175+
Failures are silently logged at DEBUG level.
176+
"""
172177
try:
173178
engine._engine.workspace["__mcp_job_id__"] = job.job_id
174179
except Exception:
@@ -187,7 +192,13 @@ async def _wait_for_completion(
187192
future: Any,
188193
temp_dir: Optional[str],
189194
) -> None:
190-
"""Background task that waits for an async job to complete."""
195+
"""Background task that waits for an async job to complete.
196+
197+
Blocks (in a thread executor) until the MATLAB future resolves
198+
or ``max_execution_time`` is exceeded. On completion the job is
199+
marked completed or failed, and the engine is released back to
200+
the pool regardless of outcome.
201+
"""
191202
max_time = self._config.execution.max_execution_time
192203
loop = asyncio.get_running_loop()
193204
try:
@@ -329,7 +340,11 @@ def _build_result(
329340

330341
@staticmethod
331342
def _safe_serialize(value: Any) -> Any:
332-
"""Convert a MATLAB workspace value to a JSON-serializable Python type."""
343+
"""Convert a MATLAB workspace value to a JSON-serializable Python type.
344+
345+
Handles primitives, nested lists/dicts, numpy arrays/scalars,
346+
and MATLAB array types. Falls back to ``repr()`` for unknown types.
347+
"""
333348
if value is None or isinstance(value, (bool, int, float, str)):
334349
return value
335350
if isinstance(value, (list, tuple)):

src/matlab_mcp/jobs/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313

1414

1515
class JobStatus(Enum):
16+
"""Lifecycle status of a MATLAB execution job.
17+
18+
Terminal statuses: COMPLETED, FAILED, CANCELLED.
19+
"""
20+
1621
PENDING = "pending"
1722
RUNNING = "running"
1823
COMPLETED = "completed"
@@ -30,6 +35,27 @@ class Job:
3035
ID of the session that owns this job.
3136
code:
3237
MATLAB code to execute.
38+
39+
Attributes
40+
----------
41+
job_id:
42+
Auto-generated unique identifier (``j-<uuid>``).
43+
status:
44+
Current lifecycle status; starts as PENDING.
45+
engine_id:
46+
ID of the engine executing this job (set when RUNNING).
47+
result:
48+
Structured result dict populated on completion.
49+
error:
50+
Error details dict populated on failure.
51+
created_at:
52+
Epoch timestamp when the job was created.
53+
started_at:
54+
Epoch timestamp when execution began.
55+
completed_at:
56+
Epoch timestamp when the job reached a terminal state.
57+
future:
58+
Handle to the background MATLAB future, if applicable.
3359
"""
3460

3561
session_id: str

src/matlab_mcp/jobs/tracker.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515

1616

1717
class JobTracker:
18-
"""Tracks all jobs (active and historical) keyed by job_id.
18+
"""Thread-safe registry that tracks all jobs (active and historical) by job_id.
19+
20+
Jobs remain in the tracker after reaching a terminal state so that
21+
clients can poll for results. Call :meth:`prune` periodically to
22+
evict stale terminal jobs.
1923
2024
Parameters
2125
----------

src/matlab_mcp/monitoring/collector.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ class MetricsCollector:
4747
"""Collects, aggregates, and persists metrics for the MATLAB MCP Server."""
4848

4949
def __init__(self, config: Any) -> None:
50+
"""Initialize the metrics collector.
51+
52+
Args:
53+
config: Application configuration; uses ``config.monitoring``
54+
for sample interval and retention settings.
55+
"""
5056
self._config = config
5157
self.start_time: float = time.time()
5258

src/matlab_mcp/monitoring/dashboard.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,52 @@
2020

2121

2222
def create_monitoring_app(state: Any) -> Starlette:
23-
"""Create a Starlette app for monitoring endpoints + dashboard."""
23+
"""Create a Starlette sub-application for monitoring endpoints and the dashboard.
24+
25+
Registers the following routes:
26+
27+
* ``/health`` -- JSON health status (200 or 503).
28+
* ``/metrics`` -- Live metrics snapshot.
29+
* ``/dashboard`` -- Static HTML dashboard page.
30+
* ``/dashboard/api/current`` -- Current metrics JSON for the dashboard.
31+
* ``/dashboard/api/history`` -- Historical time-series data.
32+
* ``/dashboard/api/events`` -- Recent event log.
33+
* ``/dashboard/static/`` -- Static file mount (CSS, JS, images).
34+
35+
Args:
36+
state: A :class:`~matlab_mcp.server.MatlabMCPServer` instance
37+
providing access to the collector and metrics store.
38+
"""
2439

2540
# Cache index.html at startup to avoid blocking the event loop
2641
index_path = STATIC_DIR / "index.html"
2742
_cached_html = index_path.read_text() if index_path.exists() else None
2843

2944
async def health_handler(request: Request) -> JSONResponse:
45+
"""Handle GET /health."""
3046
response = build_health_response(state)
3147
return JSONResponse(response, status_code=get_health_status_code(response))
3248

3349
async def metrics_handler(request: Request) -> JSONResponse:
50+
"""Handle GET /metrics."""
3451
return JSONResponse(build_metrics_response(state))
3552

3653
async def dashboard_handler(request: Request) -> HTMLResponse:
54+
"""Handle GET /dashboard -- serve the cached HTML page."""
3755
if _cached_html is not None:
3856
return HTMLResponse(_cached_html)
3957
return HTMLResponse("<h1>Dashboard not found</h1>", status_code=404)
4058

4159
async def api_current(request: Request) -> JSONResponse:
60+
"""Handle GET /dashboard/api/current -- live metrics snapshot."""
4261
return JSONResponse(build_metrics_response(state))
4362

4463
async def api_history(request: Request) -> JSONResponse:
64+
"""Handle GET /dashboard/api/history -- time-series history.
65+
66+
Query params: ``metric`` (default ``pool.utilization_pct``),
67+
``hours`` (default ``1``).
68+
"""
4569
metric = request.query_params.get("metric", "pool.utilization_pct")
4670
try:
4771
hours = float(request.query_params.get("hours", "1"))
@@ -54,6 +78,11 @@ async def api_history(request: Request) -> JSONResponse:
5478
return JSONResponse({"data": data})
5579

5680
async def api_events(request: Request) -> JSONResponse:
81+
"""Handle GET /dashboard/api/events -- recent event log.
82+
83+
Query params: ``limit`` (default ``100``), ``type`` (optional
84+
event type filter).
85+
"""
5786
try:
5887
limit = int(request.query_params.get("limit", "100"))
5988
except (ValueError, TypeError):

src/matlab_mcp/monitoring/health.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44
from typing import Any
55

66
def evaluate_health(collector: Any) -> dict[str, Any]:
7+
"""Evaluate the overall health of the MATLAB MCP Server.
8+
9+
Inspects engine pool utilization, error rates, and health-check
10+
counters to classify the server as ``"healthy"``, ``"degraded"``,
11+
or ``"unhealthy"``.
12+
13+
Args:
14+
collector: A :class:`~matlab_mcp.monitoring.collector.MetricsCollector`
15+
instance with references to the engine pool, job tracker,
16+
and session manager.
17+
18+
Returns:
19+
A dict with keys ``status``, ``uptime_seconds``, ``issues``,
20+
``engines``, ``active_jobs``, and ``active_sessions``.
21+
"""
722
issues: list[str] = []
823
pool_status = collector.pool.get_status() if collector.pool else {}
924
total = pool_status.get("total", 0)

0 commit comments

Comments
 (0)