-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
github-actions[bot] edited this page Mar 22, 2026
·
20 revisions
AI Agent (Claude, Cursor, Copilot, etc.)
│
│ MCP Protocol (stdio or SSE)
▼
┌─────────────────────────────────┐
│ MCP Server (FastMCP) │
│ ├─ Server Entry Point │
│ ├─ Tool Router │
│ ├─ Session Manager │
│ ├─ Security Validator │
│ ├─ Output Formatter │
│ ├─ Job Tracker │
│ └─ Monitoring Subsystem │
└──────────┬──────────────────────┘
│
┌──────────▼──────────────────────┐
│ Job Executor │
│ ├─ Hybrid sync/async execution │
│ ├─ Timeout-based promotion │
│ ├─ Job context injection │
│ └─ Progress tracking │
└──────────┬──────────────────────┘
│
┌──────────▼──────────────────────┐
│ Engine Pool Manager │
│ ├─ Elastic scaling (min→max) │
│ ├─ Health checks │
│ ├─ Proactive warmup │
│ └─ Idle scale-down │
└──────────┬──────────────────────┘
│
┌──────────▼──────────────────────┐
│ MATLAB Engines (2020b+) │
│ Engine 1 │ Engine 2 │ ... │ N │
└───────────────────────────────────┘
The main MCP server orchestrator. Uses FastMCP to handle MCP protocol details. Responsibilities:
- Component initialization: Creates and wires together the engine pool, job tracker, executor, session manager, security validator, and (optionally) monitoring subsystem
- Lifespan management: Handles server startup (engine pool initialization, background task launch), shutdown (graceful drain, cleanup), and security warnings
- Tool registration: Registers all built-in tools plus custom tools loaded from YAML
- Session routing: Routes requests to the appropriate session (via context) and injects session ID / temp directory
- Background tasks: Runs periodic health checks, session cleanup, and job expiration in the background
Manages a pool of MATLAB engine instances with dynamic scaling:
-
Elastic scaling: Starts with
min_engines, scales up tomax_enginesunder load - Proactive warmup: When utilization exceeds threshold (80% by default), starts a new engine before it's needed
-
Scale-down: Engines idle longer than
scale_down_idle_timeout(15 min by default) are stopped, down tomin_engines -
Health checks: Periodic
1+1eval to verify engines are responsive. Unhealthy engines are replaced - Queue management: Requests wait in an async queue when all engines are busy
- Metrics collection: Records scale-up/down events and engine state changes to the monitoring subsystem (if enabled)
Wraps a single matlab.engine instance with full lifecycle management:
-
Lifecycle states:
STOPPED,STARTING,IDLE,BUSY(tracked viaEngineStateenum) - Start/stop: Initializes MATLAB engine, applies default paths and startup commands; cleanly quits on stop
- Execution: Executes code synchronously or asynchronously (background=True) with stdout/stderr capture
- Workspace reset: Clears all variables, paths, and functions; restores defaults and re-applies startup commands
-
Health check: Trivial
1+1eval to confirm engine responsiveness -
Idle tracking: Records time of last transition to
IDLEstate for scale-down decisions
Hybrid sync/async execution orchestrator:
- Creates a job in the tracker with a unique ID
- Acquires an engine from the pool
- Injects job context into MATLAB workspace (
__mcp_job_id__,__mcp_temp_dir__, etc.) - Starts code execution asynchronously via
engine.execute(..., background=True) -
Sync path: Waits up to
sync_timeoutseconds- If completes: Returns result inline with status
"completed" - If times out: Promotes to async background task, returns status
"pending"withjob_id
- If completes: Returns result inline with status
- Async path: Spawns a background coroutine to monitor completion, capture output, build final result
- Engine release: Returns engine to pool once execution (sync or async) completes
- Metrics: Records job creation, completion, and failures to the monitoring subsystem
In-memory store for job metadata and state:
- Job lifecycle: Create, get, list, mark_running, mark_completed, mark_failed
-
Job storage: Maps job IDs to
Jobobjects containing code, status, results, stdout/stderr, timing -
Automatic pruning: Periodically removes completed jobs older than
job_retention_seconds(default 3600) - Thread-safe: Uses asyncio locks to protect concurrent access
- Query: List all jobs or jobs for a specific session
Per-user session isolation with per-session temporary directories:
- Session creation: Each session gets a unique temp directory (UUID-based or explicit ID)
-
Max sessions: Enforces
max_sessionslimit (default 50) -
Idle cleanup: Removes sessions that have been idle longer than
session_timeout(default 3600s) - Session tracking: Records creation time and last-active time for timeout calculation
- Workspace isolation: Per-session temp directories prevent cross-user interference
-
Single-user stdio mode: Uses a fixed
"default"session when stdio transport is active - Multi-user SSE mode: Creates/retrieves sessions by session ID from the request context
Pre-execution security checks preventing dangerous operations:
-
Function blocklist: Scans code for blocked functions (
system,unix,dos,!,eval,feval,evalc,evalin,assignin,perl,python). Smart parsing strips string literals and comments first to avoid false positives - Filename sanitization: Prevents path traversal in upload filenames via safe name generation
-
Upload size limits: Enforces
max_upload_size_mb(default 100) - Metrics: Records security events (check requests, violations) to the monitoring subsystem
Structures tool responses with consistent formatting:
-
Text output: Formats captured stdout with length limits; saves large output to file if
save_dirprovided - Variable formatting: Summarizes workspace variables with type, size, and (for small values) the actual value
- Success/error responses: Builds structured dicts with status, job_id, output, variables, figures, files, warnings, execution_time
- Figure handling: Delegates to Plotly converter for interactive plots and thumbnail generation
- JSON serialization: Ensures all response values are JSON-serializable
Plotly Converter (output/plotly_convert.py, output/plotly_style_mapper.py + matlab_helpers/mcp_extract_props.m)
Converts MATLAB figures to interactive Plotly JSON:
-
MATLAB-side:
mcp_extract_props.mextracts raw figure properties (line, scatter, bar, histogram, surface, image data and styling) -
Python-side:
plotly_style_mapper.pyconverts MATLAB styles (line styles, markers, colormaps, fonts, colors) to Plotly equivalents with WebGL support for large datasets (10,000+ points) -
Python-side:
plotly_convert.py/load_plotly_json()reads the saved JSON file - Result: Plotly JSON (interactive), static PNG (fallback), optional thumbnail
When monitoring.enabled = true:
- Metrics Collector: Records events (job creation/completion/failure, session creation, engine scale-up/down, security checks)
- Metrics Store: Aggregates and stores metrics (if a backing store is configured)
- Per-component integration: Pool, executor, session manager, and security validator all report events to the collector
-
Graceful fallback: If monitoring is disabled, collectors are
Noneand no metrics are recorded
Agent → execute_code("x = magic(3)")
→ Security validator (OK)
→ Create job in tracker
→ Acquire engine from pool
→ Inject job context (__mcp_job_id__, __mcp_temp_dir__)
→ Engine.execute("x = magic(3)", background=True)
→ Wait for future (default: 30s sync_timeout)
→ Future completes in <30s
→ Collect stdout/stderr, query workspace variables, convert figures
→ Format result dict (status="completed", job_id, output, variables, figures)
→ Release engine to pool
→ Return result to agent
Agent → execute_code("long_simulation()")
→ Security validator (OK)
→ Create job in tracker
→ Acquire engine from pool
→ Inject job context
→ Engine.execute("long_simulation()", background=True)
→ Wait for future (30s timeout)
→ 30s exceeded → TimeoutError
→ Spawn background coroutine to monitor future
→ Return {status: "pending", job_id: "abc123"}
→ Agent polls get_job_status("abc123") → {status: "running", progress: 45%}
→ Agent polls get_job_status("abc123") → {status: "running", progress: 90%}
→ Agent calls get_job_result("abc123") → {status: "completed", output: {...}}
→ Background coroutine collects final result, releases engine
Agent → execute_code(code1) [Pool has 2 engines, both busy]
→ Acquire engine → Blocked (queue size=1)
Agent → execute_code(code2) [Queue size increases]
→ Acquire engine → Blocked (queue size=2)
[Engine 1 finishes, released]
→ Acquire engine → Unblocks code1's executor
[Engine 2 finishes]
→ Acquire engine → Unblocks code2's executor
- One agent, one session (the fixed
"default"session) - Communication via stdin/stdout
- Simplest setup, no network configuration
- Session manager uses
get_or_create_default()for all requests
- Multiple agents, multiple sessions (each agent has a unique
session_id) - HTTP-based, supports remote connections
- Session isolation via context's
session_id - Enables per-client temp directories and workspace isolation
-
Production: Put behind a reverse proxy with authentication (
require_proxy_auth: true)- Prevents unauthorized agents from accessing the server
- Reverse proxy adds authentication headers; server validates via config flag