@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os/exec"
+ "sync"
"time"
)
@@ -12,6 +13,15 @@ var dockerMCPVersionRunner = func(ctx context.Context) error {
return cmd.Run()
}
+const dockerMCPAvailabilityTTL = 10 * time.Second
+
+var dockerMCPAvailabilityCache struct {
+ mu sync.Mutex
+ available bool
+ checkedAt time.Time
+ known bool
+}
+
// DockerMCPName is the name of the Docker MCP configuration.
const DockerMCPName = "docker"
@@ -25,6 +35,29 @@ func IsDockerMCPAvailable() bool {
return err == nil
}
+func DockerMCPAvailabilityCached() (available bool, known bool) {
+ dockerMCPAvailabilityCache.mu.Lock()
+ defer dockerMCPAvailabilityCache.mu.Unlock()
+
+ if !dockerMCPAvailabilityCache.known {
+ return false, false
+ }
+ if time.Since(dockerMCPAvailabilityCache.checkedAt) > dockerMCPAvailabilityTTL {
+ return dockerMCPAvailabilityCache.available, false
+ }
+ return dockerMCPAvailabilityCache.available, true
+}
+
+func RefreshDockerMCPAvailability() bool {
+ available := IsDockerMCPAvailable()
+ dockerMCPAvailabilityCache.mu.Lock()
+ dockerMCPAvailabilityCache.available = available
+ dockerMCPAvailabilityCache.checkedAt = time.Now()
+ dockerMCPAvailabilityCache.known = true
+ dockerMCPAvailabilityCache.mu.Unlock()
+ return available
+}
+
// IsDockerMCPEnabled checks if Docker MCP is already configured.
func (c *Config) IsDockerMCPEnabled() bool {
if c.MCP == nil {
@@ -37,6 +37,10 @@ const (
)
// Commands represents a dialog that shows available commands.
+type dockerMCPAvailabilityCheckedMsg struct {
+ available bool
+}
+
type Commands struct {
com *common.Common
keyMap struct {
@@ -66,6 +70,9 @@ type Commands struct {
customCommands []commands.CustomCommand
mcpPrompts []commands.MCPPrompt
+
+ dockerMCPAvailable *bool
+ dockerMCPCheckInFlight bool
}
var _ Dialog = (*Commands)(nil)
@@ -126,6 +133,10 @@ func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, has
closeKey.SetHelp("esc", "cancel")
c.keyMap.Close = closeKey
+ if available, known := config.DockerMCPAvailabilityCached(); known {
+ c.dockerMCPAvailable = &available
+ }
+
// Set initial commands
c.setCommandItems(c.selected)
@@ -145,6 +156,13 @@ func (c *Commands) ID() string {
// HandleMsg implements [Dialog].
func (c *Commands) HandleMsg(msg tea.Msg) Action {
switch msg := msg.(type) {
+ case dockerMCPAvailabilityCheckedMsg:
+ c.dockerMCPAvailable = &msg.available
+ c.dockerMCPCheckInFlight = false
+ if c.selected == SystemCommands {
+ c.setCommandItems(c.selected)
+ }
+ return nil
case spinner.TickMsg:
if c.loading {
var cmd tea.Cmd
@@ -207,6 +225,20 @@ func (c *Commands) HandleMsg(msg tea.Msg) Action {
return nil
}
+func checkDockerMCPAvailabilityCmd() tea.Cmd {
+ return func() tea.Msg {
+ return dockerMCPAvailabilityCheckedMsg{available: config.RefreshDockerMCPAvailability()}
+ }
+}
+
+func (c *Commands) InitialCmd() tea.Cmd {
+ if c.dockerMCPAvailable != nil || c.dockerMCPCheckInFlight {
+ return nil
+ }
+ c.dockerMCPCheckInFlight = true
+ return checkDockerMCPAvailabilityCmd()
+}
+
// Cursor returns the cursor position relative to the dialog.
func (c *Commands) Cursor() *tea.Cursor {
return InputCursor(c.com.Styles, c.input.Cursor())
@@ -446,8 +478,8 @@ func (c *Commands) defaultCommands() []*CommandItem {
commands = append(commands, NewCommandItem(c.com.Styles, "open_external_editor", "Open External Editor", "ctrl+o", ActionExternalEditor{}))
}
- // Add Docker MCP command if available and not already enabled
- if config.IsDockerMCPAvailable() && !cfg.IsDockerMCPEnabled() {
+ // Add Docker MCP command if available and not already enabled.
+ if !cfg.IsDockerMCPEnabled() && c.dockerMCPAvailable != nil && *c.dockerMCPAvailable {
commands = append(commands, NewCommandItem(c.com.Styles, "enable_docker_mcp", "Enable Docker MCP Catalog", "", ActionEnableDockerMCP{}))
}
@@ -3017,7 +3017,7 @@ func (m *UI) openCommandsDialog() tea.Cmd {
m.dialog.OpenDialog(commands)
- return nil
+ return commands.InitialCmd()
}
// openReasoningDialog opens the reasoning effort dialog.
@@ -3473,16 +3473,17 @@ func (m *UI) enableDockerMCP() tea.Msg {
ctx := context.Background()
if err := mcp.InitializeSingle(ctx, config.DockerMCPName, store); err != nil {
- // Roll back in-memory state when startup fails.
+ // Roll back runtime and in-memory state when startup fails.
+ disableErr := mcp.DisableSingle(store, config.DockerMCPName)
delete(store.Config().MCP, config.DockerMCPName)
- return util.ReportError(fmt.Errorf("docker MCP enabled but failed to start: %w", err))()
+ return util.ReportError(fmt.Errorf("failed to start docker MCP: %w", errors.Join(err, disableErr)))()
}
if err := store.PersistDockerMCPConfig(mcpConfig); err != nil {
// Roll back runtime and in-memory state if persistence fails.
- _ = mcp.DisableSingle(store, config.DockerMCPName)
+ disableErr := 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.ReportError(fmt.Errorf("docker MCP started but failed to persist configuration: %w", errors.Join(err, disableErr)))()
}
return util.NewInfoMsg("Docker MCP enabled and started successfully")