-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
The MATLAB MCP Server acts as a bridge between AI agents (Claude, Cursor, Copilot) and MATLAB, providing controlled, asynchronous access to MATLAB computation with interactive plotting and custom tool integration.
graph TB
Agent["AI Agent<br/>(Claude, Cursor, etc.)"]
Agent -->|MCP Protocol<br/>stdio or SSE| Server["FastMCP Server<br/>(server.py)"]
Server -->|Route calls| Tools["20 Built-in Tools<br/>+ Custom Tools"]
Server -->|Manage sessions| SessionMgr["Session Manager<br/>(isolation, temp dirs)"]
Server -->|Security check| Security["Security Validator<br/>(blocklist, sanitize)"]
Server -->|Track jobs| JobTracker["Job Tracker<br/>(in-memory store)"]
Server -->|Execute code| JobExecutor["Job Executor<br/>(sync/async hybrid)"]
JobExecutor -->|Acquire/release| PoolMgr["Engine Pool Manager<br/>(elastic scaling)"]
PoolMgr -->|Manage| Engines["MATLAB Engines<br/>(2020b+)"]
JobExecutor -->|Format output| Formatter["Result Formatter<br/>(text, vars, figures)"]
Formatter -->|Convert plots| PlotlyConv["Plotly Converter<br/>(MATLAB→interactive)"]
Server -->|Health/metrics| Monitor["Monitoring<br/>(collector, dashboard)"]
style Server fill:#4a90e2
style Tools fill:#7ed321
style SessionMgr fill:#f5a623
style Security fill:#d0021b
style JobExecutor fill:#50e3c2
style PoolMgr fill:#b8e986
style Engines fill:#9013fe
style Monitor fill:#417505
Entry point and orchestrator. Built on FastMCP, which handles the MCP protocol details (stdio, SSE, JSON-RPC).
Responsibilities:
- Load and validate configuration (config.yaml + environment overrides)
- Initialize all sub-components: pool, tracker, executor, sessions, security, monitoring
- Register all 20 built-in tools (code execution, discovery, job management, file I/O, admin, monitoring)
- Register custom tools from
custom_tools.yaml - Manage server lifecycle: startup initialization, background health checks, graceful shutdown with job draining
- Route incoming tool calls to implementation modules
Lifespan Management:
- Startup: Initialize pool, store, background tasks
- Shutdown: Drain active jobs (up to
drain_timeout_seconds), cleanup resources
Manages a dynamic pool of MATLAB engine instances with elastic scaling.
Scaling Strategy:
- Starts with
min_enginesengines (default 2) - Scales up to
max_engines(default 10) when demand is high -
Proactive warmup: When pool utilization exceeds
proactive_warmup_threshold(default 80%), a new engine is started before it's actually needed -
Scale-down: Engines idle longer than
scale_down_idle_timeout(default 15 min) are stopped, but never belowmin_engines
Health Management:
- Periodic health checks (every
health_check_intervalseconds): sends a trivial1+1eval to each engine - Dead engines are immediately replaced
- Health check failures logged for diagnostics
Request Queuing:
- When all engines are busy, incoming execution requests wait in an async queue (max
queue_max_size) - Queue is FIFO; oldest requests acquire engines first
Wraps a single matlab.engine.MatlabEngine instance with state tracking and lifecycle management.
State Machine:
-
STOPPED→STARTING→IDLE→BUSY→IDLE→STOPPED
Capabilities:
- Start/stop lifecycle with timeouts
- Execute code (synchronous or background)
- Workspace reset between sessions
- Health check pings
- Idle duration tracking for scale-down decisions
- Default MATLAB path setup and startup commands (e.g.,
format long)
Integration:
- Lazy imports
matlab.engineto enable mock-based testing - Catches
MatlabExecutionErrorfor proper error handling
Orchestrates the full MATLAB code execution lifecycle with hybrid sync/async behavior.
Execution Pipeline:
-
Security validation:
SecurityValidator.check_code()scans for blocked functions - Job creation: Create a Job in the tracker (initial status: PENDING)
- Engine acquisition: Wait for an available engine from the pool
-
Job context injection: Set
__mcp_job_id__and__mcp_temp_dir__in the MATLAB workspace -
Code execution: Start code execution via
engine.eval() -
Timeout decision:
- If execution completes within
sync_timeout(default 30s) → return result immediately - If timeout exceeded → promote to async, return
job_id, spawn background monitor task
- If execution completes within
- Result formatting: Build success/error response with output, variables, figures, files, warnings
- Engine release: Return engine to pool
Sync vs. Async:
- Sync: For quick operations (< 30s), agent gets result immediately
-
Async: For long operations, agent gets
job_idand polls viaget_job_status()/get_job_result()
Error Handling:
- Execution exceptions → job marked FAILED with error dict
- Timeout or cancellation → job marked CANCELLED
- Output/result serialization failures → logged but don't crash the server
In-memory CRUD store for job metadata with lifecycle management.
Operations:
- Create a new job (assigns unique UUID as
job_id) - Retrieve job status by ID or list all jobs for a session
- Mark jobs as RUNNING, COMPLETED, FAILED, or CANCELLED
- Prune completed/terminal jobs older than
job_retention_seconds(default 24 hours)
Thread Safety:
- All operations are synchronized via asyncio locks
- Supports concurrent polling and job creation
Manages per-user execution contexts with workspace isolation.
Session Lifecycle:
- Each session gets a unique temporary directory under
temp_dir(default./temp/) - Session tracks creation and last-activity timestamps
- Expiration: Sessions with no activity for
session_timeoutseconds (default 1 hour) are cleaned up - Optional: Clear MATLAB workspace between sessions to prevent variable leakage
Transport Modes:
- stdio: Single "default" session per server instance
-
SSE: Multiple sessions, one per connected client (identified by
session_idcontext)
Cleanup:
- Expired sessions have their temp directories deleted
- If
temp_cleanup_on_disconnect: true, cleanup happens immediately when session expires - Job metadata retained for
job_retention_secondsbefore being pruned
Pre-execution security controls with intelligent pattern matching.
Function Blocklist (default):
-
system,unix,dos— OS command execution -
!— Shell escape operator -
eval,feval,evalc,evalin,assignin— Dynamic code execution / workspace manipulation -
perl,python— External interpreter execution
Smart Scanning:
- Before pattern matching, the validator:
- Strips all string literals (
'...'and"...") - Strips all comments (
%...to end of line) - Then applies regex patterns with word boundaries to avoid false positives
- Strips all string literals (
- Example:
msg = 'call system to run';is allowed;system('ls');is blocked
File Protection:
- Sanitizes filenames to prevent path traversal: rejects
../, absolute paths, special characters - Enforces upload size limits via
max_upload_size_mb(default 100MB)
Optional Disabling:
- Set
blocked_functions_enabled: falseto allow all code (not recommended for production)
Monitoring:
- Security events (blocked attempts) are logged to the metrics collector
Structures tool responses into MCP-compliant dictionaries.
Formatting:
-
Output text: Truncated to
max_inline_text_length(default 50,000 chars); excess saved to file -
Variables: Extracted via
whoscommand, formatted with type/size/value summaries - Figures: Converted to Plotly JSON or static PNG/JPEG
- Files: Paths to saved files returned for agent download
- Warnings: Execution warnings (e.g., uninitialized variables) captured and included
Response Structure:
{
"status": "completed",
"job_id": "...",
"output": "...",
"variables": {...},
"figures": [...],
"files": [...],
"execution_time": 0.5
}Plotly Converter (src/matlab_mcp/output/plotly_convert.py, plotly_style_mapper.py, matlab_helpers/mcp_extract_props.m)
Converts MATLAB figures to interactive Plotly visualizations.
MATLAB Side (mcp_extract_props.m):
- Iterates over figure axes and child objects (lines, surfaces, images, patches)
- Extracts properties: colors, line styles, markers, fonts, axis limits, titles, labels, legends, grids
- Exports as JSON to a temporary file
Python Side (plotly_style_mapper.py):
- Maps MATLAB styles to Plotly equivalents:
- Line styles:
-→solid,--→dash,:→dot,-.→dashdot - Markers:
o→circle,s→square,^→triangle-up, etc. - Colormaps:
jet,viridis,hot, etc. → Plotly color scales - Fonts: Arial, Courier → web-safe defaults
- Line styles:
-
WebGL optimization: Large datasets (>10,000 points) use
scatterglfor browser performance - Handles multi-axes layouts and subplot positioning
Output:
- Complete Plotly JSON suitable for
Plotly.newPlot()in JavaScript - Static PNG/JPEG copy via
matlab.enginebuilt-in figure export - Optional thumbnail (base64-encoded, resized to fit narrow displays)
- Tracks counters: jobs completed/failed, sessions created, blocked attempts
- Records execution time statistics (ring buffer: min/avg/max/p95 latencies)
- Samples system metrics (CPU %, memory MB via
psutil) - Fire-and-forget async writes to the metrics store
- SQLite3 backing with WAL mode for concurrent access
- Two tables:
metrics(time-series snapshots) andevents(discrete events) - Supports historical queries:
get_latest(),get_history(since_time), filtered event retrieval - Auto-pruning of old data based on
retention_days
- Starlette sub-application exposing HTTP routes:
-
/health— Server health status (healthy/degraded/unhealthy) -
/metrics— Live metrics snapshot (pool, jobs, sessions, system) -
/dashboard— Web UI with Plotly charts -
/api/current,/api/history,/api/events— JSON data endpoints
-
- Static assets: HTML, CSS, JavaScript for interactive charts
- Checks:
- Pool utilization (if >90% → degraded)
- Max capacity reached (if all engines busy → unhealthy)
- Error rate (if >10% of jobs failed in last 10 min → degraded)
- Health check failures (if 2+ consecutive failures → unhealthy)
- Returns status + list of detected issues for display
sequenceDiagram
participant A as Agent
participant S as MCP Server
participant J as Job Executor
participant P as Engine Pool
participant E as MATLAB Engine
A->>S: execute_code("x = magic(3)")
S->>J: execute(code, session_id)
J->>J: Security check (OK)
J->>P: acquire()
P->>E: [waiting for engine]
P-->>J: engine
J->>E: eval("x = magic(3)")
E-->>J: result: {x: [[8,1,6],[3,5,7],[4,9,2]]}
J->>J: Format result
J->>P: release(engine)
J-->>S: {status: completed, output: "...", variables: {...}}
S-->>A: result
sequenceDiagram
participant A as Agent
participant S as MCP Server
participant J as Job Executor
participant P as Engine Pool
participant E as MATLAB Engine
participant T as Job Tracker
A->>S: execute_code("simulation(n=1000000)")
S->>J: execute(code, session_id)
J->>J: Security check (OK)
J->>T: create_job()
T-->>J: job_id = "abc123"
J->>P: acquire()
P-->>J: engine
J->>E: eval(code, background=True)
E-->>J: future
J->>J: Monitor timeout (30s exceeded)
J->>T: mark_running("abc123")
J-->>S: {status: running, job_id: "abc123"}
S-->>A: job_id
Note over J,E: Background execution continues
A->>S: get_job_status("abc123")
S->>T: get_job("abc123")
T-->>S: {status: running, progress: 45%}
S-->>A: progress
Note over J,E: Execution completes
J->>T: mark_completed("abc123", result)
J->>P: release(engine)
A->>S: get_job_result("abc123")
S->>T: get_job("abc123")
T-->>S: {status: completed, result: {...}}
S-->>A: result
graph LR
Config["config.yaml"]
CustomTools["custom_tools.yaml"]
Config -->|Load| Server["MCP Server"]
CustomTools -->|Load custom_tools.py| Server
Server -->|Register 20 built-in| Tools["Tool Registry"]
Tools -->|execute_code| Exec["exec_code_impl()"]
Tools -->|check_code| Check["check_code_impl()"]
Tools -->|get_workspace| GetWS["get_workspace_impl()"]
Tools -->|[4 job tools]| Jobs["job tools"]
Tools -->|[3 discovery]| Discovery["discovery tools"]
Tools -->|[5 file I/O]| Files["file tools"]
Tools -->|[3 monitoring]| Monitor["monitor tools"]
Server -->|Dynamic registration| CustomTools2["Custom tools"]
CustomTools2 -->|From YAML| Handlers["make_custom_tool_handler()"]
Agent["Agent"]
Agent -->|List tools| Server
Server -->>|Tool list| Agent
Agent -->|Call tool| Tools
Tools -->|Execute| Impl["Implementation"]
Decision: Auto-promote to async if code exceeds sync_timeout.
Rationale:
- Agents expect quick feedback; synchronous execution keeps the interaction snappy
- Long-running jobs (simulations, data processing) don't block the agent
- No need for agents to pre-declare async intent
Trade-off:
- More complex executor logic
- Agents must understand two response patterns (inline vs.
job_id)
Decision: Jobs stored in RAM with optional SQLite metrics store.
Rationale:
- Fast polling for job status
- No database dependency for core functionality
- Suitable for single-server deployments
Trade-off:
- Job results lost on server restart
- Limited to one server (no horizontal scaling)
- Mitigation: Job completion exported to metrics store for durability if monitoring enabled
Decision: Scale up gradually; pre-warm when threshold is near.
Rationale:
- Reduces cold-start latency for scaling events
- Avoids thundering herd of engine startups
- Engines are expensive (MATLAB license) and consume memory
Trade-off:
- Slightly higher baseline memory usage
- Warmup engines may not get used
Decision: Remove literals before pattern matching.
Rationale:
- Avoids false positives (e.g.,
msg = 'system resources') - Whitelist-by-pattern is simpler than AST parsing
- Covers the common case of benign text containing function names
Trade-off:
- Regex-based (not a full MATLAB parser)
- Edge cases possible (e.g., complex string escapes)
- Mitigation: Default blocklist covers high-risk functions; can be customized per deployment
Decision: Extract figure properties via MATLAB (mcp_extract_props.m), convert in Python.
Rationale:
- MATLAB can reliably extract figure state (no black-box rendering)
- Python conversion logic is easier to test and customize
- Output is standard Plotly JSON (portable, no extra dependencies)
Trade-off:
- Adds MATLAB execution overhead per figure
- Complex style mapping (many MATLAB line styles, markers, colormaps)
- Mitigation: Cached mapping tables; optional WebGL for large plots
Decision: Each session gets an isolated temp directory.
Rationale:
- Prevents accidental data leakage between users
- Simplifies file I/O (agents know their sandbox)
- Easy cleanup (delete directory on session end)
Trade-off:
- Slightly higher I/O overhead
- Agents can't easily share files across sessions
- Mitigation: Custom tools can coordinate via MATLAB's persistent variables
Decision: File-based config with MATLAB_MCP_* env var overrides.
Rationale:
- YAML is human-readable and flexible
- Environment overrides suit containerized deployments
- Hierarchical config (nested objects) maps naturally to Pydantic
Trade-off:
- Complex override precedence rules
- Schema validation errors can be hard to debug
- Mitigation: Comprehensive logging and config dump at startup
Decision: Monitoring is opt-in; disabled by default in config.
Rationale:
- No performance overhead for deployments that don't need it
- Dashboard and metrics collection are separate from core execution
- Suitable for both development (no DB) and production (with monitoring)
Trade-off:
- Job history and metrics lost on server restart (if monitoring off)
- Extra setup required for production observability
-
Mitigation:
config.yamldefaults tomonitoring.enabled: true; SQLite is lightweight
MCP Server ↔ Tools: FastMCP context injection + direct function calls
Tools ↔ Job Executor: Delegate actual execution; return structured results
Job Executor ↔ Engine Pool: Acquire/release engines; get status
Job Executor ↔ Job Tracker: Create/update job state; poll for results
Job Executor ↔ Session Manager: Get session temp dir; track session activity
Job Executor ↔ Security Validator: Pre-execute security check
Job Executor ↔ Result Formatter: Structure output for MCP response
Result Formatter ↔ Plotly Converter: Convert MATLAB figures to interactive JSON
All ↔ Monitoring Collector: Log events (fire-and-forget async writes)
Monitoring Collector ↔ Metrics Store: Persist metrics and events (async)
Monitoring Dashboard ↔ Metrics Store: Query history, health, events
| Operation | Latency | Notes |
|---|---|---|
Simple eval (e.g., 1+1) |
10–50 ms | Depends on engine state (busy/idle) |
| Matrix ops (100×100) | 50–200 ms | MATLAB computation time |
| Figure extraction | 100–500 ms | Includes figure iteration and JSON generation |
| Engine startup | 2–5 sec | MATLAB JVM initialization |
| Job polling | <10 ms | In-memory; minimal overhead |
| Plotly conversion | 50–200 ms | Depends on plot complexity |
- Horizontal: Not supported; single-server design (job state in RAM)
-
Vertical: Limited by MATLAB licensing and host resources
- Typical: 4–8 concurrent engines per machine
- macOS: Capped at 4 due to stability issues
- Concurrency: Hundreds of queued requests; actual parallelism limited by engine count