-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
The MATLAB MCP Server is a Python-based bridge that exposes MATLAB capabilities to AI agents (Claude, Cursor, Codex CLI, etc.) via the Model Context Protocol. It handles authentication, session isolation, engine pooling, job orchestration, security validation, and result formatting across multiple transport modes.
graph TB
Agent["AI Agent<br/>(Claude, Cursor, Codex CLI)"]
subgraph Transport["Transport Layer"]
Stdio["stdio<br/>(stdio)"]
SSE["SSE<br/>(deprecated)"]
StreamableHTTP["Streamable HTTP<br/>(recommended)"]
end
subgraph Auth["Auth Middleware"]
BearerAuth["BearerAuthMiddleware<br/>(validates Authorization header)"]
end
subgraph MCPServer["MCP Server<br/>(FastMCP 3.2.0)"]
Tools["20 Built-in Tools<br/>+ Custom Tools"]
SessionMgr["Session Manager<br/>(workspace isolation)"]
end
subgraph Execution["Job Execution Layer"]
JobExec["Job Executor<br/>(sync/async hybrid)"]
JobTracker["Job Tracker<br/>(lifecycle tracking)"]
SecurityVal["Security Validator<br/>(code safety)"]
end
subgraph Pool["Engine Pool Layer"]
PoolMgr["EnginePoolManager<br/>(elastic scaling)"]
Engine1["MATLAB Engine 1"]
Engine2["MATLAB Engine 2"]
EngineN["... Engine N"]
end
subgraph Output["Output & Monitoring"]
Formatter["Result Formatter<br/>(text, vars, figures)"]
PlotlyConv["Plotly Converter<br/>(MATLAB → interactive viz)"]
Metrics["Metrics Collector<br/>(SQLite store)"]
end
Agent -->|MCP protocol| Transport
Transport -->|HTTP/SSE| Auth
Auth -->|ASGI| MCPServer
Transport -->|stdio| MCPServer
MCPServer --> JobExec
MCPServer --> SessionMgr
JobExec --> SecurityVal
SecurityVal --> JobTracker
JobTracker --> PoolMgr
PoolMgr --> Engine1
PoolMgr --> Engine2
PoolMgr --> EngineN
Engine1 -->|code execution| MATLAB["MATLAB Runtime<br/>(R2022b+)"]
Engine2 --> MATLAB
EngineN --> MATLAB
JobExec --> Formatter
Formatter --> PlotlyConv
PlotlyConv --> Metrics
Metrics -->|result| Agent
Responsibility: Manage protocol-specific communication channels
- stdio (single-user, default): Direct stdin/stdout. No auth layer. Used for local development.
- SSE (multi-user, deprecated): Server-Sent Events over HTTP. Marked deprecated as of v2.0; kept for backward compatibility.
-
Streamable HTTP (multi-user, recommended): FastMCP 3.2+ native transport at
/mcpendpoint. Supports multiple concurrent sessions with bearer token auth.
Key Files:
-
src/matlab_mcp/config.py:ServerConfig.transportfield (Literal["stdio", "sse", "streamablehttp"]) -
src/matlab_mcp/server.py:main()function with transport branching logic
Responsibility: Enforce bearer token auth on HTTP transports (SSE, streamable HTTP)
- Validates
Authorization: Bearer <token>header on every request - Returns HTTP 401 with
WWW-Authenticateheader on invalid/missing token - Bypasses auth for
/healthendpoint (load balancer health checks) - Disabled for stdio transport (no HTTP layer)
- Token read exclusively from
MATLAB_MCP_AUTH_TOKENenvironment variable at startup - Constant-time comparison (
hmac.compare_digest) prevents timing attacks
Key Files:
-
src/matlab_mcp/auth/middleware.py:BearerAuthMiddleware(pure ASGI class) -
src/matlab_mcp/config.py:SecurityConfig(auth configuration) -
src/matlab_mcp/server.py: Middleware wiring and token generation CLI flag
Responsibility: Register and route tool calls to implementations
- FastMCP 3.2.0 instance with 20 built-in tools + dynamic custom tools
-
@mcp.tooldecorators for tool registration - Lifespan management (startup, shutdown hooks)
- Request context includes session ID, client ID, and MCP protocol state
Key Files:
-
src/matlab_mcp/server.py:MatlabMCPServerstate container, tool registration -
src/matlab_mcp/tools/*.py: Tool implementations (core, discovery, files, jobs, admin, monitoring) -
src/matlab_mcp/tools/custom.py: Dynamic tool loading fromcustom_tools.yaml
Responsibility: Isolate workspace per user session
- Creates unique session ID for each connected agent (HTTP) or uses default (stdio)
- Per-session temporary directory for file uploads/downloads
- Workspace variable clearing between sessions (optional, via config)
- Idle session cleanup after configurable timeout
Session Routing:
- stdio: Single "default" session, shared workspace
-
HTTP (SSE/streamablehttp): Session ID from header (
mcp-session-id) with fallback toclient_id(FastMCP 3.2+)
Key Files:
-
src/matlab_mcp/session/manager.py:SessionManagerclass -
src/matlab_mcp/server.py:_get_session_id(),_get_temp_dir()methods
Responsibility: Orchestrate MATLAB code execution with hybrid sync/async behavior
- Security check: Scan code for blocked functions (custom, configurable blocklist)
-
Context injection: Add
__mcp_job_id__and__mcp_temp_dir__to workspace -
Execution:
- Synchronous: Return result immediately if execution completes within
sync_timeout(default 30s) - Asynchronous: Auto-promote to background job if timeout exceeded; return
job_idimmediately; agent polls for completion
- Synchronous: Return result immediately if execution completes within
-
Progress reporting: Optional
mcp_progress()MATLAB helper updates progress files - Result formatting: Structure output (stdout, variables, figures, errors) into response
- Cleanup: Release engine back to pool; delete job metadata after retention period
Key Files:
-
src/matlab_mcp/jobs/executor.py:JobExecutorclass -
src/matlab_mcp/jobs/models.py:Jobdataclass,JobStatusenum -
src/matlab_mcp/jobs/tracker.py:JobTrackerin-memory registry -
src/matlab_mcp/matlab_helpers/mcp_progress.m: Progress file writer (called from MATLAB)
Responsibility: Manage lifecycle and availability of MATLAB engine instances
-
Elastic scaling: Start with
min_engines(default 1), scale up tomax_engines(default 4) under load - Proactive warmup: When utilization exceeds threshold (default 80%), start new engine before demand spike
-
Scale-down: Engines idle >15 min are stopped (down to
min_engines) -
Health checks: Periodic
1+1evaluation to detect dead engines; auto-replace - Queue: Requests wait in async queue when all engines busy (configurable max wait)
Engine States:
STOPPED ──start()──> STARTING ──ready──> IDLE ←──release()──┐
│ │ │
error acquire() busy
│ ↓ │
└─────> ERROR (working) ──done()──┘
Key Files:
-
src/matlab_mcp/pool/manager.py:EnginePoolManagerclass -
src/matlab_mcp/pool/engine.py:MatlabEngineWrapperclass,EngineStateenum
Responsibility: Prevent execution of dangerous MATLAB code
-
Function blocklist: Detects and rejects code containing blocked functions
- Default blocklist:
system,unix,dos,!(shell escape),eval,evalc,evalin,feval,assignin,perl,python - Smart parsing: Strips string literals and comments to avoid false positives
- Configurable via
security.blocked_functionsin config.yaml
- Default blocklist:
- Filename sanitization: Prevents path traversal in file operations (upload/delete/read)
- Upload limits: Enforces max file size and dataset limits
Key Files:
-
src/matlab_mcp/security/validator.py:SecurityValidatorclass,BlockedFunctionErrorexception
Responsibility: Structure and visualize execution results
Text Output:
- Truncate large stdout (default 10,000 chars)
- Save overflow to file and include file path in response
Variables:
- Display type, shape, value summary of workspace variables
- Truncate large collections (arrays >1000 elements)
Figures:
- Auto-detect MATLAB figures via
gcf() - Extract properties via
mcp_extract_props.m(MATLAB-side) - Convert to interactive Plotly JSON (Python-side)
- Support line, scatter, bar, histogram, 3D surface, heatmap, image
- Preserve style: colors, line styles, markers, legends, labels, fonts
- WebGL rendering for large point clouds (10k+ points)
Key Files:
-
src/matlab_mcp/output/formatter.py:ResultFormatterclass -
src/matlab_mcp/output/plotly_convert.py: Load Plotly JSON -
src/matlab_mcp/output/plotly_style_mapper.py: Convert MATLAB styles to Plotly -
src/matlab_mcp/matlab_helpers/mcp_extract_props.m: Extract figure properties
Responsibility: Collect system metrics, detect anomalies, provide observability
- In-memory metrics: Engine pool status, job counts, error rates, execution times
- Persistent storage: SQLite3 backend for time-series data (24-hour retention default)
- Health evaluation: Classify server status (healthy/degraded/unhealthy) based on thresholds
-
Dashboard: Web UI at
/dashboardwith live gauges, time-series charts, event log - Monitoring tools: MCP tools expose health, metrics, error logs to agents
Key Files:
-
src/matlab_mcp/monitoring/collector.py:MetricsCollectorclass -
src/matlab_mcp/monitoring/store.py:MetricsStoreclass (SQLite async) -
src/matlab_mcp/monitoring/health.py:evaluate_health()function -
src/matlab_mcp/monitoring/dashboard.py: Starlette sub-app with routes -
src/matlab_mcp/monitoring/static/: Dashboard HTML/CSS/JS frontend
sequenceDiagram
agent->>mcp: execute_code("x = magic(3)")
mcp->>security: validate code (OK)
mcp->>pool: acquire_engine()
pool->>engine: is_busy? (no)
pool-->>mcp: engine granted
mcp->>executor: execute(code, engine, job_id, temp_dir)
executor->>executor: inject context (job_id, temp_dir)
executor->>engine: eval("x = magic(3)")
engine->>matlab: x = magic(3)
matlab-->>engine: result (x = 3x3 matrix)
engine-->>executor: stdout, vars
executor->>formatter: format result
formatter-->>executor: response dict
executor->>pool: release_engine()
pool-->>mcp: engine released
mcp-->>agent: {status: "completed", output: "...", variables: {...}}
sequenceDiagram
agent->>mcp: execute_code("long_sim = monte_carlo(1e6)")
mcp->>security: validate code (OK)
mcp->>pool: acquire_engine()
pool-->>mcp: engine granted
mcp->>executor: execute(code, engine, job_id, temp_dir)
executor->>engine: eval(code, background=True)
engine->>matlab: long_sim = monte_carlo(1e6) [bg]
matlab-->>engine: job_id (continue in background)
executor->>tracker: mark job RUNNING
executor-->>mcp: {job_id: "j-123", status: "running"}
mcp-->>agent: {job_id: "j-123", status: "running"}
par Background Execution
engine->>matlab: [job running...]
matlab->>matlab: [progress via mcp_progress.m]
and Agent Polling
agent->>mcp: get_job_status("j-123")
mcp->>tracker: query job
tracker-->>mcp: {status: "running", progress: 45%}
mcp-->>agent: {status: "running", progress: 45%}
agent->>mcp: get_job_status("j-123")
mcp->>tracker: query job
tracker-->>mcp: {status: "running", progress: 90%}
mcp-->>agent: {status: "running", progress: 90%}
end
engine->>tracker: mark job COMPLETED
agent->>mcp: get_job_result("j-123")
mcp->>tracker: query job
tracker-->>mcp: {status: "completed", result: {...}}
mcp->>pool: release_engine()
pool-->>mcp: engine released
mcp-->>agent: {status: "completed", result: {...}}
Decision: Static bearer tokens in environment variable, constant-time comparison, 401 on invalid
Rationale:
- Agents (Claude Code, Codex CLI, Cursor) natively support
Authorization: Bearer <token>headers - OAuth requires browser redirect → incompatible with CLI agents
- Corporate firewalls often block callback URLs
- For internal team tools, static tokens are the MCP spec recommendation
- Can be rotated by restarting with new
MATLAB_MCP_AUTH_TOKEN
Trade-off: No per-user audit trail (all agents use same token). Mitigated by reverse proxy auth for production.
Decision: Synchronous by default (fast path), auto-promote to async if timeout exceeded
Rationale:
- Agent UX: Immediate results for quick commands (<30s) are better than round-trip latency
- Resource efficiency: Long jobs don't block the engine pool while waiting
- Compatibility: Agents can choose sync or async based on expected duration
Trade-off: Requires job tracking and polling. Mitigated by simple job tracker and progress reporting.
Decision: Each HTTP session gets isolated temp dir; stdio uses single shared dir
Rationale:
- Prevents accidental file leaks between users
- Automatic cleanup on session expiration
- Simplifies file permission model (no per-user MATLAB workspaces)
Trade-off: Large files uploaded to one session not visible to another. By design.
Decision: Start small (1 engine), scale up to max on demand, scale down when idle
Rationale:
- Reduces resource overhead for light workloads
- Handles traffic spikes without request loss
- Proactive warmup avoids cold-start delays
Trade-off: Pool startup may delay first request. Mitigated by pre-warming in lifespan hook.
Decision: Replace SSE with FastMCP 3.2's /mcp endpoint; keep SSE deprecated for compat
Rationale:
- SSE is officially deprecated (as of v2.0)
- Codex CLI only supports streamable HTTP
- Cleaner protocol (single POST/GET, bidirectional framing via SSE)
- Built into FastMCP 3.2+ with native session routing
Trade-off: Existing SSE integrations need migration guidance. Documented in wiki.
graph LR
Server["MCP Server<br/>(FastMCP)"]
SessionMgr["Session Manager"]
JobExec["Job Executor"]
SecurityVal["Security Validator"]
JobTracker["Job Tracker"]
PoolMgr["Engine Pool<br/>Manager"]
Engine["MATLAB<br/>Engine"]
Formatter["Result<br/>Formatter"]
Metrics["Metrics<br/>Collector"]
Server -->|create/get session| SessionMgr
Server -->|validate code| SecurityVal
Server -->|execute job| JobExec
JobExec -->|create job| JobTracker
JobExec -->|acquire/release engine| PoolMgr
JobExec -->|format result| Formatter
JobExec -->|record metrics| Metrics
PoolMgr -->|eval code| Engine
JobTracker -->|query status| JobTracker
SessionMgr -.->|temp dir path| JobExec
SecurityVal -.->|OK/BLOCKED| JobExec
Formatter -.->|formatted response| Server
Metrics -.->|health data| Server
config.yaml (default values)
↓
MATLAB_MCP_* environment variables (overrides)
↓
Pydantic models in config.py
↓
AppConfig instance in MatlabMCPServer
↓
Sub-components (pool, executor, sessions, security, monitoring)
Key config sections:
-
server: transport, host, port, logging -
pool: min/max engines, health check interval -
execution: sync timeout, workspace isolation -
security: blocked functions, upload limits, code quality -
output: text truncation, Plotly styling -
monitoring: metrics retention, dashboard -
session: idle timeout, max concurrent sessions -
hitl(optional): human-in-the-loop approval gates
- Unit tests (733 tests): Mock MATLAB engine, test each component in isolation
- Integration tests (end-to-end): Start server subprocess, connect real MCP client via HTTP, verify tools
- CI/CD: Linux (Python 3.10, 3.12), Windows (x64), macOS (x86_64, arm64)
-
Markers:
@pytest.mark.matlab(requires MATLAB),@pytest.mark.integration(requires live server)