fix(mcp/docker): only write config after docker startup succeeds

Christian Rocha created

Change summary

internal/config/docker_mcp.go | 32 +++++++++++++++++++++++---------
internal/ui/model/ui.go       | 14 ++++++++++++--
2 files changed, 35 insertions(+), 11 deletions(-)

Detailed changes

internal/config/docker_mcp.go 🔗

@@ -34,30 +34,44 @@ func (c *Config) IsDockerMCPEnabled() bool {
 	return exists
 }
 
-// EnableDockerMCP adds Docker MCP configuration and persists it.
-func (s *ConfigStore) EnableDockerMCP() error {
-	if !IsDockerMCPAvailable() {
-		return fmt.Errorf("docker mcp is not available, please ensure docker is installed and 'docker mcp version' succeeds")
-	}
-
-	mcpConfig := MCPConfig{
+func DockerMCPConfig() MCPConfig {
+	return MCPConfig{
 		Type:     MCPStdio,
 		Command:  "docker",
 		Args:     []string{"mcp", "gateway", "run"},
 		Disabled: false,
 	}
+}
+
+func (s *ConfigStore) PrepareDockerMCPConfig() (MCPConfig, error) {
+	if !IsDockerMCPAvailable() {
+		return MCPConfig{}, fmt.Errorf("docker mcp is not available, please ensure docker is installed and 'docker mcp version' succeeds")
+	}
 
-	// Add to in-memory config.
+	mcpConfig := DockerMCPConfig()
 	if s.config.MCP == nil {
 		s.config.MCP = make(map[string]MCPConfig)
 	}
 	s.config.MCP[DockerMCPName] = mcpConfig
+	return mcpConfig, nil
+}
 
-	// Persist to config file.
+func (s *ConfigStore) PersistDockerMCPConfig(mcpConfig MCPConfig) error {
 	if err := s.SetConfigField(ScopeGlobal, "mcp."+DockerMCPName, mcpConfig); err != nil {
 		return fmt.Errorf("failed to persist docker mcp configuration: %w", err)
 	}
+	return nil
+}
 
+// EnableDockerMCP adds Docker MCP configuration and persists it.
+func (s *ConfigStore) EnableDockerMCP() error {
+	mcpConfig, err := s.PrepareDockerMCPConfig()
+	if err != nil {
+		return err
+	}
+	if err := s.PersistDockerMCPConfig(mcpConfig); err != nil {
+		return err
+	}
 	return nil
 }
 

internal/ui/model/ui.go 🔗

@@ -3465,16 +3465,26 @@ func (m *UI) copyChatHighlight() tea.Cmd {
 
 func (m *UI) enableDockerMCP() tea.Msg {
 	store := m.com.Store()
-	if err := store.EnableDockerMCP(); err != nil {
+	// Stage Docker MCP in memory first so startup and persistence can be atomic.
+	mcpConfig, err := store.PrepareDockerMCPConfig()
+	if err != nil {
 		return util.ReportError(err)()
 	}
 
-	// Initialize the Docker MCP client immediately.
 	ctx := context.Background()
 	if err := mcp.InitializeSingle(ctx, config.DockerMCPName, store); err != nil {
+		// Roll back in-memory state when startup fails.
+		delete(store.Config().MCP, config.DockerMCPName)
 		return util.ReportError(fmt.Errorf("docker MCP enabled but failed to start: %w", err))()
 	}
 
+	if err := store.PersistDockerMCPConfig(mcpConfig); err != nil {
+		// Roll back runtime and in-memory state if persistence fails.
+		_ = mcp.DisableSingle(store, config.DockerMCPName)
+		delete(store.Config().MCP, config.DockerMCPName)
+		return util.ReportError(fmt.Errorf("docker MCP started but failed to persist configuration: %w", err))()
+	}
+
 	return util.NewInfoMsg("Docker MCP enabled and started successfully")
 }