From 4254d56b2e390bb0087e2f48a399149519e95512 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Mon, 1 Dec 2025 19:30:53 +0100 Subject: [PATCH] chore: pimp my docker mcp --- .../components/chat/messages/docker_mcp.go | 188 ++++++++++++++++++ .../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(-) create mode 100644 internal/tui/components/chat/messages/docker_mcp.go diff --git a/internal/tui/components/chat/messages/docker_mcp.go b/internal/tui/components/chat/messages/docker_mcp.go new file mode 100644 index 0000000000000000000000000000000000000000..707eb71effb0c6573bab5cf7220a3bcae6a3c334 --- /dev/null +++ b/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, ¶ms); 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...) +} diff --git a/internal/tui/components/chat/messages/renderer.go b/internal/tui/components/chat/messages/renderer.go index 72631d2a8e5b77b7e7e664798735a1e134fad6d5..5f0fcd8f318806ffd5424feee9eed2f153e35003 100644 --- a/internal/tui/components/chat/messages/renderer.go +++ b/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 } } diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index 123d02e63c76687d4a722957a5b363082c9a3670..6f297a82ccda53fed0f41b3f210b524330cbf710 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/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)) } diff --git a/internal/tui/components/mcp/mcp.go b/internal/tui/components/mcp/mcp.go index 782a776c5eefb946e0b858f6711bc5ec0ac705fd..cc23fa55624cfefa256a506ac0b4109e79d65dae 100644 --- a/internal/tui/components/mcp/mcp.go +++ b/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, " "), }, diff --git a/internal/tui/styles/charmtone.go b/internal/tui/styles/charmtone.go index 44508e5a24e68ea0507af0f2649ddc372711104d..1d7627720b2b48cc436bf1865328604864247ff8 100644 --- a/internal/tui/styles/charmtone.go +++ b/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. diff --git a/internal/tui/styles/icons.go b/internal/tui/styles/icons.go index d9d1ab06f96ff64f8772e1b0f4b099a0ebed2b0a..60ec34dd8fb6f7062196063414f8b7f5e815fdb3 100644 --- a/internal/tui/styles/icons.go +++ b/internal/tui/styles/icons.go @@ -10,6 +10,7 @@ const ( LoadingIcon string = "⟳" DocumentIcon string = "🖼" ModelIcon string = "◇" + ArrowIcon = "→" // Tool call icons ToolPending string = "●" diff --git a/internal/tui/styles/theme.go b/internal/tui/styles/theme.go index 9605dd2d4ed8ffcdf35a2b8e524747d0cc983bc9..1476cd156a39abced3d401b159eefe6fcbaaeb19 100644 --- a/internal/tui/styles/theme.go +++ b/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