Skip to main content
Glama

MATLAB MCP Server

main.py10.9 kB
import sys import json import asyncio import logging import numpy as np from typing import Any, Dict, List import tempfile import os # MCP specific imports from mcp.server.fastmcp import FastMCP import mcp.types as types logging.basicConfig( level=logging.INFO, # Set to DEBUG for more verbose output stream=sys.stderr, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger = logging.getLogger("MatlabMCP") import matlab.engine mcp = FastMCP("MatlabMCP") # --- MATLAB Engine Connection --- logger.info("Finding shared MATLAB sessions...") names = matlab.engine.find_matlab() logger.info(f"Found sessions: {names}") eng = None if not names: logger.error("No shared MATLAB sessions found. This server requires a shared MATLAB session.") logger.error("Please start MATLAB and run 'matlab.engine.shareEngine' in its Command Window.") sys.exit(1) # Exit if no MATLAB found else: session_name = names[0] logger.info(f"Attempting to connect to MATLAB session: {session_name}") try: eng = matlab.engine.connect_matlab(session_name) logger.info(f"Successfully connected to shared MATLAB session: {session_name}") except matlab.engine.EngineError as e: logger.error(f"Error connecting to MATLAB session '{session_name}': {e}", exc_info=True) sys.exit(1) # Exit if connection fails except Exception as e: # Catch any other unexpected connection error logger.error(f"An unexpected error occurred while trying to connect to MATLAB: {e}", exc_info=True) sys.exit(1) if eng is None: # Should not be reached if sys.exit(1) was hit, but as a safeguard logger.critical("MATLAB engine 'eng' is None after connection attempt. Exiting.") sys.exit(1) # --- Helper Function --- def matlab_to_python(data: Any) -> Any: """ Converts common MATLAB data types returned by the engine into JSON-Serializable Python types. """ if isinstance(data, (str, int, float, bool, type(None))): return data elif isinstance(data, matlab.double): np_array = np.array(data).squeeze() if np_array.ndim == 0: return float(np_array) return np_array.tolist() # Handles vectors and matrices to lists/nested lists elif isinstance(data, matlab.logical): np_array = np.array(data).squeeze() if np_array.ndim == 0: return bool(np_array) return np_array.tolist() elif isinstance(data, matlab.char): return str(data) else: logger.warning(f"Unsupported MATLAB type encountered for conversion: {type(data)}. Attempting string representation.") try: return str(data) except Exception as e_str_conv: logger.error(f"Could not convert type {type(data)} to string: {e_str_conv}") return f"Unserializable MATLAB Type: {type(data)}" # --- TODO: Add more MATLAB types like structs, cell arrays, tables --- # --- Tool Definitions --- @mcp.tool() async def runMatlabCode(code: str) -> Dict[str, Any]: """ Runs arbitrary MATLAB code in the shared MATLAB session. WARNING: Executing arbitrary code can be a security risk. This tool attempts execution via a temporary file first, then falls back to eng.evalc() to capture output. Args: code: The MATLAB code string to execute. Returns: A dictionary with: - "status": "success" or "error" - "output": (on success) A message indicating success or the captured output from eng.evalc(). - "error_type": (on error) The type of Python exception. - "stage": (on error) The stage of execution where the error occurred. - "message": (on error) A detailed error message. """ logger.info(f"runMatlabCode request: {code[:150]}...") # Log a bit more of the code if not eng: # Should be caught at startup, but good check logger.error("runMatlabCode: MATLAB engine not available.") return {"status": "error", "error_type": "RuntimeError", "message": "MATLAB engine not available."} temp_file_path = None try: # --- Attempt 1: Execute using a temporary .m file --- # This is often more robust for multi-line scripts or function definitions with tempfile.NamedTemporaryFile(mode="w", suffix=".m", delete=False, encoding='utf-8') as tmp: tmp.write(code) temp_file_path = tmp.name logger.debug(f"Attempting to run code via temporary file: {temp_file_path}") # Run blocking MATLAB call in a thread await asyncio.to_thread(eng.run, temp_file_path, nargout=0) logger.info(f"Code executed successfully using temporary file: {temp_file_path}") return {"status": "success", "output": f"Code executed successfully via temporary file ({os.path.basename(temp_file_path)})."} except matlab.engine.MatlabExecutionError as e_run: # This error means MATLAB itself had an issue running the code in the temp file logger.warning(f"Temporary file execution failed: {e_run}. Attempting eng.evalc() as fallback...") # --- Attempt 2: Fallback to eng.evalc() to capture output --- try: result = await asyncio.to_thread(eng.evalc, code) logger.info("Code executed successfully using eng.evalc() fallback.") return {"status": "success", "output": result} except matlab.engine.MatlabExecutionError as e_evalc: logger.error(f"eng.evalc() fallback also failed: {e_evalc}", exc_info=True) return { "status": "error", "error_type": "MatlabExecutionError", "stage": "evalc_fallback", "message": f"MATLAB execution failed (tried temp file then evalc): {str(e_evalc)}" } except Exception as e_evalc_other: # Catch other errors during evalc logger.error(f"Unexpected error during eng.evalc() fallback: {e_evalc_other}", exc_info=True) return { "status": "error", "error_type": e_evalc_other.__class__.__name__, "stage": "evalc_fallback", "message": f"Unexpected error during eng.evalc() fallback: {str(e_evalc_other)}" } except matlab.engine.EngineError as e_eng: # Errors related to engine communication logger.error(f"MATLAB Engine communication error in runMatlabCode: {e_eng}", exc_info=True) return {"status": "error", "error_type": "EngineError", "message": f"MATLAB Engine error: {str(e_eng)}"} except IOError as e_io: # Catch errors related to temp file I/O logger.error(f"IOError during temporary file operation for runMatlabCode: {e_io}", exc_info=True) return {"status": "error", "error_type": "IOError", "message": f"File operation error: {str(e_io)}"} except Exception as e_outer: # Catch-all for other unexpected errors logger.error(f"Unexpected error in runMatlabCode: {e_outer}", exc_info=True) return {"status": "error", "error_type": e_outer.__class__.__name__, "message": f"An unexpected error occurred: {str(e_outer)}"} finally: # --- Cleanup: Ensure temporary file is deleted --- if temp_file_path and os.path.exists(temp_file_path): try: os.remove(temp_file_path) logger.debug(f"Cleaned up temporary file: {temp_file_path}") except OSError as e_cleanup: logger.warning(f"Could not clean up temporary file {temp_file_path}: {e_cleanup}") @mcp.tool() async def getVariable(variable_name: str) -> Dict[str, Any]: """ Gets the value of a variable from the MATLAB workspace. Args: variable_name: The name of the variable to retrieve. Returns: A dictionary with: - "status": "success" or "error" - "variable": (on success) The name of the variable. - "value": (on success) The JSON-serializable value of the variable. - "error_type": (on error) The type of Python exception. - "message": (on error) A detailed error message. """ logger.info(f"getVariable request for: '{variable_name}'") if not eng: # Should be caught at startup logger.error("getVariable: MATLAB engine not available.") return {"status": "error", "error_type": "RuntimeError", "message": "MATLAB engine not available."} if not variable_name or not isinstance(variable_name, str): # Basic input validation logger.warning(f"getVariable: Invalid variable_name provided: {variable_name}") return {"status": "error", "error_type": "ValueError", "message": "Invalid variable_name: must be a non-empty string."} try: # Synchronous part for asyncio.to_thread def get_var_from_matlab_sync(): # Check if variable exists directly in the workspace if variable_name not in eng.workspace: raise KeyError(f"Variable '{variable_name}' not found in MATLAB workspace.") return eng.workspace[variable_name] matlab_value = await asyncio.to_thread(get_var_from_matlab_sync) python_value = matlab_to_python(matlab_value) # Test JSON serialization of the converted value try: json.dumps({"test_value": python_value}) # Wrapped in a dict for a robust test logger.info(f"Successfully retrieved and converted variable '{variable_name}'.") return {"status": "success", "variable": variable_name, "value": python_value} except TypeError as json_err: logger.error(f"Serialization Error: Failed to serialize MATLAB value for '{variable_name}' (type: {type(matlab_value)}, py_type: {type(python_value)}) after conversion: {json_err}", exc_info=True) return { "status": "error", "error_type": "TypeError", "message": f"Value for variable '{variable_name}' could not be JSON serialized after conversion. Original MATLAB type: {type(matlab_value)}" } except KeyError as ke: # Variable not found logger.warning(f"getVariable: {ke}") return {"status": "error", "error_type": "KeyError", "message": str(ke)} except matlab.engine.EngineError as e_eng: logger.error(f"MATLAB Engine error during getVariable for '{variable_name}': {e_eng}", exc_info=True) return {"status": "error", "error_type": "EngineError", "message": f"MATLAB Engine error: {str(e_eng)}"} except Exception as e: logger.error(f"Unexpected error in getVariable for '{variable_name}': {e}", exc_info=True) return {"status": "error", "error_type": e.__class__.__name__, "message": f"An unexpected error occurred: {str(e)}"} if __name__ == "__main__": logger.info("Starting MATLAB MCP server...") # mcp.run() blocks until shutdown mcp.run(transport='stdio') # This line will only execute AFTER the server has stopped. logger.info("MATLAB MCP Server has shut down.")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jigarbhoye04/MatlabMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server