MATLAB MCP Server
by Tsuchijo
import os
from pathlib import Path
import base64
import subprocess
import sys
from typing import Optional, Dict, Any
from mcp.server.fastmcp import FastMCP, Image, Context
import io
from contextlib import redirect_stdout
# Get MATLAB path from environment variable with default fallback
MATLAB_PATH = os.getenv('MATLAB_PATH', '/Applications/MATLAB_R2024a.app')
# Initialize FastMCP server with dependencies
mcp = FastMCP(
"MATLAB",
dependencies=[
"mcp[cli]"
]
)
def ensure_matlab_engine():
"""Ensure MATLAB engine is installed for the current Python environment."""
try:
import matlab.engine
return True
except ImportError:
if not os.path.exists(MATLAB_PATH):
raise RuntimeError(
f"MATLAB installation not found at {MATLAB_PATH}. "
"Please set MATLAB_PATH environment variable to your MATLAB installation directory."
)
# Try to install MATLAB engine
engine_setup = Path(MATLAB_PATH) / "extern/engines/python/setup.py"
if not engine_setup.exists():
raise RuntimeError(
f"MATLAB Python engine setup not found at {engine_setup}. "
"Please verify your MATLAB installation."
)
print(f"Installing MATLAB engine from {engine_setup}...", file=sys.stderr)
try:
subprocess.run(
[sys.executable, str(engine_setup), "install"],
check=True,
capture_output=True,
text=True
)
print("MATLAB engine installed successfully.", file=sys.stderr)
import matlab.engine
return True
except subprocess.CalledProcessError as e:
raise RuntimeError(
f"Failed to install MATLAB engine: {e.stderr}\n"
"Please try installing manually or check your MATLAB installation."
)
# Try to initialize MATLAB engine
ensure_matlab_engine()
import matlab.engine
eng = matlab.engine.start_matlab()
# Create a directory for MATLAB scripts if it doesn't exist
MATLAB_DIR = Path("matlab_scripts")
MATLAB_DIR.mkdir(exist_ok=True)
@mcp.tool()
def create_matlab_script(script_name: str, code: str) -> str:
"""Create a new MATLAB script file.
Args:
script_name: Name of the script (without .m extension)
code: MATLAB code to save
Returns:
Path to the created script
"""
if not script_name.isidentifier():
raise ValueError("Script name must be a valid MATLAB identifier")
script_path = MATLAB_DIR / f"{script_name}.m"
with open(script_path, 'w') as f:
f.write(code)
return str(script_path)
@mcp.tool()
def create_matlab_function(function_name: str, code: str) -> str:
"""Create a new MATLAB function file.
Args:
function_name: Name of the function (without .m extension)
code: MATLAB function code including function definition
Returns:
Path to the created function file
"""
if not function_name.isidentifier():
raise ValueError("Function name must be a valid MATLAB identifier")
# Verify code starts with function definition
if not code.strip().startswith('function'):
raise ValueError("Code must start with function definition")
function_path = MATLAB_DIR / f"{function_name}.m"
with open(function_path, 'w') as f:
f.write(code)
return str(function_path)
@mcp.tool()
def execute_matlab_script(script_name: str, args: Optional[Dict[str, Any]] = None) -> dict:
"""Execute a MATLAB script and return results."""
script_path = MATLAB_DIR / f"{script_name}.m"
if not script_path.exists():
raise FileNotFoundError(f"Script {script_name}.m not found")
# Add script directory to MATLAB path
eng.addpath(str(MATLAB_DIR))
# Clear previous figures
eng.close('all', nargout=0)
# Create a temporary file for MATLAB output
temp_output_file = MATLAB_DIR / f"temp_output_{script_name}.txt"
# Execute the script
result = {}
try:
if args:
# Convert Python types to MATLAB types
matlab_args = {k: matlab.double([v]) if isinstance(v, (int, float)) else v
for k, v in args.items()}
eng.workspace['args'] = matlab_args
# Set up diary to capture output
eng.eval(f"diary('{temp_output_file}')", nargout=0)
eng.eval(script_name, nargout=0)
eng.eval("diary off", nargout=0)
# Read captured output
if temp_output_file.exists():
with open(temp_output_file, 'r') as f:
printed_output = f.read().strip()
# Clean up temp file
os.remove(temp_output_file)
else:
printed_output = "No output captured"
result['printed_output'] = printed_output
# Rest of your code for figures and workspace variables...
# Capture figures if any were generated
figures = []
fig_handles = eng.eval('get(groot, "Children")', nargout=1)
if fig_handles:
for i, fig in enumerate(fig_handles):
# Save figure to temporary file
temp_file = f"temp_fig_{i}.png"
eng.eval(f"saveas(figure({i+1}), '{temp_file}')", nargout=0)
# Read the file and convert to base64
with open(temp_file, 'rb') as f:
img_data = f.read()
figures.append(Image(data=img_data, format='png'))
# Clean up temp file
os.remove(temp_file)
result['figures'] = figures
# Get workspace variables
var_names = eng.eval('who', nargout=1)
for var in var_names:
if var != 'args': # Skip the args we passed in
val = eng.workspace[var]
# Clean variable name for JSON compatibility
clean_var_name = var.strip().replace(' ', '_')
val_str = str(val)
# Truncate long values to prevent excessive output
max_length = 1000 # Maximum length for variable values
if len(val_str) > max_length:
val_str = val_str[:max_length] + "... [truncated]"
val = val_str # Replace the original value with the string representation
result[clean_var_name] = val
except Exception as e:
raise RuntimeError(f"MATLAB execution error: {str(e)}")
return result
@mcp.tool()
def call_matlab_function(function_name: str, args: Any) -> dict:
"""Call a MATLAB function with arguments."""
function_path = MATLAB_DIR / f"{function_name}.m"
if not function_path.exists():
raise FileNotFoundError(f"Function {function_name}.m not found")
# Add function directory to MATLAB path
eng.addpath(str(MATLAB_DIR))
# Clear previous figures
eng.close('all', nargout=0)
# Create a temporary file for MATLAB output
temp_output_file = MATLAB_DIR / f"temp_output_{function_name}.txt"
# Convert Python arguments to MATLAB types
matlab_args = []
for arg in args:
if isinstance(arg, (int, float)):
matlab_args.append(matlab.double([arg]))
elif isinstance(arg, list):
matlab_args.append(matlab.double(arg))
else:
matlab_args.append(arg)
result = {}
try:
# Set up diary to capture output
eng.eval(f"diary('{temp_output_file}')", nargout=0)
# Call the function
output = getattr(eng, function_name)(*matlab_args)
# Turn off diary
eng.eval("diary off", nargout=0)
# Read captured output
if temp_output_file.exists():
with open(temp_output_file, 'r') as f:
printed_output = f.read().strip()
# Clean up temp file
os.remove(temp_output_file)
else:
printed_output = "No output captured"
result['output'] = str(output)
result['printed_output'] = printed_output
# Capture figures - rest of your code remains the same
figures = []
fig_handles = eng.eval('get(groot, "Children")', nargout=1)
if fig_handles:
for i, fig in enumerate(fig_handles):
# Save figure to temporary file
temp_file = f"temp_fig_{i}.png"
eng.eval(f"saveas(figure({i+1}), '{temp_file}')", nargout=0)
# Read the file and convert to base64
with open(temp_file, 'rb') as f:
img_data = f.read()
figures.append(Image(data=img_data, format='png'))
# Clean up temp file
os.remove(temp_file)
result['figures'] = figures
except Exception as e:
raise RuntimeError(f"MATLAB execution error: {str(e)}")
return result
@mcp.resource("matlab://scripts/{script_name}")
def get_script_content(script_name: str) -> str:
"""Get the content of a MATLAB script.
Args:
script_name: Name of the script (without .m extension)
Returns:
Content of the MATLAB script
"""
script_path = MATLAB_DIR / f"{script_name}.m"
if not script_path.exists():
raise FileNotFoundError(f"Script {script_name}.m not found")
with open(script_path) as f:
return f.read()
if __name__ == "__main__":
mcp.run(transport='stdio')