chore: pimp my docker mcp

Kujtim Hoxha created

Change summary

internal/tui/components/chat/messages/docker_mcp.go | 188 +++++++++++++++
internal/tui/components/chat/messages/renderer.go   |  33 ++
internal/tui/components/chat/messages/tool.go       |   8 
internal/tui/components/mcp/mcp.go                  |   7 
internal/tui/styles/charmtone.go                    |   9 
internal/tui/styles/icons.go                        |   1 
internal/tui/styles/theme.go                        |   9 
7 files changed, 238 insertions(+), 17 deletions(-)

Detailed changes

internal/tui/components/chat/messages/docker_mcp.go πŸ”—

@@ -0,0 +1,188 @@
+package messages
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"charm.land/lipgloss/v2"
+	"charm.land/lipgloss/v2/table"
+	"github.com/charmbracelet/crush/internal/tui/styles"
+
+	"github.com/charmbracelet/crush/internal/stringext"
+)
+
+type dockerMCPRenderer struct {
+	baseRenderer
+}
+
+// Render displays file content with optional limit and offset parameters
+func (dr dockerMCPRenderer) Render(v *toolCallCmp) string {
+	var params map[string]any
+	if err := dr.unmarshalParams(v.call.Input, &params); err != nil {
+		return dr.renderError(v, "Invalid view parameters")
+	}
+
+	tool := strings.ReplaceAll(v.call.Name, "mcp_crush_docker_", "")
+
+	main := v.call.Input
+	extraArgs := map[string]string{}
+	switch tool {
+	case "mcp-find":
+		if query, ok := params["query"]; ok {
+			if qStr, ok := query.(string); ok {
+				main = qStr
+			}
+		}
+		for k, v := range params {
+			if k == "query" {
+				continue
+			}
+
+			data, _ := json.Marshal(v)
+			extraArgs[k] = string(data)
+		}
+	case "mcp-add":
+		if name, ok := params["name"]; ok {
+			if nStr, ok := name.(string); ok {
+				main = nStr
+			}
+		}
+		for k, v := range params {
+			if k == "name" {
+				continue
+			}
+
+			data, _ := json.Marshal(v)
+			extraArgs[k] = string(data)
+		}
+	case "mcp-remove":
+		if name, ok := params["name"]; ok {
+			if nStr, ok := name.(string); ok {
+				main = nStr
+			}
+		}
+		for k, v := range params {
+			if k == "name" {
+				continue
+			}
+
+			data, _ := json.Marshal(v)
+			extraArgs[k] = string(data)
+		}
+	}
+
+	args := newParamBuilder().
+		addMain(main)
+
+	for k, v := range extraArgs {
+		args.addKeyValue(k, v)
+	}
+
+	width := v.textWidth()
+	if v.isNested {
+		width -= 4 // Adjust for nested tool call indentation
+	}
+	header := dr.makeHeader(v, tool, width, args.build()...)
+	if v.isNested {
+		return v.style().Render(header)
+	}
+	if res, done := earlyState(header, v); done {
+		return res
+	}
+
+	if tool == "mcp-find" {
+		return joinHeaderBody(header, dr.renderMCPServers(v))
+	}
+	return joinHeaderBody(header, renderPlainContent(v, v.result.Content))
+}
+
+type FindMCPResponse struct {
+	Servers []struct {
+		Name        string `json:"name"`
+		Description string `json:"description"`
+	} `json:"servers"`
+}
+
+func (dr dockerMCPRenderer) renderMCPServers(v *toolCallCmp) string {
+	t := styles.CurrentTheme()
+	var result FindMCPResponse
+	if err := dr.unmarshalParams(v.result.Content, &result); err != nil {
+		return renderPlainContent(v, v.result.Content)
+	}
+
+	if len(result.Servers) == 0 {
+		return t.S().Muted.Render("No MCP servers found.")
+	}
+	width := min(120, v.textWidth())
+	rows := [][]string{}
+	moreServers := ""
+	for i, server := range result.Servers {
+		if i > 9 {
+			moreServers = t.S().Subtle.Render(fmt.Sprintf("... and %d mode", len(result.Servers)-10))
+			break
+		}
+		rows = append(rows, []string{t.S().Base.Render(server.Name), t.S().Muted.Render(server.Description)})
+
+	}
+	serverTable := table.New().
+		Wrap(false).
+		BorderTop(false).
+		BorderBottom(false).
+		BorderRight(false).
+		BorderLeft(false).
+		BorderColumn(false).
+		BorderRow(false).
+		StyleFunc(func(row, col int) lipgloss.Style {
+			if row == table.HeaderRow {
+				return lipgloss.NewStyle()
+			}
+			switch col {
+			case 0:
+				return lipgloss.NewStyle().PaddingRight(1)
+			}
+			return lipgloss.NewStyle()
+		}).Rows(rows...).Width(width)
+	if moreServers != "" {
+		return serverTable.Render() + "\n" + moreServers
+	}
+	return serverTable.Render()
+}
+
+func (dr dockerMCPRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string {
+	t := styles.CurrentTheme()
+	mainTool := "Docker MCP"
+	action := tool
+	actionStyle := t.S().Base.Foreground(t.BlueDark)
+	switch tool {
+	case "mcp-find":
+		action = "Find"
+	case "mcp-add":
+		action = "Add"
+		actionStyle = t.S().Base.Foreground(t.GreenLight)
+	case "mcp-remove":
+		action = "Remove"
+		actionStyle = t.S().Base.Foreground(t.RedLighter)
+	default:
+		action = strings.ReplaceAll(tool, "-", " ")
+		action = strings.ReplaceAll(tool, "_", " ")
+		action = stringext.Capitalize(tool)
+	}
+	if v.isNested {
+		return dr.makeNestedHeader(v, tool, width, params...)
+	}
+	icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
+	if v.result.ToolCallID != "" {
+		if v.result.IsError {
+			icon = t.S().Base.Foreground(t.RedDark).Render(styles.ToolError)
+		} else {
+			icon = t.S().Base.Foreground(t.Green).Render(styles.ToolSuccess)
+		}
+	} else if v.cancelled {
+		icon = t.S().Muted.Render(styles.ToolPending)
+	}
+	tool = t.S().Base.Foreground(t.Blue).Render(mainTool)
+	arrow := t.S().Base.Foreground(t.BlueDark).Render(styles.ArrowIcon)
+	prefix := fmt.Sprintf("%s %s %s %s ", icon, tool, arrow, actionStyle.Render(action))
+	return prefix + renderParamList(false, width-lipgloss.Width(prefix), params...)
+}

internal/tui/components/chat/messages/renderer.go πŸ”—

@@ -13,6 +13,7 @@ import (
 	"github.com/charmbracelet/crush/internal/agent/tools"
 	"github.com/charmbracelet/crush/internal/ansiext"
 	"github.com/charmbracelet/crush/internal/fsext"
+	"github.com/charmbracelet/crush/internal/stringext"
 	"github.com/charmbracelet/crush/internal/tui/components/core"
 	"github.com/charmbracelet/crush/internal/tui/highlight"
 	"github.com/charmbracelet/crush/internal/tui/styles"
@@ -946,7 +947,7 @@ func renderParamList(nested bool, paramsWidth int, params ...string) string {
 	}
 
 	if len(params) == 1 {
-		return t.S().Subtle.Render(mainParam)
+		return t.S().Muted.Render(mainParam)
 	}
 	otherParams := params[1:]
 	// create pairs of key/value
@@ -968,14 +969,16 @@ func renderParamList(nested bool, paramsWidth int, params ...string) string {
 	remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
 	if remainingWidth < 30 {
 		// No space for the params, just show the main
-		return t.S().Subtle.Render(mainParam)
+		return t.S().Muted.Render(mainParam)
 	}
 
 	if len(parts) > 0 {
-		mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
+		paramsStyles := t.S().Subtle.Render(fmt.Sprintf("(%s)", strings.Join(parts, ", ")))
+		mainParam = t.S().Muted.Render(mainParam)
+		mainParam = fmt.Sprintf("%s %s", mainParam, paramsStyles)
 	}
 
-	return t.S().Subtle.Render(ansi.Truncate(mainParam, paramsWidth, "…"))
+	return ansi.Truncate(mainParam, paramsWidth, "…")
 }
 
 // earlyState returns immediately‑rendered error/cancelled/ongoing states.
@@ -1159,6 +1162,25 @@ func truncateHeight(s string, h int) string {
 	return s
 }
 
+func mcpToolName(name string) string {
+	if strings.HasPrefix(name, "mcp_crush_docker") {
+		name = strings.ReplaceAll(name, "mcp_crush_docker_", "")
+		if name == "mcp-find" {
+			name = "find"
+		}
+		if name == "mcp-add" {
+			name = "add"
+		}
+		if name == "mcp-remove" {
+			name = "remove"
+		}
+		name = strings.ReplaceAll(name, "_", " ")
+		name = strings.ReplaceAll(name, "-", " ")
+		return "Docker MCP: " + stringext.Capitalize(name)
+	}
+	return name
+}
+
 func prettifyToolName(name string) string {
 	switch name {
 	case agent.AgentToolName:
@@ -1194,6 +1216,9 @@ func prettifyToolName(name string) string {
 	case tools.WriteToolName:
 		return "Write"
 	default:
+		if strings.HasPrefix(name, "mcp_") {
+			return mcpToolName(name)
+		}
 		return name
 	}
 }

internal/tui/components/chat/messages/tool.go πŸ”—

@@ -181,11 +181,13 @@ func (m *toolCallCmp) View() string {
 		return box.Render(m.renderPending())
 	}
 
-	r := registry.lookup(m.call.Name)
-
-	if m.isNested {
+	if strings.HasPrefix(m.call.Name, "mcp_crush_docker") {
+		r := dockerMCPRenderer{}
 		return box.Render(r.Render(m))
 	}
+
+	r := registry.lookup(m.call.Name)
+
 	return box.Render(r.Render(m))
 }
 

internal/tui/components/mcp/mcp.go πŸ”—

@@ -85,12 +85,15 @@ func RenderMCPList(opts RenderOptions) []string {
 		} else if l.MCP.Disabled {
 			description = t.S().Subtle.Render("disabled")
 		}
-
+		name := l.Name
+		if l.Name == "crush_docker" {
+			name = "Docker MCP"
+		}
 		mcpList = append(mcpList,
 			core.Status(
 				core.StatusOpts{
 					Icon:         icon.String(),
-					Title:        l.Name,
+					Title:        name,
 					Description:  description,
 					ExtraContent: strings.Join(extraContent, " "),
 				},

internal/tui/styles/charmtone.go πŸ”—

@@ -52,10 +52,11 @@ func NewCharmtoneTheme() *Theme {
 		GreenDark:  charmtone.Guac,
 		GreenLight: charmtone.Bok,
 
-		Red:      charmtone.Coral,
-		RedDark:  charmtone.Sriracha,
-		RedLight: charmtone.Salmon,
-		Cherry:   charmtone.Cherry,
+		Red:        charmtone.Coral,
+		RedDark:    charmtone.Sriracha,
+		RedLight:   charmtone.Salmon,
+		RedLighter: charmtone.Tuna,
+		Cherry:     charmtone.Cherry,
 	}
 
 	// Text selection.

internal/tui/styles/icons.go πŸ”—

@@ -10,6 +10,7 @@ const (
 	LoadingIcon  string = "⟳"
 	DocumentIcon string = "πŸ–Ό"
 	ModelIcon    string = "β—‡"
+	ArrowIcon           = "β†’"
 
 	// Tool call icons
 	ToolPending string = "●"

internal/tui/styles/theme.go πŸ”—

@@ -71,10 +71,11 @@ type Theme struct {
 	GreenLight color.Color
 
 	// Reds
-	Red      color.Color
-	RedDark  color.Color
-	RedLight color.Color
-	Cherry   color.Color
+	Red        color.Color
+	RedDark    color.Color
+	RedLight   color.Color
+	RedLighter color.Color
+	Cherry     color.Color
 
 	// Text selection.
 	TextSelection lipgloss.Style