-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
graph TB
Agent["AI Agent<br/>(Claude, Cursor, etc.)"]
Agent -->|MCP Protocol<br/>stdio or SSE| Server["MCP Server<br/>(FastMCP)"]
Server -->|Tool calls| ToolImpl["Tool Implementations<br/>Core • Discovery • Files<br/>Jobs • Custom • Admin"]
ToolImpl --> SecVal["Security Validator<br/>Function blocklist<br/>Filename sanitization"]
SecVal --> Executor["Job Executor<br/>Sync/async hybrid<br/>Timeout promotion<br/>Progress injection"]
Executor --> PoolMgr["Engine Pool Manager<br/>Elastic scaling<br/>Health checks<br/>Proactive warmup"]
PoolMgr --> Engines["MATLAB Engines<br/>Engine 1 • Engine 2 • ... • N"]
Executor --> Formatter["Result Formatter<br/>Text • Variables • Figures"]
Formatter --> PlotConv["Plotly Converter<br/>Figure extraction<br/>Style mapping<br/>WebGL optimization"]
Formatter --> Output["Output<br/>Text • JSON • Images<br/>Thumbnails"]
Output -->|MCP Response| Agent
style Agent fill:#e1f5ff
style Server fill:#fff3e0
style ToolImpl fill:#f3e5f5
style SecVal fill:#ffebee
style Executor fill:#e8f5e9
style PoolMgr fill:#fce4ec
style Engines fill:#e0f2f1
style Formatter fill:#f1f8e9
style PlotConv fill:#ede7f6
style Output fill:#fff9c4
The central orchestrator built on FastMCP. Responsibilities:
- Register all 20 built-in tools and dynamically load custom tools from YAML
- Initialize and wire together all subcomponents (pool, executor, session manager, security validator)
- Handle server lifecycle events (startup, shutdown, graceful drain)
- Manage authentication/session context based on transport mode (stdio vs SSE)
- Optional monitoring dashboard integration
Manages a pool of MATLAB engine instances with elastic scaling:
- Min/max engines: Configurable range (e.g., 2–10 engines)
- Proactive warmup: When utilization exceeds threshold (default 80%), start a new engine before demand peaks
-
Scale-down: Engines idle longer than timeout (default 15 min) are terminated, never below
min_engines -
Health checks: Periodic
1+1evaluations verify responsiveness; unhealthy engines are automatically replaced - Acquisition queue: Requests wait in an async FIFO queue when all engines are busy; configurable max queue size
Encapsulates a single matlab.engine instance with lifecycle and state management:
- State machine: STOPPED → STARTING → IDLE ↔ BUSY
- Lazy initialization: Engine module only imported on first use (aids testability)
- Workspace reset: Clears variables/functions/paths between sessions for isolation
- Background execution: Supports async code evaluation via thread pool
- Health checks: Self-diagnostic ping; propagates errors to pool for replacement
Orchestrates the complete execution lifecycle with hybrid sync/async behavior:
- Job creation: Registers job in tracker with unique ID
- Engine acquisition: Waits for available engine from pool
-
Context injection: Injects
__mcp_job_id__,__mcp_session_id__,__mcp_temp_dir__into workspace - Execution: Starts background MATLAB evaluation
-
Timeout decision:
-
Sync path: Completes within
sync_timeout(default 30s) → return result inline - Async path: Exceeds timeout → return pending status, background task monitors completion
-
Sync path: Completes within
-
Result formatting: Calls
ResultFormatterto structure output - Cleanup: Resets engine workspace, returns to pool
Thread-safe in-memory registry for job lifecycle:
- CRUD operations: Create, retrieve, list, cancel jobs by ID
- Filtering: Find jobs by session ID for isolated retrieval
- Pruning: Automatic cleanup of stale terminal jobs after configurable retention period (default 24 hours)
- State transitions: PENDING → RUNNING → COMPLETED/FAILED/CANCELLED
Manages per-user session isolation:
- Unique sessions: Each session assigned unique ID and isolated temp directory
- Activity tracking: Monitors idle time; sessions timeout after inactivity (default 1 hour)
- Capacity limits: Enforces max concurrent sessions (default 50); evicts least-recently-used on overflow
- Cleanup: Optionally deletes session temp files on expiration
- Default session: stdio transport uses a single synthetic session
Pre-execution code inspection and request sanitization:
- Function blocklist: Detects dangerous MATLAB functions (system, eval, shell escape, etc.) with smart scanning that ignores string literals and comments
-
Filename sanitization: Rejects path-traversal attempts (
../../etc/passwd) and non-alphanumeric characters - Upload limits: Enforces maximum file size (default 100MB)
- Violation logging: Records attempts via optional metrics collector
Structures raw MATLAB execution results into MCP-compliant response dicts:
- Text output: Truncates long output (default 50KB inline); saves overflow to files
- Variable summary: Extracts type, size, and value preview from workspace query
- Figures: Passes to Plotly converter for interactive JSON generation
- Files: Lists generated files with paths
- Errors: Wraps exception details with helpful formatting
Converts MATLAB figures to interactive Plotly JSON via a two-stage pipeline:
MATLAB-side (mcp_extract_props.m):
- Extracts raw figure properties (axes, line/scatter/bar/surface/heatmap/histogram traces, grid, legend, colormap)
- Handles multiple layout types: single axes, grid subplots, tiled layouts
- Detects FastPlot objects for high-resolution streaming data
Python-side (plotly_style_mapper.py):
- Maps MATLAB line styles, markers, and colormaps to Plotly equivalents
- Converts RGB arrays to CSS color strings
- Computes subplot domains (position fractions)
- WebGL optimization: Uses WebGL for traces with >10,000 points to prevent browser lag
Output:
- Plotly JSON dict (embeddable in MCP response)
- Static PNG thumbnail (optional, configurable DPI)
- Base64 preview for agent display
sequenceDiagram
Agent->>+Server: execute_code("x = magic(3)")
Server->>+SecVal: check_blocked_functions()
SecVal-->>-Server: OK
Server->>+PoolMgr: acquire()
PoolMgr-->>-Server: engine_1
Server->>+Executor: execute(code, engine_1, session)
Executor->>Executor: _inject_job_context()
Executor->>+Engine: eval(code, background=True)
Engine->>-Executor: Future
Executor->>Executor: wait(Future, timeout=30s)
Executor->>Executor: _build_result()
Executor-->>-Server: {status: "completed", output: "ans = [...]"}
Server->>+PoolMgr: release(engine_1)
PoolMgr-->>-Server: OK
Server-->>-Agent: result
sequenceDiagram
Agent->>+Server: execute_code("long_sim(1000000)")
Server->>SecVal: check_blocked_functions()
SecVal-->>Server: OK
Server->>+PoolMgr: acquire()
PoolMgr-->>-Server: engine_2
Server->>+Executor: execute(code, engine_2, session)
Executor->>Executor: _inject_job_context()
Executor->>Engine: eval(code, background=True)
Engine-->>Executor: Future
Executor->>Executor: wait(Future, timeout=30s)
Note over Executor: Timeout exceeded!
Executor->>Executor: Create background task
Executor-->>-Server: {status: "pending", job_id: "j123"}
Server-->>-Agent: {status: "pending", job_id: "j123"}
par Background Monitoring
Executor->>Engine: future.result()
Note over Executor: Still running...
Engine->>Engine: Compute progress
Engine->>Engine: mcp_progress()
Engine-->>Executor: result
Executor->>Tracker: mark_completed()
Executor->>PoolMgr: release(engine_2)
and Agent Poll Loop
Agent->>Server: get_job_status("j123")
Server-->>Agent: {status: "running", progress: 45%}
Note over Agent: Wait 5 seconds
Agent->>Server: get_job_status("j123")
Server-->>Agent: {status: "running", progress: 90%}
Agent->>Server: get_job_result("j123")
Server-->>Agent: {status: "completed", output: "..."}
end
sequenceDiagram
Agent->>+Server: execute_code("plot(x,y); ...")
Server->>Executor: execute()
Executor->>+Engine: eval(code, background=True)
Engine->>Engine: Create figure
Engine-->>-Executor: result
Executor->>Executor: mcp_extract_props(fig)
Engine->>Engine: JSON file to temp_dir
Executor->>+Formatter: _build_result()
Formatter->>+PlotlyConv: load_plotly_json()
PlotlyConv->>PlotlyConv: Parse figure_props.json
PlotlyConv-->>-Formatter: {data: [...], layout: {...}}
Formatter->>+PlotlyMapper: convert_traces()
PlotlyMapper->>PlotlyMapper: Map styles, colormaps
PlotlyMapper-->>-Formatter: Plotly-formatted traces
Formatter->>Formatter: Generate thumbnail
Formatter-->>-Executor: {figures: [{plotly_json, png_thumbnail}]}
Executor-->>Server: Complete result
Server-->>Agent: MCP response with figure
Decision: Code completes synchronously if done within sync_timeout; otherwise auto-promotes to async background job.
Rationale:
- Speed: Most queries (simple calculations, data reads) complete quickly; agents get instant feedback
- Long jobs: Simulation, optimization, training doesn't block the pool or agent
- Simplicity: No need for agents to pre-declare async; the system decides transparently
Trade-off:
- Requires careful tuning of
sync_timeoutper deployment - Agents must poll for async results (polling loop overhead)
- Early timeout assumption (agent might be happy to wait 60s, but we default to 30s)
Decision: Start with min_engines, scale up to max_engines under load, warm up proactively when utilization exceeds threshold.
Rationale:
- Resource efficiency: Don't pay for 10 engines if you only use 2 most of the time
- Responsiveness: Proactive warmup (at 80% utilization) avoids queue wait for the next request
- Headroom: Max engines prevents runaway resource consumption
Trade-off:
- Adds complexity (state machine, health checks, warmup timing)
- Potential false warmups on traffic spikes (wasted engine startup cost)
- Scale-down delay (15 min idle timeout) can leave unused engines running briefly
Decision: Run clear all; fclose all; restoredefaultpath between sessions to isolate user state.
Rationale:
- Security: User B can't access User A's variables or files
- Correctness: Each session starts with a clean slate
Trade-off:
- Loss of workspace context (can't reuse computed values across sessions)
- Startup delay (restore default path, re-run startup scripts)
- Requires session-based model (not a shared global workspace)
Decision: Maintain a blocklist of dangerous functions; strip string literals and comments before scanning.
Rationale:
- Simple allowlist alternative: Blocklist is easier to maintain; users can whitelist MATLAB's vast function library
- False-positive avoidance: Comments and strings frequently mention "system", "eval", etc.; smart stripping prevents spurious blocks
Trade-off:
- Incomplete scanning (e.g., dynamically constructed function names via string concatenation bypass the check)
- Not cryptographically secure (determined attacker can construct obfuscated payloads)
- Regular maintenance of blocklist as MATLAB evolves
Decision: Store job metadata in memory (Python dict); periodically prune stale terminal jobs.
Rationale:
- Speed: O(1) job lookup; no database latency for active jobs
- Simplicity: No database dependency for small deployments
- History: Keep completed jobs for agent query (default 24 hours)
Trade-off:
- Memory growth: Long-running servers with high job volume may accumulate memory
- No persistence: Jobs lost on server restart
- Scaling limit: Single-process limitation (can't shard across multiple server instances without shared store)
Mitigation: Configurable job_retention_seconds; alternative: swap for SQLite-backed store for production.
Decision: MATLAB-side extraction of figure properties → Python-side Plotly conversion (two-stage pipeline).
Rationale:
- No screenshot: Avoids rendering PNG and extracting features; pure data conversion
- Interactive: Plotly JSON enables zoom, pan, legend toggle in agent UI
-
Extensible: Easy to add new plot types by extending
plotly_style_mapper.py
Trade-off:
- Incomplete coverage: Only covers main plot types (line, scatter, bar, surface, heatmap, histogram); custom plot types may not convert
- Feature loss: Advanced MATLAB figure features (custom callbacks, annotations) not represented in Plotly
- Complexity: Two-stage pipeline requires coordination between MATLAB and Python
Decision: Multi-user deployments use SSE (HTTP) behind a reverse proxy; stdio for single-agent/local.
Rationale:
- Network: SSE traverses firewalls, NAT, proxies more easily than stdio
- Auth: Reverse proxy handles authentication once; server stays simple
- Scalability: Multiple concurrent agents (each with own session)
Trade-off:
- Setup complexity: Requires reverse proxy (nginx, Caddy, Traefik)
- Latency: HTTP overhead vs. stdio's direct pipe
-
Security responsibility: Moves authentication burden to operator (server logs warnings if
require_proxy_authnot set)
Each tool is implemented as a standalone module under src/matlab_mcp/tools/:
-
core.py:execute_code,check_code,get_workspace -
jobs.py:get_job_status,get_job_result,cancel_job,list_jobs -
discovery.py:list_toolboxes,list_functions,get_help -
files.py:upload_data,delete_file,list_files,read_script,read_data,read_image -
admin.py:get_pool_status -
custom.py: Loader and handler factory for YAML-defined custom tools -
monitoring.py:get_server_metrics,get_server_health,get_error_log(if enabled)
Each tool implementation:
- Validates inputs (sanitize filenames, check injection patterns)
- Delegates to executor/pool/tracker as needed
- Calls formatter to structure output
- Returns MCP-compliant response dict
If monitoring.enabled: true:
-
MetricsCollector: Accumulates counters (jobs/sessions/errors) and maintains ring buffer of execution times -
MetricsStore(SQLite): Persists metrics time-series and events for historical queries -
evaluate_health(): Assesses server status (healthy/degraded/unhealthy) based on pool utilization, error rates, uptime -
create_monitoring_app(): Starlette sub-application exposing/health,/metrics,/dashboardendpoints - Dashboard UI (HTML/CSS/JS): Real-time charts (Plotly), gauges, event log, time-range selector
The MATLAB MCP Server is a multi-layered system designed for elastic resource management, transparent async promotion, tight security, and extensibility via custom tools. The engine pool handles all complexity; tools focus on MCP protocol details. Monitoring is optional but recommended for production deployments.