-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
AI Agent (Claude, Cursor, Copilot, etc.)
│
│ MCP Protocol (stdio or SSE)
▼
┌─────────────────────────────────┐
│ MCP Server (FastMCP) │
│ ├─ 20 built-in tools │
│ ├─ Custom tools (from YAML) │
│ ├─ Session manager │
│ ├─ Security validator │
│ ├─ Result formatter │
│ ├─ Job tracker │
│ └─ Monitoring (optional) │
└──────────┬──────────────────────┘
│
┌──────────▼──────────────────────┐
│ Job Executor │
│ ├─ Hybrid sync/async execution │
│ ├─ Timeout-based promotion │
│ ├─ Job context injection │
│ └─ Background task monitoring │
└──────────┬──────────────────────┘
│
┌──────────▼──────────────────────┐
│ Engine Pool Manager │
│ ├─ Elastic scaling (min→max) │
│ ├─ Health checks │
│ ├─ Workspace reset │
│ └─ Idle scale-down │
└──────────┬──────────────────────┘
│
┌──────────▼──────────────────────┐
│ MATLAB Engines (2020b+) │
│ Engine 1 │ Engine 2 │ ... │ N │
└───────────────────────────────────┘
The entry point. Uses FastMCP to handle MCP protocol details. Responsibilities:
- Register all 20 built-in tools + custom tools from YAML
- Manage server lifecycle (startup/shutdown via lifespan context manager)
- Route tool calls to implementation modules
- Create and manage shared component instances:
EnginePoolManager,JobTracker,JobExecutor,SessionManager,SecurityValidator, optionalMetricsCollector - Run background tasks (health checks, session cleanup, metrics collection)
- Handle both stdio (single default session) and SSE (multi-session) transports
- Log security warnings (e.g., SSE without proxy auth)
Key class: MatlabMCPServer — holds all stateful components. Key methods:
-
_get_session_id(ctx)— returns "default" for stdio, or context's session_id for SSE -
_get_temp_dir(session_id)— retrieves or creates session, returns its temp directory
Manages a pool of MATLAB engine instances:
-
Initialization: Starts
min_enginesengines in parallel during startup -
Elastic scaling: On
acquire(), returns an available engine if present. If none available and total <max_engines, starts a new engine. If at max capacity, waits for one to become available -
Scale-down:
run_health_checks()stops idle engines beyondmin_enginesthat have been idle longer thanscale_down_idle_timeout(default 15 min) -
Health checks: Periodic
1+1eval to verify engines are responsive. Dead engines are stopped and replaced -
Queue management: Uses
asyncio.Queueto track available engines and serialize acquisitions -
Recording events: Emits
engine_scale_upevent to optionalMetricsCollector
Key methods:
-
async start()— start min_engines in parallel -
async acquire()— acquire or create an engine, mark busy -
async release(engine)— reset workspace, mark idle, return to queue -
async stop()— gracefully stop all engines -
async run_health_checks()— check idle engines, replace dead ones, scale down excess
Wraps a single matlab.engine instance:
-
State tracking: Tracks
EngineState(STOPPED, STARTING, IDLE, BUSY) and idle time -
Lifecycle:
start()launches the engine, applies default paths and startup commands;stop()quits it -
Health check:
health_check()runs trivial1+1eval to confirm responsiveness -
Code execution:
execute(code, nargout, background, stdout, stderr)evaluates MATLAB code with optional background execution and I/O capture -
Workspace reset:
reset_workspace()clears all, runs cleanup sequence, restores paths and startup commands -
Lazy imports: Uses lazy
importlibto importmatlab.engineso tests can mock it
Key properties:
-
state— current engine state -
idle_seconds— seconds since last marked idle -
is_alive— checks if underlying engine object is healthy
Hybrid sync/async execution orchestrator:
-
Job creation: Creates a
Jobin the tracker with unique ID - Engine acquisition: Acquires an engine from the pool
-
Context injection: Injects job metadata (
__mcp_job_id__,__mcp_temp_dir__,__mcp_session_id__) into MATLAB workspace -
Background execution: Starts code execution with
engine.execute(code, background=True)and captures stdout/stderr viaio.StringIO -
Sync wait: Waits up to
sync_timeoutseconds for the future to complete- If completes: builds result dict inline, marks job completed, releases engine, returns status="completed"
- If times out: promotes to async, launches background task, returns status="pending" with job_id
- Error handling: On execution error, marks job failed and returns error result
-
Metrics: Records
job_completedandjob_failedevents to optionalMetricsCollector
Key methods:
-
async execute(session_id, code, temp_dir)— main entry point, returns result dict -
_inject_job_context(engine, job, temp_dir)— sets job variables in MATLAB -
_build_result(engine, raw_result, job, temp_dir)— formats output, variables, figures -
async _wait_for_completion(job, engine, future, temp_dir)— background task for async jobs
In-memory store for job metadata and results:
- Create/get/list/cancel jobs
- Store raw results and status transitions
- Prune completed jobs older than
job_retention_seconds(default 3600) - Thread-safe with
asyncio.Lock
Key model: Job with fields: job_id, session_id, code, status, engine_id, future, result, error_type, message, created_at, started_at, completed_at, _stdout, _stderr
Per-user session isolation:
-
Session creation: Each session gets a unique temp directory under
config.execution.temp_dir - Default session: stdio transport uses a fixed "default" session
- SSE transport: Creates sessions on-demand keyed to client's session_id
-
Session cleanup:
cleanup_expired()removes sessions idle longer thansession_timeout(default 3600s), skipping those with active jobs -
Max sessions: Enforces
max_sessionslimit (default 50) -
Metrics: Records
session_createdevent to optionalMetricsCollector
Key class: Session with fields: session_id, temp_dir, created_at, last_active
Key methods:
-
create_session(session_id=None)— create session with unique temp dir -
get_session(session_id)— retrieve existing session -
get_or_create_default()— get or create "default" session for stdio -
destroy_session(session_id)— clean up session and remove temp dir -
cleanup_expired(has_active_jobs_fn=None)— remove idle sessions
Pre-execution security checks:
-
Function blocklist: Scans code for dangerous functions:
system,unix,dos,!,eval,feval,evalc,evalin,assignin,perl,python. Strips comments and string literals first to avoid false positives - Filename sanitization: Prevents path traversal in upload filenames
-
Upload size limits: Enforces
max_upload_size_mb(default 100) -
Metrics: Records
security_violationevents to optionalMetricsCollector
Structures tool responses:
-
Text output: Formats with length limits (
max_inline_text_length, default 10000 chars). Truncated text can be saved to file - Variable formatting: Summarizes workspace variables with name, type, size, value (large items show type/size only)
- Success response: Builds dict with status, job_id, output, variables, figures, files, warnings, execution_time
- Error response: Builds dict with error_type, message, matlab_id (if available)
- File listings: Formats workspace and session file listings
Key methods:
-
format_text(text, save_dir=None)— truncate text, optionally save to file -
format_variables(variables)— summarize workspace dict -
build_success_response(...)— assemble result dict -
build_error_response(...)— assemble error dict
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) -
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 saved JSON file - Result includes: Plotly JSON + static PNG + optional thumbnail
Optional metrics collection and storage:
- MetricsCollector: Records events (engine scale-up/down, job completed/failed, security violations, session created) with timestamps
- MetricsStore: Persists metrics to disk, supports filtering and retrieval
-
Enabled via:
config.monitoring.enabled = true - No overhead when disabled
Agent → execute_code("x = magic(3)")
→ Security check (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)
→ Complete in <sync_timeout (30s default)
→ Build result (text, variables, figures)
→ Release engine (reset workspace)
→ Mark job completed
→ Return {"status": "completed", "job_id": "abc123", output: {...}}
Agent → execute_code("long_simulation()")
→ Security check (OK)
→ Create job in tracker
→ Acquire engine from pool
→ Inject job context
→ Engine.execute(background=True) starts in background
→ sync_timeout (30s) exceeded
→ Promote to async background task
→ Release engine from executor (kept busy in background)
→ 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}
→ Background task completes, marks job completed
→ Agent calls get_job_result("abc123") → full result
→ Engine released
Job context variables injected into MATLAB workspace before execution:
__mcp_job_id__ = "uuid-string"
__mcp_temp_dir__ = "/path/to/session/temp"
__mcp_session_id__ = "session-uuid"
User code can reference these to:
- Save artifacts to __mcp_temp_dir__
- Log job progress to files
- Access workspace isolation guarantees
- One agent, one session (the "default" session)
- Communication via stdin/stdout
- Simplest setup, no network
- Session manager creates and reuses a single "default" session
- Multiple agents, multiple sessions
- HTTP-based, supports remote connections
- Session isolation: each request's
context.session_idmaps to a uniqueSession - Sessions are created on-demand and cleaned up after
session_timeout -
Production: Put behind a reverse proxy with auth (
require_proxy_auth: true) - Security warning logged if SSE enabled without
require_proxy_auth
┌─────────────────────────────────────────────────┐
│ FastMCP Server (server.py) │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ MatlabMCPServer (state holder) │ │
│ ├────────────────────────────────────────┤ │
│ │ - config: AppConfig │ │
│ │ - pool: EnginePoolManager │───┼──→ Manages engine lifecycle
│ │ - tracker: JobTracker │───┼──→ Stores job state/results
│ │ - executor: JobExecutor │───┼──→ Executes code (uses pool)
│ │ - sessions: SessionManager │───┼──→ Manages temp dirs
│ │ - security: SecurityValidator │───┼──→ Pre-exec checks
│ │ - collector: MetricsCollector (opt) │───┼──→ Records metrics
│ └────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ Tool handlers call state │
│ │
└─────────────────────────────────────────────────┘
│ │ │
│ │ │
Tool A Tool B Tool N
(execute_code) (get_workspace) (list_files)
└─────────────────┬─────────────────┘
│
All use shared pool,
tracker, executor, sessions,
security, formatter, etc.