Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Run MATLAB® using AI applications. The MATLAB MCP Core Server allows your AI ap
- [Setup](#setup)
- [Claude Code](#claude-code)
- [Claude Desktop](#claude-desktop)
- [Cursor](#cursor)
- [GitHub Copilot in Visual Studio Code](#github-copilot-in-visual-studio-code)
- [Arguments](#arguments)
- [Tools](#tools)
Expand All @@ -20,7 +21,7 @@ Run MATLAB® using AI applications. The MATLAB MCP Core Server allows your AI ap

1. Install [MATLAB (MathWorks)](https://www.mathworks.com/help/install/ug/install-products-with-internet-connection.html) 2020b or later and add it to the system PATH.
2. Download the [Latest Release](https://github.com/matlab/matlab-mcp-core-server/releases/latest) from GitHub®. Alternatively, you can install [Go](https://go.dev/doc/install) and build the binary from source using `go install github.com/matlab/matlab-mcp-core-server/cmd/matlab-mcp-core-server`.
3. Add the MATLAB MCP Core Server to your AI application. You can find instructions for adding MCP servers in the documentation of your AI application. For example instructions on using Claude Code®, Claude Desktop®, and GitHub Copilot in Visual Studio® Code, see below. Note that you can customize the server by specifying optional [arguments](#arguments).
3. Add the MATLAB MCP Core Server to your AI application. You can find instructions for adding MCP servers in the documentation of your AI application. For example instructions on using Claude Code®, Claude Desktop®, Cursor, and GitHub Copilot in Visual Studio® Code, see below. Note that you can customize the server by specifying optional [arguments](#arguments).

#### Download on macOS

Expand Down Expand Up @@ -86,6 +87,27 @@ Follow the instructions on the page [Connect to local MCP servers (MCP)](https:/
```
After saving the configuration file, quit and restart Claude Desktop.

### Cursor

Configure the MATLAB MCP Core Server in Cursor by creating an `mcp.json` file. You can create a project-specific configuration in `.cursor/mcp.json` within your project directory, or a global configuration in `~/.cursor/mcp.json` in your home directory. In the configuration file, add the following, remembering to insert the full path to the server binary you acquired in the setup, as well as any other arguments:

```json
{
"mcpServers": {
"matlab": {
"command": "fullpath/to/matlab-mcp-core-server-binary",
"args": [
"--initial-working-folder=/home/username/Documents"
]
}
}
}
```

After saving the configuration file, restart Cursor to apply the changes.

For details on adding MCP servers in Cursor, see [Model Context Protocol (Cursor)](https://cursor.com/docs/context/mcp#model-context-protocol).

### GitHub Copilot in Visual Studio Code

VS Code provides different methods to [Add an MCP Server (VS Code)](https://code.visualstudio.com/docs/copilot/customization/mcp-servers?wt.md_id=AZ-MVP-5004796#_add-an-mcp-server). MathWorks recommends you follow the steps in the section **"Add an MCP server to a workspace `mcp.json` file"**. In your `mcp.json` configuration file, add the following, remembering to insert the full path to the server binary you acquired in the setup, as well as any arguments:
Expand Down
30 changes: 30 additions & 0 deletions cmd/matlab-mcp-core-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,43 @@ package main

import (
"context"
"fmt"
"log/slog"
"os"

"github.com/matlab/matlab-mcp-core-server/internal/utils/instancelock"
"github.com/matlab/matlab-mcp-core-server/internal/wire"
)

func main() {
// Check for existing instance before doing anything else
instanceLock, err := instancelock.New()
if err != nil {
slog.With("error", err).Error("Failed to create instance lock.")
os.Exit(1)
}

// Try to acquire lock, killing existing instance if found
// This ensures a fresh start when Cursor restarts the MCP server
acquired, err := instanceLock.TryLockWithKill(true)
if err != nil {
slog.With("error", err).Error("Failed to acquire instance lock.")
os.Exit(1)
}

if !acquired {
// This shouldn't happen if killExisting is true, but handle it anyway
fmt.Fprintf(os.Stderr, "MATLAB MCP Core Server is already running. Only one instance is allowed.\n")
os.Exit(0)
}

// Ensure lock is released on exit
defer func() {
if err := instanceLock.Unlock(); err != nil {
slog.With("error", err).Warn("Failed to release instance lock on exit.")
}
}()

modeSelector, err := wire.InitializeModeSelector()
if err != nil {
// As we failed to even initialize, we cannot use a LoggerFactory,
Expand Down
110 changes: 103 additions & 7 deletions internal/adaptors/globalmatlab/globalmatlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ package globalmatlab

import (
"context"
"fmt"
"os"
"sync"
"time"

"github.com/matlab/matlab-mcp-core-server/internal/entities"
)
Expand Down Expand Up @@ -32,6 +35,7 @@ type GlobalMATLAB struct {
matlabStartingDir string
sessionID entities.SessionID
cachedStartErr error
isReady bool
}

func New(
Expand All @@ -49,6 +53,9 @@ func New(
}

func (g *GlobalMATLAB) Initialize(ctx context.Context, logger entities.Logger) error {
logger = logger.With("mcp_server_pid", os.Getpid())
logger.Debug("GlobalMATLAB.Initialize called")

var err error
g.matlabRoot, err = g.matlabRootSelector.SelectFirstMATLABVersionOnPath(ctx, logger)
if err != nil {
Expand All @@ -65,44 +72,133 @@ func (g *GlobalMATLAB) Initialize(ctx context.Context, logger entities.Logger) e
return err
}

logger.Debug("GlobalMATLAB.Initialize completed successfully")
return nil
}

func (g *GlobalMATLAB) Client(ctx context.Context, logger entities.Logger) (entities.MATLABSessionClient, error) {
if err := g.ensureMATLABClientIsValid(ctx, logger); err != nil {
return nil, err
}
// Retry logic with exponential backoff for transient connection failures
var lastErr error
maxRetries := 5
baseDelay := 200 * time.Millisecond

for attempt := 0; attempt < maxRetries; attempt++ {
if attempt > 0 {
// Exponential backoff: 200ms, 400ms, 800ms, 1600ms, 3200ms
delay := baseDelay * time.Duration(1<<uint(attempt-1))
logger.With("attempt", attempt+1).With("delay_ms", delay.Milliseconds()).Debug("Retrying MATLAB client connection")
time.Sleep(delay)
}

client, err := g.matlabManager.GetMATLABSessionClient(ctx, logger, g.sessionID)
if err != nil {
return nil, err
if err := g.ensureMATLABClientIsValid(ctx, logger); err != nil {
lastErr = err
// Don't retry on permanent errors (cached start errors)
if g.cachedStartErr != nil {
return nil, err
}
continue
}

client, err := g.matlabManager.GetMATLABSessionClient(ctx, logger, g.sessionID)
if err != nil {
lastErr = err
continue
}

// Test the connection with a simple eval to ensure MATLAB is ready
g.lock.Lock()
needsReadyCheck := !g.isReady
g.lock.Unlock()

if needsReadyCheck {
if err := g.waitForMATLABReady(ctx, logger, client); err != nil {
lastErr = err
logger.WithError(err).With("attempt", attempt+1).Debug("MATLAB not ready yet, will retry")
continue
}
g.lock.Lock()
g.isReady = true
g.lock.Unlock()
logger.Debug("MATLAB connection verified and ready")
}

return client, nil
}

return client, nil
return nil, fmt.Errorf("failed to get MATLAB client after %d attempts: %w", maxRetries, lastErr)
}

func (g *GlobalMATLAB) ensureMATLABClientIsValid(ctx context.Context, logger entities.Logger) error {
g.lock.Lock()
defer g.lock.Unlock()

if g.cachedStartErr != nil {
logger.Debug("ensureMATLABClientIsValid: returning cached error")
return g.cachedStartErr
}

var sessionIDZeroValue entities.SessionID
if g.sessionID == sessionIDZeroValue {
logger.With("matlab_root", g.matlabRoot).Debug("ensureMATLABClientIsValid: starting new MATLAB session")
sessionID, err := g.matlabManager.StartMATLABSession(ctx, logger, entities.LocalSessionDetails{
MATLABRoot: g.matlabRoot,
StartingDirectory: g.matlabStartingDir,
ShowMATLABDesktop: true,
})
if err != nil {
g.cachedStartErr = err
logger.WithError(err).Error("ensureMATLABClientIsValid: failed to start MATLAB session")
return err
}

g.sessionID = sessionID
g.isReady = false // Reset readiness when starting a new session
logger.With("session_id", sessionID).Debug("ensureMATLABClientIsValid: MATLAB session started, waiting for connection to be ready")
} else {
logger.With("session_id", g.sessionID).Debug("ensureMATLABClientIsValid: reusing existing MATLAB session")
}

return nil
}

// waitForMATLABReady tests the MATLAB connection with a simple eval to ensure it's ready
// This gives MATLAB time to fully initialize the Embedded Connector before accepting requests
func (g *GlobalMATLAB) waitForMATLABReady(ctx context.Context, logger entities.Logger, client entities.MATLABSessionClient) error {
// Create a timeout context for the readiness check (max 30 seconds)
readyCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

// Try a simple eval with retries
maxAttempts := 10
baseDelay := 500 * time.Millisecond

for attempt := 0; attempt < maxAttempts; attempt++ {
if attempt > 0 {
delay := baseDelay * time.Duration(1<<uint(attempt-1))
if delay > 5*time.Second {
delay = 5 * time.Second // Cap at 5 seconds
}
logger.With("attempt", attempt+1).With("delay_ms", delay.Milliseconds()).Debug("Waiting for MATLAB to be ready")

select {
case <-readyCtx.Done():
return fmt.Errorf("timeout waiting for MATLAB to be ready: %w", readyCtx.Err())
case <-time.After(delay):
}
}

// Test connection with a simple eval
_, err := client.Eval(readyCtx, logger, entities.EvalRequest{
Code: "1+1",
})

if err == nil {
logger.Debug("MATLAB connection test successful")
return nil
}

logger.WithError(err).With("attempt", attempt+1).Debug("MATLAB connection test failed, will retry")
}

return fmt.Errorf("MATLAB connection not ready after %d attempts", maxAttempts)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package localmatlabsession

import (
"os"
"runtime"

"github.com/matlab/matlab-mcp-core-server/internal/adaptors/matlabmanager/matlabservices/datatypes"
Expand Down Expand Up @@ -51,7 +52,8 @@ func NewStarter(
}

func (m *Starter) StartLocalMATLABSession(logger entities.Logger, request datatypes.LocalSessionDetails) (embeddedconnector.ConnectionDetails, func() error, error) {
logger.Debug("Starting a local MATLAB session")
logger = logger.With("mcp_server_pid", os.Getpid())
logger.With("matlab_root", request.MATLABRoot).With("show_desktop", request.ShowMATLABDesktop).Debug("StartLocalMATLABSession called")

sessionDir, err := m.directoryFactory.Create(logger)
if err != nil {
Expand Down Expand Up @@ -79,12 +81,16 @@ func (m *Starter) StartLocalMATLABSession(logger entities.Logger, request dataty
startupCode := "sessionPath = '" + sessionDirPath + "';addpath(sessionPath);matlab_mcp.initializeMCP();clear sessionPath;"

startupFlags := m.processDetails.StartupFlag(runtime.GOOS, request.ShowMATLABDesktop, startupCode)
logger.With("flags", startupFlags).Debug("MATLAB startup flags")

processID, processCleanup, err := m.matlabProcessLauncher.Launch(logger, sessionDirPath, request.MATLABRoot, request.StartingDirectory, startupFlags, env)
if err != nil {
logger.WithError(err).Error("Failed to launch MATLAB process")
return embeddedconnector.ConnectionDetails{}, nil, err
}

logger.With("matlab_process_id", processID).Debug("MATLAB process launched")

if err = m.watchdog.RegisterProcessPIDWithWatchdog(processID); err != nil {
logger.WithError(err).Warn("Failed to register process with watchdog")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ func (*ProcessDetails) StartupFlag(os string, showMATLAB bool, startupCode strin
"-noDisplayDesktop",
"-wait",
"-log",
"/minimize",
)
} else {
// Unix platforms (Linux/macOS)
startupFlags = append(startupFlags,
"-minimize",
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ func TestProcessDetails_StartupFlag_HappyPath(t *testing.T) {
"-nosplash",
"-softwareopengl",
"-nodesktop",
"-minimize",
},
},
{
Expand All @@ -209,6 +210,7 @@ func TestProcessDetails_StartupFlag_HappyPath(t *testing.T) {
"-noDisplayDesktop",
"-wait",
"-log",
"/minimize",
},
},
{
Expand All @@ -225,6 +227,7 @@ func TestProcessDetails_StartupFlag_HappyPath(t *testing.T) {
"-nosplash",
"-softwareopengl",
"-nodesktop",
"-minimize",
},
},
} {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ func startMatlab(logger entities.Logger, matlabRoot string, workingDir string, a
si.StdOutput = windows.Handle(stdIO.stdOut.Fd())
si.StdErr = windows.Handle(stdIO.stdErr.Fd())

// Set window to start minimized to reduce visual impact
// STARTF_USESHOWWINDOW = 0x00000001, SW_MINIMIZE = 6
si.Flags = 0x00000001 // STARTF_USESHOWWINDOW
si.ShowWindow = 6 // SW_MINIMIZE

creationFlags := uint32(windows.CREATE_NEW_PROCESS_GROUP | windows.DETACHED_PROCESS | windows.CREATE_UNICODE_ENVIRONMENT)

err = windows.CreateProcess(
Expand Down Expand Up @@ -85,9 +90,14 @@ func startMatlab(logger entities.Logger, matlabRoot string, workingDir string, a
return nil, fmt.Errorf("error finding MATLAB launcher process: %w", err)
}

// On Windows, the process we launch is a launcher process that then launches the actual MATLAB process.
// Therefore, we need to find the child process of the launcher process.
// THere should be only one process, and that would be the actual MATLAB process.
// On Windows, MATLAB uses a launcher architecture:
// - The MCP launches matlab.exe (launcher process)
// - The launcher then spawns MATLAB.exe (actual MATLAB process)
// - This is expected Windows behavior, not a bug
// - We need to find the child process of the launcher process to track the actual MATLAB instance
// - There should be only one child process, which is the actual MATLAB process
// Note: If you see TWO FULL MATLAB instances (not just matlab.exe + MATLAB.exe), this suggests
// multiple MCP server processes are running, which would cause duplicate session initialization.
matlabProcess, err := waitForMATLABProcess(logger, matlabLauncherProcess)
if err != nil {
return nil, err
Expand Down
Loading