# Architecture ## System Overview The MATLAB MCP Server is a Python-based bridge that connects AI agents (Claude, Cursor, etc.) to MATLAB via the Model Context Protocol. It provides elastic engine pooling, session isolation, security validation, and hybrid sync/async execution. ```mermaid graph TB Agent["AI Agent
(Claude, Cursor, etc.)"] Agent -->|MCP Protocol
stdio or HTTP| Server["MCP Server
(FastMCP 3.2.0)"] Server -->|Tool Calls| Tools["20+ Built-in Tools
+ Custom Tools"] Tools -->|Job Creation| JobExec["Job Executor
(Sync/Async Promotion)"] JobExec -->|Engine Acquire| PoolMgr["Engine Pool Manager
(Elastic Scaling)"] PoolMgr -->|Engine Lifecycle| Engines["MATLAB Engine Pool
(min_engines → max_engines)"] Tools -->|Workspace Queries| EngineAPI["MATLAB Engine API"] Tools -->|Code Security| SecVal["Security Validator
(Blocked Functions,
Path Traversal)"] JobExec -->|Session Mgmt| Sessions["Session Manager
(Per-user Isolation)"] Tools -->|File I/O| FileOps["File Operations
(upload, delete, read)"] FileOps -->|Temp Dir| Sessions Tools -->|Plotting| Converter["Plotly Converter
(MATLAB → Interactive JSON)"] Tools -->|Metrics| Monitor["Monitoring Dashboard
(HTTP UI)"] Monitor -->|Query| Collector["Metrics Collector
(Events, Percentiles)"] Collector -->|Persist| Store["SQLite Store"] style Server fill:#4A90E2 style Tools fill:#7CB342 style JobExec fill:#FB8C00 style PoolMgr fill:#E53935 style Engines fill:#8E24AA ``` ## Core Components ### 1. **MCP Server** (`src/matlab_mcp/server.py`) FastMCP-based server that: - Registers 20 built-in tools + custom MATLAB functions as MCP tools - Manages tool lifespan (startup, shutdown, graceful drain) - Routes incoming requests to tool implementations - Handles session allocation and cleanup - Supports three transports: - **stdio** (single-user, default) - **SSE** (multi-user, deprecated) - **streamable HTTP** (multi-user, recommended) **Key design decision:** Bearer token auth via ASGI middleware (Phase 2) validates all HTTP requests before reaching the MCP layer. ### 2. **Engine Pool Manager** (`src/matlab_mcp/pool/manager.py`) Manages MATLAB engine instances with elastic scaling: ```mermaid graph LR Req["Tool Request"] Req --> Acquire["Acquire Engine
from Pool"] Acquire --> Available{"Engine
Available?"} Available -->|Yes| Run["Execute
Immediately"] Available -->|No| Check{"Count
< max?"} Check -->|Yes| Scale["Scale Up:
Start New Engine"] Check -->|No| Queue["Enqueue Request
Wait for Release"] Scale --> Run Queue --> Run Run --> Release["Release to Pool"] Release --> ScaleDown{"Idle >15min
& Count > min?"} ScaleDown -->|Yes| Stop["Stop Engine"] ScaleDown -->|No| Ready["Return to Available"] style Acquire fill:#FB8C00 style Scale fill:#E53935 style Queue fill:#FFA726 style Stop fill:#C62828 ``` - **Minimum engines:** Always running (warmth for quick response) - **Proactive warmup:** When utilization > 80%, starts next engine before it's needed - **On-demand scaling:** Creates engines up to `max_engines` when all are busy - **Scale-down:** Stops idle engines > 15 minutes, down to minimum - **Health checks:** `1+1` eval every 60 seconds; unhealthy engines are replaced - **Queueing:** When full, requests wait in an async queue ### 3. **Job Executor** (`src/matlab_mcp/jobs/executor.py`) Orchestrates the complete execution lifecycle with hybrid sync/async promotion: ```mermaid sequenceDiagram participant Agent participant Server participant Executor participant Pool participant Engine participant Store Agent->>Server: execute_code("x = magic(3)") Server->>Executor: create_and_run_job() Executor->>Executor: security_check() ✓ Executor->>Pool: acquire_engine() Pool->>Engine: Engine available? Engine-->>Pool: Yes Pool-->>Executor: engine handle Executor->>Engine: inject_job_context(__mcp_job_id__) Executor->>Engine: eval(code, sync=True, timeout=30s) Engine->>Engine: code executes alt Completes < 30s (Sync Path) Engine-->>Executor: result, output, vars Executor->>Store: mark_completed(job_id) Executor->>Pool: release_engine() Executor-->>Server: return result immediately Server-->>Agent: {output, variables, figures} else Exceeds 30s (Async Path) Engine->>Engine: (background execution) Executor-->>Executor: mark_running() Executor-->>Server: return {job_id, status: "running"} Server-->>Agent: {job_id: "abc123"} Agent->>Server: get_job_status("abc123") Server-->>Agent: {status: "running", progress: 45%} Engine->>Engine: (completes in background) Executor->>Store: mark_completed(job_id) Executor->>Pool: release_engine() Agent->>Server: get_job_result("abc123") Server-->>Agent: {output, variables, figures} end ``` **Key design decisions:** - **Sync timeout:** 30 seconds for user-facing code (configurable) - **Promotion trigger:** If execution exceeds timeout, move to background job - **Context injection:** Every job gets unique ID and temp directory in MATLAB workspace - **Progress reporting:** Jobs can call `mcp_progress()` MATLAB helper to report percentage/message ### 4. **Session Manager** (`src/matlab_mcp/session/manager.py`) Isolates workspace state between agents: | Aspect | stdio | streamable HTTP | |--------|-------|-----------------| | Sessions | Single "default" | Per-client (via `ctx.session_id`) | | Temp Dir | `/tmp/matlab_mcp/` | `/tmp/matlab_mcp/s_/` | | Workspace | Reused across tools | Cleared between jobs (configurable) | | Duration | Server lifetime | `session_timeout` (15 min idle) | **Cleanup:** Expired sessions auto-deleted; files in temp directories are accessible via `read_script`, `read_data`, `read_image` tools. ### 5. **Security Validator** (`src/matlab_mcp/security/validator.py`) Pre-execution code validation: ```mermaid graph TD Code["User Code"] -->|Input| Strip["Strip Comments
& Strings"] Strip -->|Cleaned| Check["Check for
Blocked Functions"] Check -->|Found| Block["Raise
BlockedFunctionError"] Check -->|Not Found| OK["✓ Allow Execution"] Block --> Agent["Return Error
to Agent"] OK --> Engine["Send to MATLAB"] style Check fill:#E53935 style Block fill:#C62828 style OK fill:#7CB342 ``` **Blocked by default:** `system`, `unix`, `dos`, `!`, `eval`, `feval`, `evalc`, `evalin`, `assignin`, `perl`, `python`, shell escapes **Smart detection:** Strips string literals and comments before scanning to avoid false positives (e.g., `code = "system('ls')"` is safe) **Custom blocklist:** Can be extended via `config.yaml` or disabled entirely (with explicit acknowledgment) ### 6. **Human-in-the-Loop Gates** (`src/matlab_mcp/hitl/gate.py`) Optional approval gates for sensitive operations (Phase 4): ```mermaid graph TD Tool["Tool Call
(execute_code)"] Tool -->|Check Config| GateEnabled{"Gate
Enabled?"} GateEnabled -->|No| Execute["✓ Execute
Immediately"] GateEnabled -->|Yes| Check2{"Protected
Function?"} Check2 -->|No| Execute Check2 -->|Yes| Elicit["Elicit User
Approval via
MCP Protocol"] Elicit -->|Approved| Execute Elicit -->|Declined| Reject["✗ Reject
with Error"] Elicit -->|Cancelled| Reject style GateEnabled fill:#FB8C00 style Check2 fill:#FB8C00 style Execute fill:#7CB342 style Reject fill:#E53935 ``` **Configuration:** - `enabled` (default: false) — Master switch - `protected_functions` — Function names requiring approval - `all_execute` — Require approval for all code, not just protected functions - `protect_file_ops` — Require approval for uploads/deletes ### 7. **Result Formatter** (`src/matlab_mcp/output/formatter.py`) Structures tool responses into MCP format: ```mermaid graph TD Result["MATLAB Result
(output, vars, figures)"] Result -->|Text| TruncText["Truncate to
max_text_length"] Result -->|Variables| FormatVars["Format with
Type & Size Info"] Result -->|Figures| Convert["Convert to
Plotly JSON"] TruncText -->|Long| SaveFile["Save to File
& Return Path"] TruncText -->|Short| Return["Return Inline"] Convert -->|Success| JSON["Plotly JSON
+ PNG + Thumbnail"] Convert -->|Fail| PNG["Static PNG
Only"] SaveFile --> Final["Assemble Response"] Return --> Final JSON --> Final PNG --> Final Final --> Agent["Return to Agent"] style Convert fill:#4A90E2 ``` **Output limits (configurable):** - Text: 10,000 characters (save excess to disk) - Variables: Show type/size, exclude large objects (>10MB) - Figures: Plotly JSON + 400px-wide PNG thumbnail ### 8. **Plotly Figure Conversion** MATLAB figures → interactive Plotly JSON for agent visualization: ```mermaid graph LR MATLAB["MATLAB
figure, plot()
bar(), etc."] MATLAB -->|mcp_extract_props.m| Extract["Extract Properties
(JSON)"] Extract -->|Save to Temp| File["matlab_figure_props.json"] File -->|Load| Load["load_plotly_json()"] Load -->|Parse| Props["Figure Properties
Dict"] Props -->|Convert| Style["plotly_style_mapper.py
(colors, lines,
markers, fonts)"] Style -->|Build Plotly| Plotly["Plotly Figure Dict
{traces, layout}"] Plotly -->|Serialize| JSON["JSON Response
to Agent"] MATLAB -->|saveas(...png)| PNG["Static PNG
(fallback)"] PNG -->|Thumbnail| Thumb["Base64 PNG
400px wide"] JSON -->|Agent UI| Render["Interactive
Plot"] Thumb -->|Agent UI| Display["Embedded
Thumbnail"] style Extract fill:#8E24AA style Style fill:#4A90E2 style Plotly fill:#7CB342 ``` **Supported trace types:** line, scatter, bar, histogram, surface, heatmap, image, patch **Performance optimization:** Uses WebGL rendering for datasets > 10,000 points ### 9. **Monitoring Dashboard** (`src/matlab_mcp/monitoring/`) Real-time HTTP dashboard with metrics collection: ```mermaid graph TB Tools["Tool Calls
(code exec, file ops)"] Events["Events
(completed, failed,
sessions, scaling)"] Tools -->|Fire & Forget| Collector["MetricsCollector
(in-memory)"] Events -->|Fire & Forget| Collector Collector -->|Sample (every 10s)| Store["MetricsStore
(SQLite)"] Collector -->|Query Current| Current["/metrics
endpoint"] Store -->|Query History| History["/dashboard/api/history
endpoint"] Store -->|Query Events| EventLog["/dashboard/api/events
endpoint"] Current -->|HTTP JSON| UI["Dashboard UI
(Plotly charts)"] History -->|HTTP JSON| UI EventLog -->|HTTP JSON| UI UI -->|1-second refresh| Display["Live Metrics
(pool, jobs, errors)"] style Collector fill:#FB8C00 style Store fill:#E53935 style UI fill:#4A90E2 ``` **Available metrics:** - Pool utilization (busy/total engines) - Job throughput (completed, failed, cancelled per minute) - Execution time (avg, p95, p99) - Active sessions - System memory usage - Error rates by type ## Data Flow: Complete Example ### Scenario: Agent runs signal processing code with async promotion ``` 1. AGENT sends: tool: execute_code code: "x = randn(1, 100000); Y = fft(x); plot(abs(Y))" 2. SERVER receives, dispatches to execute_code_impl(): 3. SECURITY validates code - Scans for blocked functions: ✓ (fft, plot are safe) - Checks for file I/O, system calls: ✓ 4. HITL checks (if enabled): - Is all_execute gate on? No - Code calls protected functions? No - Proceed without approval 5. JOB EXECUTOR: - Creates job in tracker (status: PENDING) - Acquire engine from pool (waits if all busy) - Inject context: __mcp_job_id__ = "job_abc123" - Execute synchronously with 30s timeout 6. ENGINE starts code (background=False initially): - x = randn(1, 100000) [200ms] - Y = fft(x) [5000ms total, exceeds 30s cutoff] - At 30s mark: timeout triggered 7. EXECUTOR auto-promotes to async: - Moves to background (background=True) - Returns immediately with job_id - Future awaited by background task 8. AGENT receives: response: { status: "running", job_id: "job_abc123", message: "Long-running job promoted to async" } 9. AGENT polls get_job_status("job_abc123"): - Job tracker returns: {status: "running", progress: 0} - No progress file yet (code hasn't called mcp_progress()) - Agent waits 2 seconds, polls again 10. ENGINE finishes (5500ms total): - plot(abs(Y)) [figure generated] - Execution complete 11. EXECUTOR: - Extracts figure via mcp_extract_props.m (JSON props) - Captures output and variables - Formats result - Marks job COMPLETED - Releases engine back to pool 12. AGENT calls get_job_result("job_abc123"): response: { status: "completed", output: "Y = [5.2 3.1 2.8 ...]", variables: { x: {type: "double", size: "1x100000"}, Y: {type: "double", size: "1x100000"} }, figures: [{ type: "plotly", data: {traces: [...], layout: {...}}, png_thumbnail: "data:image/png;base64,..." }] } 13. AGENT displays interactive Plotly figure in UI ``` ## Authentication & Transport ### Phase 1-2: FastMCP 3.0 + Bearer Token Auth - **HTTP transports** (SSE, streamable HTTP): `BearerAuthMiddleware` validates `Authorization: Bearer ` header - **Token source:** `MATLAB_MCP_AUTH_TOKEN` environment variable (static, no rotation mid-session) - **Health endpoint:** `/health` bypassed (allows agents to check readiness without token) - **Stdio transport:** No auth (single-user, assumed trusted) ### Phase 3: Streamable HTTP Transport - **Endpoint:** `http://127.0.0.1:8765/mcp` (default) - **Per-session routing:** `ctx.session_id` for multi-agent isolation - **Fallback:** `ctx.client_id` for stateless mode (no persistent workspace) ### Phase 5: Windows 10 No-Admin Support - **Default host:** `127.0.0.1` (loopback, no Firewall UAC prompt) - **Temp dir:** Uses `tempfile.gettempdir()` (platform-aware, not hardcoded `/tmp`) - **Deployment:** Single-machine scenarios avoid admin requirement entirely ## Design Decisions & Trade-offs | Decision | Rationale | Trade-off | |----------|-----------|-----------| | **Elastic pooling** | Handles variable load without pre-allocating expensive engines | Scale-up latency (~5s per engine) on load spikes | | **Sync→async promotion** | Responsive UX for quick queries; async for long jobs | Dual code paths, more testing | | **MATLAB workspace isolation** | Security + correctness (side effects don't cross sessions) | Startup cost per session (~200ms) | | **Bearer tokens (not OAuth)** | Simplicity for CLI agents, no external dependencies | No token rotation, revocation requires restart | | **SQLite metrics store** | Lightweight, no external DB, point-in-time queries | Limited to single-machine deployments | | **Streamable HTTP (not SSE)** | Single transport for all clients, simpler reverse proxy setup | New in FastMCP 3.x, less battle-tested than SSE | | **Plotly figures (not static PNG)** | Interactive visualization in agents (zoom, pan, tooltips) | Larger JSON payloads, WebGL fallback needed for huge datasets | | **Security blocklist** | Pragmatic: block dangerous functions, not all-allow | New functions added to MATLAB can bypass (mitigated by monitoring) | ## Known Issues & Monitoring 1. **ctx.session_id stability under streamable HTTP** (Phase 3 blocker) - Some agents may not provide consistent `ctx.session_id` across requests - Fallback to `ctx.client_id` implemented; may reduce workspace isolation 2. **Memory leak in failed engine startup** (Phase 1 issue) - Engines that crash during `start()` not fully cleaned up - Mitigated by health checks (bad engines replaced within 60s) 3. **Windows 10 CI environment** (Phase 5 blocker) - GitHub Actions Windows runners may not support MATLAB installation - Workaround: `--inspect` mode (mock engines) for CI; live Windows testing deferred to post-v2.0 4. **Large figure Plotly JSON** (Known limitation) - Figures with 100k+ traces can exceed result size limits - Mitigation: PNG fallback; agents should request PNG for large datasets ## Testing Strategy - **Unit tests (732 tests, 185 test classes):** Components tested in isolation with mocks - **Integration tests (CI):** Server starts in `--inspect` mode (mock engines), real MCP client connects via streamable HTTP with bearer auth, tools execute - **Live tests (manual, deferred):** Windows no-admin deployment, multi-agent session isolation, agent UI rendering of Plotly figures