Skip to content
Merged
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
19 changes: 19 additions & 0 deletions cmd/mcpproxy-tray/internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,25 @@ func (c *Client) RestartServer(serverName string) error {
return nil
}

// ForceReconnectAllServers triggers reconnection attempts for all upstream servers
func (c *Client) ForceReconnectAllServers(reason string) error {
endpoint := "/api/v1/servers/reconnect"
if reason != "" {
endpoint = endpoint + "?reason=" + url.QueryEscape(reason)
}

resp, err := c.makeRequest("POST", endpoint, nil)
if err != nil {
return err
}

if !resp.Success {
return fmt.Errorf("API error: %s", resp.Error)
}

return nil
}

// TriggerOAuthLogin triggers OAuth login for a server
func (c *Client) TriggerOAuthLogin(serverName string) error {
endpoint := fmt.Sprintf("/api/v1/servers/%s/login", serverName)
Expand Down
24 changes: 19 additions & 5 deletions cmd/mcpproxy-tray/internal/state/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ func (m *Machine) determineNewState(currentState State, event Event) State {
return StateCoreErrorPortConflict
case EventDBLocked:
return StateCoreErrorDBLocked
case EventDockerUnavailable:
return StateCoreErrorDocker
case EventConfigError:
return StateCoreErrorConfig
case EventPermissionError:
Expand All @@ -225,6 +227,8 @@ func (m *Machine) determineNewState(currentState State, event Event) State {
return StateCoreErrorPortConflict
case EventDBLocked:
return StateCoreErrorDBLocked
case EventDockerUnavailable:
return StateCoreErrorDocker
case EventConfigError:
return StateCoreErrorConfig
case EventPermissionError:
Expand Down Expand Up @@ -252,6 +256,8 @@ func (m *Machine) determineNewState(currentState State, event Event) State {
return StateCoreErrorPortConflict
case EventDBLocked:
return StateCoreErrorDBLocked
case EventDockerUnavailable:
return StateCoreErrorDocker
case EventConfigError:
return StateCoreErrorConfig
case EventPermissionError:
Expand Down Expand Up @@ -287,20 +293,28 @@ func (m *Machine) determineNewState(currentState State, event Event) State {
return StateShuttingDown
}

case StateCoreErrorPortConflict, StateCoreErrorDBLocked, StateCoreErrorGeneral:
case StateCoreErrorPortConflict, StateCoreErrorDBLocked, StateCoreErrorPermission, StateCoreErrorGeneral:
switch event {
case EventShutdown:
return StateShuttingDown
// Error states persist - require user to fix issue manually
// No auto-retry or auto-transition to failed state
// Error states persist - require user to fix issue manually
// No auto-retry or auto-transition to failed state
}

case StateCoreErrorConfig:
switch event {
case EventShutdown:
return StateShuttingDown
// Config errors persist - require user to fix config manually
// Stay in StateCoreErrorConfig for all other events
// Config errors persist - require user to fix config manually
// Stay in StateCoreErrorConfig for all other events
}

case StateCoreErrorDocker:
switch event {
case EventRetry:
return StateLaunchingCore
case EventShutdown:
return StateShuttingDown
}

case StateFailed:
Expand Down
46 changes: 42 additions & 4 deletions cmd/mcpproxy-tray/internal/state/states.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ const (
// StateCoreErrorDBLocked represents core failed due to database lock
StateCoreErrorDBLocked State = "core_error_db_locked"

// StateCoreErrorDocker represents core failed due to Docker being unavailable
StateCoreErrorDocker State = "core_error_docker"

// StateCoreRecoveringDocker represents Docker recovery in progress
StateCoreRecoveringDocker State = "core_recovering_docker"

// StateCoreErrorConfig represents core failed due to configuration error
StateCoreErrorConfig State = "core_error_config"

Expand Down Expand Up @@ -84,6 +90,12 @@ const (
// EventPermissionError indicates core failed due to permission error
EventPermissionError Event = "permission_error"

// EventDockerUnavailable indicates Docker engine is unavailable or paused
EventDockerUnavailable Event = "docker_unavailable"

// EventDockerRecovered indicates Docker engine became available again
EventDockerRecovered Event = "docker_recovered"

// EventGeneralError indicates core failed with general error
EventGeneralError Event = "general_error"

Expand Down Expand Up @@ -112,7 +124,7 @@ type Info struct {

// GetInfo returns metadata for a given state
func GetInfo(state State) Info {
timeout90s := 90 * time.Second // Must exceed health monitor's readinessTimeout (60s)
timeout90s := 90 * time.Second // Must exceed health monitor's readinessTimeout (60s)
timeout5s := 5 * time.Second
timeout10s := 10 * time.Second

Expand All @@ -133,7 +145,7 @@ func GetInfo(state State) Info {
Name: StateWaitingForCore,
Description: "Waiting for core to become ready",
UserMessage: "Core starting up...",
Timeout: &timeout90s, // Increased to 90s to allow Docker isolation startup (health timeout is 60s)
Timeout: &timeout90s, // Increased to 90s to allow Docker isolation startup (health timeout is 60s)
},
StateConnectingAPI: {
Name: StateConnectingAPI,
Expand Down Expand Up @@ -176,6 +188,20 @@ func GetInfo(state State) Info {
CanRetry: false,
// No timeout - config errors persist until user fixes the config
},
StateCoreErrorDocker: {
Name: StateCoreErrorDocker,
Description: "Docker engine unavailable or paused",
UserMessage: "Docker engine unavailable - resume Docker Desktop",
IsError: true,
CanRetry: true,
},
StateCoreRecoveringDocker: {
Name: StateCoreRecoveringDocker,
Description: "Docker recovery in progress",
UserMessage: "Docker engine recovered - reconnecting servers...",
CanRetry: false,
Timeout: &timeout10s,
},
StateCoreErrorPermission: {
Name: StateCoreErrorPermission,
Description: "Core failed due to permission error",
Expand Down Expand Up @@ -229,6 +255,7 @@ func CanTransition(from, to State) bool {
StateWaitingForCore,
StateCoreErrorPortConflict,
StateCoreErrorDBLocked,
StateCoreErrorDocker,
StateCoreErrorConfig,
StateCoreErrorGeneral,
StateShuttingDown,
Expand All @@ -237,7 +264,8 @@ func CanTransition(from, to State) bool {
StateConnectingAPI,
StateCoreErrorPortConflict, // ADD: Handle port conflict
StateCoreErrorDBLocked, // ADD: Handle DB lock
StateCoreErrorConfig, // ADD: Handle config error
StateCoreErrorDocker,
StateCoreErrorConfig, // ADD: Handle config error
StateCoreErrorGeneral,
StateLaunchingCore, // Retry
StateShuttingDown,
Expand All @@ -247,7 +275,8 @@ func CanTransition(from, to State) bool {
StateReconnecting,
StateCoreErrorPortConflict, // ADD: Handle port conflict during connection
StateCoreErrorDBLocked, // ADD: Handle DB lock during connection
StateCoreErrorConfig, // ADD: Handle config error during connection
StateCoreErrorDocker,
StateCoreErrorConfig, // ADD: Handle config error during connection
StateCoreErrorGeneral,
StateShuttingDown,
},
Expand All @@ -273,6 +302,15 @@ func CanTransition(from, to State) bool {
// Error persists - only shutdown allowed
StateShuttingDown,
},
StateCoreErrorDocker: {
StateCoreRecoveringDocker, // Transition to recovering when Docker comes back
StateShuttingDown,
},
StateCoreRecoveringDocker: {
StateLaunchingCore, // Launch core after Docker recovery
StateCoreErrorDocker, // Back to error if Docker fails again
StateShuttingDown,
},
StateCoreErrorGeneral: {
// Error persists - only shutdown allowed
StateShuttingDown,
Expand Down
Loading
Loading